問題場景
身為一個表單表格工程師呵晚,自然日復(fù)一日的寫著表單表格坦袍,本以為已經(jīng)沒啥難點的時候轉(zhuǎn)眼間就來了一個有意思的情況弧岳,在超大量 數(shù)據(jù)綁定在 vue 的時候出現(xiàn)了表單操作起來卡頓的情況。
這里先貼上本項目出現(xiàn)的情況演示的 github 上的地址,tag1.0.1(https://github.com/everlose/more-form-demo/tree/v1.0.1)
如圖所見酱酬,當(dāng)在 input 輸入數(shù)據(jù)的時候壹无,連續(xù)輸入會感覺明顯的延遲。
那么,這到底是怎么回事勺届?
代碼
上述的表單數(shù)據(jù)項修改頻繁由后端返回,于是在前端需要渲染從后端返回的 68kb 的一個 JSON 數(shù)據(jù)串娶耍,包括所有配置表單項以及其可能的選項值免姿,數(shù)據(jù)見這里
核心渲染是有這么一段
??
????{{config.title}}
????
????????????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">
????????????????v-if="item.type === 'radio'"
????????????????v-model="formData[item.code]">
????????????????????v-for="(option, radioIndex) in formOptions[item.optionCode]"
????????????????????:key="option.value"
????????????????????:label="option.value"
????????????????????:disabled="item.disabled">
????????????????????{{?option.label?}}
????????????????v-else-if="item.type === 'input'"
????????????????:class="{ longInput: item.isLongInput }"
????????????????:placeholder="item.placeholder || '請輸入'"
????????????????v-model="formData[item.code]"
????????????????:label="item.label"
????????????????:disabled="item.disabled"
????????????????:maxlength="item.maxLength">
????????????????v-else-if="item.type === 'select'"
????????????????v-model="formData[item.code]"
????????????????:disabled="item.disabled"
????????????????:placeholder="item.placeholder || '請選擇'">
????????????????????v-for="(option, optionsIndex) in formOptions[item.optionCode]"
????????????????????:key="option.value"
????????????????????:label="option.label"
????????????????????:value="option.value">
這就是一個簡單的雙層遍歷渲染所有表單配置項的模版代碼,其中的 formConfig 正是所有配置表單項榕酒,數(shù)據(jù)量極多胚膊。formOptions 掛載了所有表單選項值,也是動輒幾千項想鹰。
思路
正當(dāng)我對著這么高的操作延時發(fā)愁的時候紊婉,組里一個大佬提醒我,可能是 Vue.prototype._update 這個觸發(fā)的太頻繁了辑舷。
我急忙找到這一段打了個斷點調(diào)試
Vue.prototype._update 這函數(shù)里觸發(fā)的是 VNode 虛擬節(jié)點的比對更新喻犁,打斷點調(diào)試后發(fā)現(xiàn)實際上這是一個循環(huán),在控制臺里輸出 this.$el 的時候能得到正在深度遍歷中的節(jié)點何缓,沿著根結(jié)點 App(也是 formConfig 數(shù)據(jù)綁定的作用域) 開始直到具體觸發(fā)輸入的那個表單元素株汉。
在本項目里是使用了遍歷輸出所有的表單元素,并且當(dāng)前組件的作用域是直接掛在根結(jié)點上的歌殃,是否就是這個遍歷引發(fā)了如此高的延時呢?于是我找到上圖右側(cè)的調(diào)用堆棧蝙云,發(fā)現(xiàn)正是 flushSchedulerQueue 函數(shù)寫著一個 for 循環(huán)氓皱。
在 flushSchedulerQueue 函數(shù)中的 for 循環(huán)里頭尾插入代碼來獲取耗費時間。
結(jié)果得知輸入時的延遲大概在 300ms 之上勃刨。
似乎問題就找到了波材,flushSchedulerQueue 函數(shù)針對 data 中數(shù)據(jù)的修改把 watcher 推送進隊列里在更新,這一循環(huán)耗費的時間比較長身隐。
解決
其實早在調(diào)試 Vue.prototype._update 函數(shù)就初見端倪廷区,循環(huán)中的 this.$el 從當(dāng)前組件的根部開始深度遍歷,遍歷了太多次贾铝,那么只要想辦法縮小當(dāng)前組件所綁定的數(shù)據(jù)量就解決了隙轻。
于是核心代碼調(diào)整為
? ? ??
只是用一個 edit-form 包裹剛剛所有的 el-form-item 的渲染代碼就解決了,再次調(diào)試 Vue.prototype._update 得出遍歷節(jié)點 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ì)上這就是一個原則,最好不要在一個vue組件上直接綁定如此多的數(shù)據(jù)叁巨,如果有大量數(shù)據(jù)請分多個組件綁定斑匪。這么淺嘗輒止實在讓人不夠盡興,于是這里貼上 Vue.prototype._update 前的關(guān)鍵部分調(diào)用堆棧以及其函數(shù)作用锋勺。
找到項目中 node_modules 下的 vue.esm.js
# 往input里輸入將會觸發(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實際上是Watcher對象的實例狡蝶,這里觸發(fā)視圖更新操作
677?subs[i].update();?subs實際上是包裹watcher的數(shù)組??
3093?Watcher.prototype.update??
# 把watcher塞進一個隊列里,這里是和異步更新視圖有關(guān)贮勃。
3100?queueWatcher(this);??
2945?function?queueWatcher?(watcher)?push到隊列里??
# nextTick是具體做異步更新的部分
2963?nextTick(flushSchedulerQueue);??
1778?function?nextTick?(cb,?ctx)??
# 異步操作實際上是原生 H5 MessageChannel API 通道通信來推送消息來實現(xiàn)變化贪惹。
1738?port.postMessage(1);
# 注意在異步操作中,最終傳入的回調(diào)函數(shù)被執(zhí)行來進行下面視圖的更新衙猪。這里是執(zhí)行一個任務(wù)調(diào)度隊列的調(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);??
# 進入vue的生命周期中的update函數(shù)
2548?Vue.prototype._update??
# patch做的是vnode的節(jié)點比對丝格,最終把新的vnode結(jié)構(gòu)渲染到具體視圖,不再多做描述棵譬。
2572?vm.$el?=?vm.__patch__(prevVnode,?vnode);??
貼上提供思路的大佬的github地址: https://github.com/answershuto