记 vue 表单的一个性能问题
背景
产品反馈表单页太卡了,这是一个有意思的情况,让我看看。
如图所见,当在 input 输入数据的时候,连续输入会感觉明显的延迟。
那个项目最多情况下,表单数量达到千数。笔者在 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
打断点调试,如下图:
省略函数进入步骤 patch -> patchVnode -> updateChildren,直到 updateChildren 发现核心比对逻辑
比对这 10000 个节点。简单来说相同 key 和标签名的被判定为相同节点,相同节点还得继续去递归比对其子节点是否相同。并且比对过程中,还需要判定更新的内容里有 attr、 class、listener、style 等等信息,由此产生的计算量还是挺大的。笔者下图与本次调试无关,但能简单揭示下比对逻辑。
笔者在这里 pachVnode 里执行 updateChildren 的地方,打印耗时,发现当 input 为 1000 项的时候每输入一个字符耗时一般是个位数的毫秒。
而 input 为 10000 项时,每个字符输入响应需要 50~100 毫秒的话,快速输入一串字符,产生的卡顿感就会比较厉害。
而在我们实际的项目中,表单复杂的多,比对的层级深,或许 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 组件上绑定那么多的元素,请拆分成多个子组件。。