背景
產品反饋表單頁太卡了,這是一個有意思的情況,讓我看看。
如圖所見,當在 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 組件上綁定那么多的元素,請拆分成多個子組件。。