詳解Vue的異步更新實現(xiàn)原理
最近面試總是會被問到這么一個問題:在使用vue的時候,將for循環(huán)中聲明的變量i從1增加到100,然后將i展示到頁面上,頁面上的i是從1跳到100,還是會怎樣?答案當然是只會顯示100,并不會有跳轉的過程。
怎么可以讓頁面上有從1到100顯示的過程呢,就是用setTimeout或者Promise.then等方法去模擬。
講道理,如果不在vue里,單獨運行這段程序的話,輸出一定是從1到100,但是為什么在vue中就不一樣了呢?
for(let i=1; i<=100; i++){
console.log(i);
}
這就涉及到Vue底層的異步更新原理,也要說一說nextTick的實現(xiàn)。不過在說nextTick之前,有必要先介紹一下JS的事件運行機制。
JS運行機制
眾所周知,JS是基于事件循環(huán)的單線程的語言。 執(zhí)行的步驟大致是:
- 當代碼執(zhí)行時,所有同步的任務都在主線程上執(zhí)行,形成一個執(zhí)行棧;
- 在主線程之外還有一個任務隊列(task queue),只要異步任務有了運行結果就在任務隊列中放置一個事件;
- 一旦執(zhí)行棧中所有同步任務執(zhí)行完畢(主線程代碼執(zhí)行完畢),此時主線程不會空閑而是去讀取任務隊列。此時,異步的任務就結束等待的狀態(tài)被執(zhí)行。
- 主線程不斷重復以上的步驟。

我們把主線程執(zhí)行一次的過程叫一個tick,所以nextTick就是下一個tick的意思,也就是說用nextTick的場景就是我們想在下一個tick做一些事的時候。
所有的異步任務結果都是通過任務隊列來調度的。而任務分為兩類:宏任務(macro task)和微任務(micro task)。它們之間的執(zhí)行規(guī)則就是每個宏任務結束后都要將所有微任務清空。 常見的宏任務有setTimeout/MessageChannel/postMessage/setImmediate,微任務有MutationObsever/Promise.then。
想要透徹學習事件循環(huán),推薦Jake在JavaScript全球開發(fā)者大會的演講,保證講懂!
nextTick原理
派發(fā)更新
大家都知道vue的響應式的靠依賴收集和派發(fā)更新來實現(xiàn)的。在修改數(shù)組之后的派發(fā)更新過程,會觸發(fā)setter的邏輯,執(zhí)行dep.notify():
// src/core/observer/watcher.js
class Dep {
notify() {
//subs是Watcher的實例數(shù)組
const subs = this.subs.slice()
for(let i=0, l=subs.length; i<l; i++){
subs[i].update()
}
}
}
遍歷subs里每一個Watcher實例,然后調用實例的update方法,下面我們來看看update是怎么去更新的:
class Watcher {
update() {
...
//各種情況判斷之后
else{
queueWatcher(this)
}
}
}
update執(zhí)行后又走到了queueWatcher,那就繼續(xù)去看看queueWatcher干啥了(希望不要繼續(xù)套娃了:
//queueWatcher 定義在 src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
//根據(jù)id是否重復做優(yōu)化
if(has[id] == null){
has[id] = true
if(!flushing){
queue.push(watcher)
}else{
let i=queue.length - 1
while(i > index && queue[i].id > watcher.id){
i--
}
queue.splice(i + 1, 0, watcher)
}
if(!waiting){
waiting = true
//flushSchedulerQueue函數(shù): Flush both queues and run the watchers
nextTick(flushSchedulerQueue)
}
}
}
這里queue在pushwatcher時是根據(jù)id和flushing做了一些優(yōu)化的,并不會每次數(shù)據(jù)改變都觸發(fā)watcher的回調,而是把這些watcher先添加到⼀個隊列⾥,然后在nextTick后執(zhí)⾏flushSchedulerQueue。
flushSchedulerQueue函數(shù)是保存更新事件的queue的一些加工,讓更新可以滿足Vue更新的生命周期。
這里也解釋了為什么for循環(huán)不能導致頁面更新,因為for是主線程的代碼,在一開始執(zhí)行數(shù)據(jù)改變就會將它push到queue里,等到for里的代碼執(zhí)行完畢后i的值已經(jīng)變化為100時,這時vue才走到nextTick(flushSchedulerQueue)這一步。
nextTick源碼
接著打開vue2.x的源碼,目錄core/util/next-tick.js,代碼量很小,加上注釋才110行,是比較好理解的。
const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
首先將傳入的回調函數(shù)cb(上節(jié)的flushSchedulerQueue)壓入callbacks數(shù)組,最后通過timerFunc函數(shù)一次性解決。
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
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)
}
}
timerFunc下面一大片if else是在判斷不同的設備和不同情況下選用哪種特性去實現(xiàn)異步任務:優(yōu)先檢測是否原生⽀持Promise,不⽀持的話再去檢測是否⽀持MutationObserver,如果都不行就只能嘗試宏任務實現(xiàn),首先是setImmediate,這是⼀個⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的話最后就會降級為 setTimeout 0。
這⾥使⽤callbacks⽽不是直接在nextTick中執(zhí)⾏回調函數(shù)的原因是保證在同⼀個 tick 內多次執(zhí)⾏nextTick,不會開啟多個異步任務,⽽把這些異步任務都壓成⼀個同步任務,在下⼀個 tick 執(zhí)⾏完畢。
nextTick使用
nextTick不僅是vue的源碼文件,更是vue的一個全局API。下面來看看怎么使用吧。
當設置 vm.someData = 'new value',該組件不會立即重新渲染。當刷新隊列時,組件會在下一個事件循環(huán)tick中更新。多數(shù)情況我們不需要關心這個過程,但是如果你想基于更新后的 DOM 狀態(tài)來做點什么,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發(fā)人員使用數(shù)據(jù)驅動的方式思考,避免直接接觸 DOM,但是有時我們必須要這么做。為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM,可以在數(shù)據(jù)變化之后立即使用Vue.nextTick(callback)。這樣回調函數(shù)將在 DOM 更新完成后被調用。
官網(wǎng)用例:
<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改數(shù)據(jù)
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
并且因為$nextTick() 返回一個 Promise 對象,所以也可以使用async/await 語法去處理事件,非常方便。
以上就是詳解Vue的異步更新實現(xiàn)原理的詳細內容,更多關于vue 異步更新的資料請關注腳本之家其它相關文章!
- vue在使用ECharts時的異步更新和數(shù)據(jù)加載詳解
- vue的狀態(tài)更新方式(異步更新解決)
- VUE異步更新DOM - 用$nextTick解決DOM視圖的問題
- 淺談Vuejs中nextTick()異步更新隊列源碼解析
- vue中的任務隊列和異步更新策略(任務隊列,微任務,宏任務)
- vue中$nextTick的用法講解
- Vue中this.$nextTick的作用及用法
- Vue中的nextTick作用和幾個簡單的使用場景
- Vue中this.$nextTick()的理解與使用方法
- 簡單理解Vue中的nextTick方法
- vue2.0$nextTick監(jiān)聽數(shù)據(jù)渲染完成之后的回調函數(shù)方法
- 深入理解Vue nextTick 機制
- Vue2異步更新及nextTick原理詳解
相關文章
vue2+elementui上傳照片方式(el-upload超簡單)
這篇文章主要介紹了vue2+elementui上傳照片方式(el-upload超簡單),具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03

