Before I developed a background management system, which used the component library Vue and Element-UI, I encountered a very interesting problem, and I would like to share it with you.

Here’s the scenario: on a list display page, I use the Element-UI table component, and the new requirement is to support drag-and-drop sorting on top of the original table. However, the original component itself does not support drag and drop sorting, and since it is directly introduced into the Element-UI, it is not convenient to modify its source code, so the more feasible method is to directly manipulate the DOM.

In the Mounted lifecycle function, perform a real DOM operation on this.$el, listen for the drag’s events, move the DOM in the event callback, and update the data.

HTML5 Drag events are similar to Touch events, but we can also implement them manually, but we can also use an open source Sortable library to pass this.$el directly, listen to the package after the callback, and according to the development mode of Vue. Update the actual Data in the move DOM callback to keep the Data consistent with the DOM.

If you think this is the end of it, you are wrong, stolen laziness will be returned sooner or later… I thought this scheme was very beautiful, but when I tried to debug it, A strange phenomenon appeared: after A and B drag and switch positions, B and A magically changed back! What’s going on here? There seems to be nothing wrong with our operation. After the actual DOM is moved, we also move the corresponding data, and the order of the data array should be the same as the order in which the DOM was rendered.

What’s the problem?

Recall how Vue was implemented. Prior to Vue2.0, bidirectional binding was implemented through defineProperty dependency injection and tracing. For v-for array instructions, if a unique Key is specified, the difference between the elements in the array is calculated using an efficient Diff algorithm with minimal movement or deletion. However, after the introduction of Virtual Dom after Vue2.0, the Dom Diff algorithm of Children element is actually similar to the former. The only difference is that before 2.0, Diff is directly aimed at the array object of V-for instruction, and after 2.0, it is aimed at Virtual Dom. The DOM Diff algorithm is not described here. The virtual DOM Diff algorithm is explained clearly here

Let’s say our array of list elements is

[' A ','B'.'C'.'D']

Copy the code

The rendered DOM node is

[$A.$B.$C.$D]

Copy the code

So the structure of the Virtual Dom is

[{elm:$A,data:'A'},

 {elm:$B,data:'B'},

 {elm:$C,data:'C'},

 {elm:$D,data:'D'}]

Copy the code

Suppose that after drag sort, the real DOM becomes

[$B.$A.$C.$D]

Copy the code

At this point we have only manipulated the real DOM and repositioned it, while the structure of the Virtual DOM remains unchanged

[{elm:$A,data:'A'},

 {elm:$B,data:'B'},

 {elm:$C,data:'C'},

 {elm:$D,data:'D'}]

Copy the code

At this point, we also sort the list elements according to the real DOM

[' B ','A'.'C'.'D']

Copy the code

At this time, according to the Diff algorithm, the Patch calculated is. The first two terms of VNode are nodes of the same type, so the Patch is directly updated, that is, the Patch is directly updatedB,A, the real DOM is back

[$A.$B.$C.$D]

Copy the code

Therefore, the problem of drag and drop being updated by Patch algorithm appears. The operation path can be simply understood as

Drag and drop to move the real DOM -> manipulate data array -> Patch algorithm to update the real DOM

Root cause The root cause is an inconsistency between the Virtual DOM and the real DOM. So before Vue2.0, this problem did not exist because the Virtual DOM was not introduced. Try to avoid direct DOM manipulation when using the Vue framework

The solution is to set a key that uniquely identifies each VNode, which is how Vue recommends using the V-for directive. The sameVnode method is called when checking whether two VNodes are of the same type

function sameVnode (a, b{

  return (

    a.key === b.key &&

    a.tag === b.tag &&

    a.isComment === b.isComment &&

    isDef(a.data) === isDef(b.data) &&

    sameInputType(a, b)

  )

}

Copy the code

Since the root cause is the inconsistency between the real DOM and VNode, it can be restored by dragging and moving the real DOM in the callback function

[$B.$A.$C.$D]

Copy the code

Back into

[$A.$B.$C.$D]

Copy the code

Let the DOM operation back to Vue drag and move the real DOM -> restore the move operation -> manipulate the data array -> Patch algorithm to update the real DOM code as follows

var app = new Vue({

  el'#app'.

  mounted:function(){

      var $ul = this.$el.querySelector('#ul')

      var that = this

      new Sortable($ul, {

          onUpdate:function(event){

              var newIndex = event.newIndex,

                  oldIndex = event.oldIndex

                  $li = $ul.children[newIndex],

                  $oldLi = $ul.children[oldIndex]

              // Delete the moved node first

              $ul.removeChild($li)    

              // Insert the moved node into the original node to restore the moved operation

              if(newIndex > oldIndex) {

                  $ul.insertBefore($li,$oldLi)

              } else {

                  $ul.insertBefore($li,$oldLi.nextSibling)

              }

              // Update the items array

              var item = that.items.splice(oldIndex,1)

              that.items.splice(newIndex,0,item[0])

              // The next tick will be the patch update

          }

      })

  },

  data:function({

      return {

          message'Hello Vue! '.

          items: [{

              key:'1'.

              name:'1'

}, {

              key:'2'.

              name:'2'

}, {

              key:'3'.

              name:'3'

}, {

              key:'4'.

              name:'4'

          }]

      }

  },

  watch: {

      items:function(){

          console.log(this.items.map(item= > item.name))

      }

  }

})

Copy the code

3) Violent solution! Do not go patch update, through v-if Settings, directly re-render. Of course, I don’t recommend doing this, but I just offer this idea

mounted:function(){

    var $ul = this.$el.querySelector('#ul')

    var that = this

    var updateFunc = function(event){

        var newIndex = event.newIndex,

            oldIndex = event.oldIndex

        var item = that.items.splice(oldIndex,1)

        that.items.splice(newIndex,0,item[0])



        // Violence rerenders!

        that.reRender = false

        // Re-render with nextTick and V-if

        that.$nextTick(function(){

            that.reRender = true

            that.$nextTick(function(){

                // After rerendering, rebind Sortable

                new Sortable(that.$el.querySelector('#ul'), {

                    onUpdate:updateFunc

                })

            })

        })

    }

    new Sortable($ul, {

        onUpdate:updateFunc

    })

},

Copy the code

Therefore, we usually in the use of the framework, but also to understand the implementation principle of the framework, otherwise encounter some tricky situations will be unable to start ~

Source: author: Wen Xing links: https://www.jianshu.com/p/d92b9efe3e6a Jane books copyright owned by the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.