Vue中使用Sortable

作者: 文兴 | 来源:发表于2018-04-04 22:56 被阅读268次

    之前开发一个后台管理系统,里面用到了VueElement-UI这个组件库,遇到一个挺有意思的问题,和大家分享一下。

    场景是这样,在一个列表展示页上,我使用了Element-UI的表格组件,新的需求是在原表格的基础上支持拖拽排序。但是原有的组件本身不支持拖拽排序,而且由于是直接引入的Element-UI,不方便修改它的源码,所以比较可行的方法只能是直接操作DOM

    具体的做法是在mounted生命周期函数里,对this.$el进行真实DOM的操作,监听drag的一系列事件,在事件回调里移动DOM,并更新data。

    HTML5 Drag事件还是挺多的,和Touch事件差不多,自己手工实现也可以,不过这里就偷了个懒,直接用了一个开源的Sortable库,直接传入this.$el,监听封装后的回调,并且根据Vue的开发模式,在移动DOM的回调里更新实际的Data数据,保持数据和DOM的一致性

    如果你以为到这就结束了,那就大错特错,偷过的懒迟早要还。。。本以为这个方案是很美好的,没想到刚想调试一下,就出现了诡异的现象:A和B拖拽交换位置之后,B和A又神奇得换回去了!这是怎么回事?似乎我们的操作没有什么问题,在真实DOM移动了之后,我们也移动了相应的data,数据数组的顺序和渲染出DOM的顺序应该是一致的。

    问题出在哪里?我们回忆一下Vue的实现原理,在Vue2.0之前是通过defineProperty依赖注入和跟踪的方式实现双向绑定。针对v-for数组指令,如果指定了唯一的Key,则会通过高效的Diff算法计算出数组内元素的差异,进行最少的移动或删除操作。而Vue2.0之后在引入了Virtual Dom之后,Children元素的Dom Diff算法和前者其实是相似的,唯一的区别就是,2.0之前Diff直接针对v-for指令的数组对象,2.0之后则针对Virtual Dom。DOM Diff算法在这里不再赘述,这里解释的比较清楚virtual-dom diff算法

    假设我们的列表元素数组是

    [‘A’,'B','C','D']
    

    渲染出来后的DOM节点是

    [$A,$B,$C,$D]
    

    那么Virtual Dom对应的结构就是

    [{elm:$A,data:'A'},
     {elm:$B,data:'B'},
     {elm:$C,data:'C'},
     {elm:$D,data:'D'}]
    

    假设拖拽排序之后,真实的DOM变为

    [$B,$A,$C,$D]
    

    此时我们只操作了真实DOM,改编了它的位置,而Virtual Dom的结构并没有改变,依然是

    [{elm:$A,data:'A'},
     {elm:$B,data:'B'},
     {elm:$C,data:'C'},
     {elm:$D,data:'D'}]
    

    此时我们把列表元素也按照真实DOM排序后变成

    [‘B’,'A','C','D']
    

    这时候根据Diff算法,计算出的Patch为,VNode前两项是同类型的节点,所以直接更新,即把$A节点更新成$B,把$B节点更新成$A,真实DOM又变回了

    [$A,$B,$C,$D]
    

    所以就出现了拖拽之后又被Patch算法更新了一次的问题,操作路径可以简单理解为

    拖拽移动真实DOM -> 操作数据数组 -> Patch算法再更新真实DOM

    根本原因

    根本原因是Virtual DOM真实DOM之间出现了不一致。
    所以在Vue2.0以前,因为没有引入Virtual DOM,这个问题是不存在的。
    在使用Vue框架的时候要尽量避免直接操作DOM

    解决方案

    1. 通过设置key唯一标志每一个VNode,这也是Vue推荐的使用v-for指令的方式。因为在判断两个VNode是否为同类型时会调用sameVnode方法,优先判断key是否相同
    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)
      )
    }
    
    1. 因为根本原因是真实DOM和VNode不一致,所以可以通过把拖拽移动真实DOM的操作还原,即在回调函数里,把[$B,$A,$C,$D]还原成[$A,$B,$C,$D],让DOM的操作交还给Vue

    拖拽移动真实DOM ->还原移动操作 -> 操作数据数组 -> Patch算法再更新真实DOM
    代码如下

    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]
                        // 先删除移动的节点
                        $ul.removeChild($li)    
                        // 再插入移动的节点到原有节点,还原了移动的操作
                        if(newIndex > oldIndex) {
                            $ul.insertBefore($li,$oldLi)
                        } else {
                            $ul.insertBefore($li,$oldLi.nextSibling)
                        }
                        // 更新items数组
                        var item = that.items.splice(oldIndex,1)
                        that.items.splice(newIndex,0,item[0])
                        // 下一个tick就会走patch更新
                    }
                })
            },
            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))
                }
            }
        })
    

    3.暴力解决!不走patch更新,通过v-if设置,直接重新渲染一遍。当然不建议这么做,只是提供这种思路~

            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])
    
                    // 暴力重新渲染!
                    that.reRender = false
                    // 借助nextTick和v-if重新渲染
                    that.$nextTick(function(){
                        that.reRender = true
                        that.$nextTick(function(){
                            // 重新渲染之后,重新进行Sortable绑定
                            new Sortable(that.$el.querySelector('#ul'), {
                                onUpdate:updateFunc
                            })
                        })
                    })
                }
                new Sortable($ul, {
                    onUpdate:updateFunc
                })
            },
    

    所以,我们平时在使用框架的时候,也要去了解框架的实现原理的,否则遇到一些棘手的情况就会无从下手~

    相关文章

      网友评论

      • 大山的那边:虚拟dom,真是dom,现在推荐的是绑定key,可以不写的时候绑定,这个bug就不会出现了吧
      • 淼先生:解决思路就是:先用sortable拖动真实dom实现视觉意义上的拖动排序,然后操作渲染数组,通过排序原始数组实现真正意义的dom渲染变动。

        这样理解没毛病吧?
        文兴:sortable不仅仅是视觉意义上的拖动,它会改变真实DOM的排序,所以操作渲染数组之前要还原排序后的DOM为排序前的状态

      本文标题:Vue中使用Sortable

      本文链接:https://www.haomeiwen.com/subject/cnvbcxtx.html