
1.異步說明
Vue 實作響應式并 不是資料發生變化之後 DOM 立即變化 ,而是按一定的政策進行 DOM 的更新。
2.事件循環說明
簡單來說,Vue在修改資料後,視圖不會立刻更新,而是等
同一事件循環中的所有資料變化完成之後,再統一進行視圖更新。
二、Vue.nextTick 的機制
1、為什麼用Vue.nextTick()
首先來了解一下JS的運作機制。
JS運作機制(Event Loop)
JS執行是單線程的,它是基于事件循環的。- 所有同步任務都在主線程上執行,形成一個執行棧。
- 主線程之外,會存在一個任務隊列,隻要異步任務有了結果,就在任務隊列中放置一個事件。
- 當執行棧中的所有同步任務執行完後,就會讀取任務隊列。那些對應的異步任務,會結束等待狀态,進入執行棧。
- 主線程不斷重複第三步。
這裡主線程的執行過程就是一個
tick
,而所有的異步結果都是通過任務隊列來排程。
Event Loop
分為宏任務和微任務,無論是執行宏任務還是微任務,完成後都會進入到一下
tick
,
并在兩個tick
之間進行UI渲染 。
由于Vue DOM更新是異步執行的,即修改資料時,視圖不會立即更新,而是會監聽資料變化,并緩存在同一事件循環中,等同一資料循環中的所有資料變化完成之後,再統一進行視圖更新。為了確定得到更新後的DOM,是以設定了
Vue.nextTick()
方法。
2、什麼是Vue.nextTick()
是Vue的核心方法之一,官方文檔解釋如下:
在下次DOM更新循環結束之後執行延遲回調。在修改資料之後立即使用這個方法,擷取更新後的DOM。
MutationObserver
先簡單介紹下
MutationObserver
:MO是HTML5中的API,是一個用于監視DOM變動的接口,它可以監聽一個DOM對象上發生的子節點删除、屬性修改、文本内容修改等。
調用過程是要先給它綁定回調,得到MO執行個體,這個回調會在MO執行個體監聽到變動時觸發。這裡MO的回調是放在
microtask
中執行的。
// 建立MO執行個體
const observer = new MutationObserver(callback)
const textNode = '想要監聽的Don節點'
observer.observe(textNode, {
characterData: true // 說明監聽文本内容的修改
})
源碼淺析
nextTick
的實作單獨有一個JS檔案來維護它,在
src/core/util/next-tick.js
中。
nextTick
源碼主要分為兩塊:能力檢測和根據能力檢測以不同方式執行回調隊列。
能力檢測
由于宏任務耗費的時間是大于微任務的,是以在浏覽器支援的情況下,優先使用微任務。如果浏覽器不支援微任務,再使用宏任務。
// 空函數,可用作函數占位符
import { noop } from 'shared/util'
// 錯誤處理函數
import { handleError } from './error'
// 是否是IE、IOS、内置函數
import { isIE, isIOS, isNative } from './env'
// 使用 MicroTask 的辨別符,這裡是因為火狐在<=53時 無法觸發微任務,在modules/events.js檔案中引用進行安全排除
export let isUsingMicroTask = false
// 用來存儲所有需要執行的回調函數
const callbacks = []
// 用來标志是否正在執行回調函數
let pending = false
// 對callbacks進行周遊,然後執行相應的回調函數
function flushCallbacks () {
pending = false
// 這裡拷貝的原因是:
// 有的cb 執行過程中又會往callbacks中加入内容
// 比如 $nextTick的回調函數裡還有$nextTick
// 後者的應該放到下一輪的nextTick 中執行
// 是以拷貝一份目前的,周遊執行完目前的即可,避免無休止的執行下去
const copies = callbcks.slice(0)
callbacks.length = 0
for(let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc // 異步執行函數 用于異步延遲調用 flushCallbacks 函數
// 在2.5中,我們使用(宏)任務(與微任務結合使用)。
// 然而,當狀态在重新繪制之前發生變化時,就會出現一些微妙的問題
// (例如#6813,out-in轉換)。
// 同樣,在事件處理程式中使用(宏)任務會導緻一些奇怪的行為
// 是以,我們現在再次在任何地方使用微任務。
// 優先使用 Promise
if(typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// IOS 的UIWebView, Promise.then 回調被推入 microTask 隊列,但是隊列可能不會如期執行
// 是以,添加一個空計時器強制執行 microTask
if(isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if(!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString === '[object MutationObserverConstructor]')) {
// 當 原生Promise 不可用時,使用 原生MutationObserver
// e.g. PhantomJS, iOS7, Android 4.4
let counter = 1
// 建立MO執行個體,監聽到DOM變動後會執行回調flushCallbacks
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true // 設定true 表示觀察目标的改變
})
// 每次執行timerFunc 都會讓文本節點的内容在 0/1之間切換
// 切換之後将新值複制到 MO 觀測的文本節點上
// 節點内容變化會觸發回調
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter) // 觸發回調
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
延遲調用優先級如下:
Promise > MutationObserver > setImmediate > setTimeout
export function nextTick(cb? Function, ctx: Object) {
let _resolve
// cb 回調函數會統一處理壓入callbacks數組
callbacks.push(() => {
if(cb) {
try {
cb.call(ctx)
} catch(e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// pending 為false 說明本輪事件循環中沒有執行過timerFunc()
if(!pending) {
pending = true
timerFunc()
}
// 當不傳入 cb 參數時,提供一個promise化的調用
// 如nextTick().then(() => {})
// 當_resolve執行時,就會跳轉到then邏輯中
if(!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
next-tick.js
對外暴露了
nextTick
這一個參數,是以每次調用
Vue.nextTick
時會執行:
- 把傳入的回調函數
壓入cb
數組callbacks
- 執行
函數,延遲調用timerFunc
函數flushCallbacks
- 周遊執行
數組中的所有函數callbacks
這裡的
callbacks
沒有直接在
nextTick
中執行回調函數的原因是保證在同一個
tick
内多次執行
nextTick
,不會開啟多個異步任務,而是把這些異步任務都壓成一個同步任務,在下一個
tick
執行完畢。
附加
noop
的定義如下
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
export function noop (a?: any, b?: any, c?: any) {}
3、怎麼用
文法:
Vue.nextTick([callback, context])
參數 :
-
:回調函數,不傳時提供promise調用{Function} [callback]
-
:回調函數執行的上下文環境,不傳預設是自動綁定到調用它的執行個體上。{Object} [context]
//改變資料
vm.message = 'changed'
//想要立即使用更新後的DOM。這樣不行,因為設定message後DOM還沒有更新
console.log(vm.$el.textContent) // 并不會得到'changed'
//這樣可以,nextTick裡面的代碼會在DOM更新後執行
Vue.nextTick(function(){
// DOM 更新了
//可以得到'changed'
console.log(vm.$el.textContent)
})
// 作為一個 Promise 使用 即不傳回調
Vue.nextTick()
.then(function () {
// DOM 更新了
})
Vue執行個體方法
vm.$nextTick
做了進一步封裝,把context參數設定成目前Vue執行個體。
4、小結
使用
Vue.nextTick()
是為了可以擷取更新後的DOM 。
觸發時機:在同一事件循環中的資料變化後,DOM完成更新,立即執行
Vue.nextTick()
的回調。
同一事件循環中的代碼執行完畢 -> DOM 更新 -> nextTick callback觸發
應用場景:
- 在Vue生命周期的
鈎子函數進行的DOM操作一定要放在created()
的回調函數中。 原因 :是Vue.nextTick()
鈎子函數執行時DOM其實并未進行渲染。created()
- 在資料變化後要執行的某個操作,而這個操作需要使用随資料改變而改變的DOM結構的時候,這個操作應該放在
的回調函數中。 原因 :Vue異步執行DOM更新,隻要觀察到資料變化,Vue将開啟一個隊列,并緩沖在同一事件循環中發生的所有資料改變,如果同一個watcher被多次觸發,隻會被推入到隊列中一次。Vue.nextTick()
版本分析
2.6 版本優先使用 microtask 作為異步延遲包裝器,且寫法相對簡單。而2.5 版本中,nextTick 的實作是 microTimerFunc、macroTimerFunc 組合實作的,延遲調用優先級是:Promise > setImmediate > MessageChannel > setTimeout,具體見源碼。
2.5 版本在重繪之前狀态改變時會有小問題(如 #6813)。此外,在事件處理程式中使用 macrotask 會導緻一些無法規避的奇怪行為(如 #7109,#7153等)。
microtask 在某些情況下也是會有問題的,因為 microtask 優先級比較高,事件會在順序事件(如#4521,#6690 有變通方法)之間甚至在同一事件的冒泡過程中觸發(#6566)。