记 Vue 表单的一个性能问题

Ever-Lose  |  2017. 11. 02   |  阅读 3356 次
无线开发

背景

产品反馈表单页太卡了,这是一个有意思的情况,让我看看。

如图所见,当在 input 输入数据的时候,连续输入会感觉明显的延迟。

qq20200209-153426.gif

那个项目最多情况下,表单数量达到千数。笔者在 demo 里简化实现,并把表单数量提升到 10000,把下面的代码粘贴运行一边就能得到卡顿效果。

<!DOCTYPE html>  
<html>  
<head>  
    <title>Form Demo</title>
</head>  
<body>  
    <div id="app">
        <template v-for="item in options">
            <input type="text" v-model="item.data">    
        </template>
    </div>

    <!-- Vue.js v2.6.11 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        let options = []
        for (let i = 0; i < 10000; i++) {
            options.push({
                data: '',
            })
        }
        var app = new Vue({
            el: '#app',
            data: {
                options: options,
            },
        })
        window.app = app;
        console.log(app);

        // 接着控制台里输入
        // var event = new CustomEvent('test', { 'detail': ['a', 'c', 'e', 'f', 'b', 'd'] }); window.dispatchEvent(event);
        // 能把 message 改为这个数组
    </script>
</body>  
</html>  

前置知识梳理

众所周知,vue2 里的数据使用 Object.defineProperty 设定 get/set 来进行劫持,而当数据改变时,将会触发 set,在 set 里触发广播通知被观察者进行更新的。

Object.defineProperty(obj, key, {  
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
        // ...
    },
    set: function reactiveSetter (newVal) {
        // ...
        dep.notify();
    }
})

// ------ dep 的结构如下
/*{
    id: 118,
    subs: [Watcher]
}*/

// ------- Watcher 的结构
/*{
    vm,
    cb,
    deps,
    express,
    ...
}*/

Vue 把更新收集到队列里,并每隔一段时间去执行,一般是这些被观察者 Watcher 的表达式。

总之在这里执行的更新语句是

expression: "function () { vm._update(vm._render(), hydrating); }"  

其中 _render 执行完后得出此组件的 Vnode,并传给 Vue.prototype._update 语句进行更新。

ok,知识梳理完毕,那么到底 _update 里怎么做的,让页面更新渲染如此之慢呢?

调试过程

Vue.prototype._update 打断点调试,如下图:

image

省略函数进入步骤 patch -> patchVnode -> updateChildren,直到 updateChildren 发现核心比对逻辑

image

比对这 10000 个节点。简单来说相同 key 和标签名的被判定为相同节点,相同节点还得继续去递归比对其子节点是否相同。并且比对过程中,还需要判定更新的内容里有 attr、 class、listener、style 等等信息,由此产生的计算量还是挺大的。笔者下图与本次调试无关,但能简单揭示下比对逻辑。

image

笔者在这里 pachVnode 里执行 updateChildren 的地方,打印耗时,发现当 input 为 1000 项的时候每输入一个字符耗时一般是个位数的毫秒。

image

而 input 为 10000 项时,每个字符输入响应需要 50~100 毫秒的话,快速输入一串字符,产生的卡顿感就会比较厉害。

image

而在我们实际的项目中,表单复杂的多,比对的层级深,或许 1000 不到的表单就能产生这样的效果。

解决

既然更新 10000 个节点费力,那何不缩小更新范围呢。把表单拆成若干组,每组包裹在组件中,输入时只会更新那个组件,影响范围就笑得多。由此产生的更新如下:

<!DOCTYPE html>  
<html>  
<head>  
    <title>Form Demo</title>
</head>  
<body>  
    <div id="app">
        <input-group :forms="forms" v-for="(forms, index) in options" :key="index"></input-group>
    </div>

    <!-- Vue.js v2.6.11 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
        Vue.component('input-group', {
            props: ['forms'],
            template: `<div>
                <template v-for="item in forms">
                    <input type="text" v-model="item.data">
                </template>
            </div>`
        })

        let options = []
        for (let i = 0; i < 100; i++) {
            for (let j = 0; j < 100; j++) {
                options[i] = options[i] || [];
                options[i].push({
                    data: '',
                })
            }

        }
        var app = new Vue({
            el: '#app',
            data: {
                options: options,
            },
        })
        window.app = app;
        console.log(app);

        // 接着控制台里输入
        // var event = new CustomEvent('test', { 'detail': ['a', 'c', 'e', 'f', 'b', 'd'] }); window.dispatchEvent(event);
        // 能把 message 改为这个数组
    </script>
</body>  
</html>  

每个字符的更新就降低到 3ms 的样子,响应快得多了。

总结

本质上这就是一个原则,不要在一个 vue 组件上绑定那么多的元素,请拆分成多个子组件。。

分享到

   
MobX 绑定过程和其中的一些坑的总结
加入我们