問題場(chǎng)景
身為一個(gè)表單表格工程師础芍,自然日復(fù)一日的寫著表單表格,本以為已經(jīng)沒啥難點(diǎn)的時(shí)候轉(zhuǎn)眼間就來了一個(gè)有意思的情況贤惯,在超大量 數(shù)據(jù)綁定在 vue 的時(shí)候出現(xiàn)了表單操作起來卡頓的情況菜皂。
這里先貼上本項(xiàng)目出現(xiàn)的情況演示的 github 上的地址,tag1.0.1(https://github.com/everlose/more-form-demo/tree/v1.0.1)
如圖所見镇草,當(dāng)在 input 輸入數(shù)據(jù)的時(shí)候眶痰,連續(xù)輸入會(huì)感覺明顯的延遲。
那么梯啤,這到底是怎么回事竖伯?
代碼
上述的表單數(shù)據(jù)項(xiàng)修改頻繁由后端返回,于是在前端需要渲染從后端返回的 68kb 的一個(gè) JSON 數(shù)據(jù)串,包括所有配置表單項(xiàng)以及其可能的選項(xiàng)值七婴,數(shù)據(jù)見這里
核心渲染是有這么一段
<div class="basic-info ct-form" v-for="(config, configIndex) in formConfig" :key="configIndex">
<h3 class="form__title">{{config.title}}</h3> <el-form class="form-content" ref="form" label-width="150px"> <el-form-item class="basic-form-item" v-for="(item, itemIndex) in config.formItems" :key="itemIndex" :prop="item.code" :label="item.name" :required="item.required" :rules="item.rules"> <el-radio-group v-if="item.type === 'radio'" v-model="formData[item.code]"> <el-radio v-for="(option, radioIndex) in formOptions[item.optionCode]" :key="option.value" :label="option.value" :disabled="item.disabled"> {{ option.label }} </el-radio> </el-radio-group> <el-input v-else-if="item.type === 'input'" :class="{ longInput: item.isLongInput }" :placeholder="item.placeholder || '請(qǐng)輸入'" v-model="formData[item.code]" :label="item.label" :disabled="item.disabled" :maxlength="item.maxLength"> </el-input> <el-select v-else-if="item.type === 'select'" v-model="formData[item.code]" :disabled="item.disabled" :placeholder="item.placeholder || '請(qǐng)選擇'"> <el-option v-for="(option, optionsIndex) in formOptions[item.optionCode]" :key="option.value" :label="option.label" :value="option.value"> </el-option> </el-select> </el-form-item> </el-form>
</div>
這就是一個(gè)簡(jiǎn)單的雙層遍歷渲染所有表單配置項(xiàng)的模版代碼祟偷,其中的 formConfig 正是所有配置表單項(xiàng),數(shù)據(jù)量極多打厘。formOptions 掛載了所有表單選項(xiàng)值修肠,也是動(dòng)輒幾千項(xiàng)。
思路
正當(dāng)我對(duì)著這么高的操作延時(shí)發(fā)愁的時(shí)候婚惫,組里一個(gè)大佬提醒我氛赐,可能是 Vue.prototype._update 這個(gè)觸發(fā)的太頻繁了。
我急忙找到這一段打了個(gè)斷點(diǎn)調(diào)試
Vue.prototype._update 這函數(shù)里觸發(fā)的是 VNode 虛擬節(jié)點(diǎn)的比對(duì)更新先舷,打斷點(diǎn)調(diào)試后發(fā)現(xiàn)實(shí)際上這是一個(gè)循環(huán)艰管,在控制臺(tái)里輸出 this.$el 的時(shí)候能得到正在深度遍歷中的節(jié)點(diǎn),沿著根結(jié)點(diǎn) App(也是 formConfig 數(shù)據(jù)綁定的作用域) 開始直到具體觸發(fā)輸入的那個(gè)表單元素蒋川。
在本項(xiàng)目里是使用了遍歷輸出所有的表單元素牲芋,并且當(dāng)前組件的作用域是直接掛在根結(jié)點(diǎn)上的,是否就是這個(gè)遍歷引發(fā)了如此高的延時(shí)呢捺球?于是我找到上圖右側(cè)的調(diào)用堆棧缸浦,發(fā)現(xiàn)正是 flushSchedulerQueue 函數(shù)寫著一個(gè) for 循環(huán)。
在 flushSchedulerQueue 函數(shù)中的 for 循環(huán)里頭尾插入代碼來獲取耗費(fèi)時(shí)間氮兵。
結(jié)果得知輸入時(shí)的延遲大概在 300ms 之上裂逐。
似乎問題就找到了,flushSchedulerQueue 函數(shù)針對(duì) data 中數(shù)據(jù)的修改把 watcher 推送進(jìn)隊(duì)列里在更新泣栈,這一循環(huán)耗費(fèi)的時(shí)間比較長(zhǎng)卜高。
解決
其實(shí)早在調(diào)試 Vue.prototype._update 函數(shù)就初見端倪,循環(huán)中的 this.$el 從當(dāng)前組件的根部開始深度遍歷南片,遍歷了太多次掺涛,那么只要想辦法縮小當(dāng)前組件所綁定的數(shù)據(jù)量就解決了。
于是核心代碼調(diào)整為
<div class="basic-info ct-form" v-for="(config, configIndex) in formConfig" :key="configIndex">
<edit-form :config="config" :data="formData" :options="formOptions"></edit-form></div>
只是用一個(gè) edit-form 包裹剛剛所有的 el-form-item 的渲染代碼就解決了疼进,再次調(diào)試 Vue.prototype._update 得出遍歷節(jié)點(diǎn) this.$el 已經(jīng)變?yōu)橄聢D所示的 div.edit-form 了薪缆,flushSchedulerQueue 函數(shù) for 循環(huán)的延遲也變?yōu)?10ms 左右
修復(fù)版的代碼在2.0.0的tag上,這里貼上鏈接(https://github.com/everlose/more-form-demo/tree/v2.0.0)
后記
本質(zhì)上這就是一個(gè)原則伞广,最好不要在一個(gè)vue組件上直接綁定如此多的數(shù)據(jù)拣帽,如果有大量數(shù)據(jù)請(qǐng)分多個(gè)組件綁定。這么淺嘗輒止實(shí)在讓人不夠盡興嚼锄,于是這里貼上 Vue.prototype._update 前的關(guān)鍵部分調(diào)用堆棧以及其函數(shù)作用诞外。
找到項(xiàng)目中 node_modules 下的 vue.esm.js
往input里輸入將會(huì)觸發(fā)model data的更新
978 set: function reactiveSetter (newVal)
訂閱器dep是數(shù)據(jù)綁定和視圖更新的關(guān)鍵,這里觸發(fā)去通知相關(guān)視圖的更新
994 dep.notify();
673 Dep.prototype.notify
notify函數(shù)里的subs實(shí)際上是Watcher對(duì)象的實(shí)例灾票,這里觸發(fā)視圖更新操作
677 subs[i].update(); subs實(shí)際上是包裹watcher的數(shù)組
3093 Watcher.prototype.update
把watcher塞進(jìn)一個(gè)隊(duì)列里,這里是和異步更新視圖有關(guān)茫虽。
3100 queueWatcher(this);
2945 function queueWatcher (watcher) push到隊(duì)列里
nextTick是具體做異步更新的部分
2963 nextTick(flushSchedulerQueue);
1778 function nextTick (cb, ctx)
異步操作實(shí)際上是原生 H5 MessageChannel API 通道通信來推送消息來實(shí)現(xiàn)變化刊苍。
1738 port.postMessage(1);
注意在異步操作中既们,最終傳入的回調(diào)函數(shù)被執(zhí)行來進(jìn)行下面視圖的更新。這里是執(zhí)行一個(gè)任務(wù)調(diào)度隊(duì)列的調(diào)度過程正什,需要循環(huán)遍歷啥纸。
2856 function flushSchedulerQueue
3108 Watcher.prototype.run
Evaluate the getter, and re-collect dependencies.
3043 Watcher.prototype.get
watcher中的getter的name就叫updateComponent,于是被執(zhí)行
2689 updateComponent
2690 vm._update(vm._render(), hydrating);
進(jìn)入vue的生命周期中的update函數(shù)
2548 Vue.prototype._update
patch做的是vnode的節(jié)點(diǎn)比對(duì)婴氮,最終把新的vnode結(jié)構(gòu)渲染到具體視圖斯棒,不再多做描述。
2572 vm.$el = vm.patch(prevVnode, vnode);
貼上提供思路的大佬的github地址: https://github.com/answershuto