VUE的nextTick
一、定義[nextTick、事件迴圈]
nextTick的由來:
由於VUE的資料驅動檢視更新,是非同步的,即修改資料的當下,檢視不會立刻更新,而是等同一事件迴圈中的所有資料變化完成之後,再統一進行檢視更新。
nextTick的觸發時機:
在同一事件迴圈中的資料變化後,DOM完成更新,立即執行nextTick(callback)內的回撥。
應用場景:
需要在檢視更新之後,基於新的檢視進行操作。
以上出現了事件迴圈的概念,其涉及到JS的執行機制,包括主執行緒的執行棧、非同步佇列、非同步API、事件迴圈的協作,此處不展開之後再總結。大致理解:主執行緒完成同步環境執行,查詢任務佇列,提取隊首的任務,放入主執行緒中執行;執行完畢,再重複該操作,該過程稱為事件迴圈。而主執行緒的每次讀取任務佇列操作,是一個事件迴圈的開始。非同步callback不可能處在同一事件迴圈中。
簡單總結事件迴圈:
同步程式碼執行 -> 查詢非同步佇列,推入執行棧,執行callback1[事件迴圈1] ->查詢非同步佇列,推入執行棧,執行callback2[事件迴圈2]...
即每個非同步callback,最終都會形成自己獨立的一個事件迴圈。
結合nextTick的由來,可以推出每個事件迴圈中,nextTick觸發的時機:
同一事件迴圈中的程式碼執行完畢 -> DOM 更新 -> nextTick callback觸發
tips:本文的任務佇列、訊息佇列、非同步佇列指同一個東西,均指macrotask queue。
二、例項理解nextTick的使用,並給出在頁面渲染上的優化巧用
(tips:程式碼的正確閱讀方式:看template組成、跳過script程式碼、看程式碼後面的用例設計、看之後的程式碼分析、同時結合回頭結合script程式碼理解)
<template> <div> <ul> <li v-for="item in list1">{{item}}</li> </ul> <ul> <li v-for="item in list2">{{item}}</li> </ul> <ol> <li v-for="item in list3">{{item}}</li> </ol> <ol> <li v-for="item in list4">{{item}}</li> </ol> <ol> <li v-for="item in list5">{{item}}</li> </ol> </div> </template> <script type="text/javascript"> export default { data() { return { list1: [], list2: [], list3: [], list4: [], list5: [] } }, created() { this.composeList12() this.composeList34() this.composeList5() this.$nextTick(function() { // DOM 更新了 console.log('finished test ' + new Date().toString()) console.log(document.querySelectorAll('li').length) }) }, methods: { composeList12() { let me = this let count = 10000 for (let i = 0; i < count; i++) { Vue.set(me.list1, i, 'I am a 測試資訊~~啦啦啦' + i) } console.log('finished list1 ' + new Date().toString()) for (let i = 0; i < count; i++) { Vue.set(me.list2, i, 'I am a 測試資訊~~啦啦啦' + i) } console.log('finished list2 ' + new Date().toString()) this.$nextTick(function() { // DOM 更新了 console.log('finished tick1&2 ' + new Date().toString()) console.log(document.querySelectorAll('li').length) }) }, composeList34() { let me = this let count = 10000 for (let i = 0; i < count; i++) { Vue.set(me.list3, i, 'I am a 測試資訊~~啦啦啦' + i) } console.log('finished list3 ' + new Date().toString()) this.$nextTick(function() { // DOM 更新了 console.log('finished tick3 ' + new Date().toString()) console.log(document.querySelectorAll('li').length) }) setTimeout(me.setTimeout1, 0) }, setTimeout1() { let me = this let count = 10000 for (let i = 0; i < count; i++) { Vue.set(me.list4, i, 'I am a 測試資訊~~啦啦啦' + i) } console.log('finished list4 ' + new Date().toString()) me.$nextTick(function() { // DOM 更新了 console.log('finished tick4 ' + new Date().toString()) console.log(document.querySelectorAll('li').length) }) }, composeList5() { let me = this let count = 10000 this.$nextTick(function() { // DOM 更新了 console.log('finished tick5-1 ' + new Date().toString()) console.log(document.querySelectorAll('li').length) }) setTimeout(me.setTimeout2, 0) }, setTimeout2() { let me = this let count = 10000 for (let i = 0; i < count; i++) { Vue.set(me.list5, i, 'I am a 測試資訊~~啦啦啦' + i) } console.log('finished list5 ' + new Date().toString()) me.$nextTick(function() { // DOM 更新了 console.log('finished tick5 ' + new Date().toString()) console.log(document.querySelectorAll('li').length) }) } } } </script>
2.1、用例設計
用例1:通過list1、2、3驗證,處在同步程式碼中的DOM更新情況及nextTick的觸發時機;
用例2:通過list3、list4驗證,同步程式碼及非同步程式碼中Dom更新及nextTick觸發的區別;
用例3:通過list4、list5對比驗證,多個非同步程式碼中nextTick觸發的區別;
用例4:通過在檢視更新後獲取DOM中<li>的數量,判斷nextTick序列渲染的時間點。
2.2、程式碼分析
函式執行步驟:
事件迴圈1:
step1: this.composeList12() -> update list1, update list2 -> 繫結tick’1&2’
step2: this.composeList34() -> update list3, 設定非同步1setTimeout1 -> 繫結tick’3’
step3: this.composeList5() -> 繫結tick’5-1’ -> 設定非同步2setTimeout2
step4: 繫結tick’test’
事件迴圈2:
將setTimeout1的callback推入執行棧 -> update list4 -> 繫結tick’4’
事件迴圈3:
將setTimeout2的callback推入執行棧 -> update list5 -> 繫結tick’5’
2.3、推斷輸出訊息
由於同一事件迴圈中的tick按執行順序,因此訊息輸出為即:
[同步環境]update list1 -> update list2 -> update list3 -> tick‘1&2’ -> tick‘3’ -> tick’5-1’ -> tick’test'
[事件迴圈1]->update list4 -> tick’4’
[事件迴圈2] ->update list5 -> tick’5’
2.4、實際執行結果如下圖
該demo中,設定了5個size為10000的陣列,從而能從時間及訊息輸出兩個維度來了解nextTick的執行情況。另外,額外增加了一個引數,即更新後的檢視中<li>的數量,從這個數量,可以考察出同一事件迴圈中的nextTick執行情況。由執行結果圖可以看出實際的輸出與推導的輸出結果相符合。
2.5、總結
從用例1得出:
a、在同一事件迴圈中,只有所有的資料更新完畢,才會呼叫nextTick;
b、僅在同步執行環境資料完全更新完畢,DOM才開始渲染,頁面才開始展現;
c、在同一事件迴圈中,如果存在多個nextTick,將會按最初的執行順序進行呼叫;
從用例1+用例4得出:
d、從同步執行環境中的四個tick對應的‘li’數量均為30000可看出,同一事件迴圈中,nextTick所在的檢視是相同的;
從用例2得出:
e、只有同步環境執行完畢,DOM渲染完畢之後,才會處理非同步callback
從用例3得出:
f、每個非同步callback最後都會處在一個獨立的事件迴圈中,對應自己獨立的nextTick;
從用例1結論中可得出:
g、這個事件環境中的資料變化完成,在進行渲染[檢視更新],可以避免DOM的頻繁變動,從而避免了因此帶來的瀏覽器卡頓,大幅度提升效能;
從b可以得出:
h、在首屏渲染、使用者互動過程中,要巧用同步環境及非同步環境;首屏展現的內容,儘量保證在同步環境中完成;其他內容,拆分到非同步中,從而保證效能、體驗。
tips:
1、可產生非同步callback的有:promise(microtask queue)、setTimeout、MutationObserver、DOM事件、Ajax等;
2、 vue DOM的檢視更新實現,,使用到了ES6的Promise及HTML5的MutationObserver,當環境不支援時,使用setTimeout(fn, 0)替代。上述的三種方法,均為非同步API。其中MutationObserver類似事件,又有所區別;事件是同步觸發,其為非同步觸發,即DOM發生變化之後,不會立刻觸發,等當前所有的DOM操作都結束後觸發。關於非同步API、事件迴圈將在以後補充。