1. 程式人生 > 程式設計 >詳解Vue的非同步更新實現原理

詳解Vue的非同步更新實現原理

最近面試總是會被問到這麼一個問題:在使用vue的時候,將for迴圈中宣告的變數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的實現。不過在說nextTick之前,有必要先介紹一下JS的事件執行機制。

JS執行機制

眾所周知,JS是基於事件迴圈的單執行緒的語言。 執行的步驟大致是:

  1. 當代碼執行時,所有同步的任務都在主執行緒上執行,形成一個執行棧;
  2. 在主執行緒之外還有一個任務佇列(task queue),只要非同步任務有了執行結果就在任務佇列中放置一個事件;
  3. 一旦執行棧中所有同步任務執行完畢(主執行緒程式碼執行完畢),此時主執行緒不會空閒而是去讀取任務佇列。此時,非同步的任務就結束等待的狀態被執行。
  4. 主執行緒不斷重複以上的步驟。

詳解Vue的非同步更新實現原理

我們把主執行緒執行一次的過程叫一個tick,所以nextTick就是下一個tick的意思,也就是說用nextTick的場景就是我們想在下一個tick做一些事的時候。

所有的非同步任務結果都是通過任務佇列來排程的。而任務分為兩類:巨集任務(macro task)和微任務(micro task)。它們之間的執行規則就是每個巨集任務結束後都要將所有微任務清空。 常見的巨集任務有setTimeout/MessageChannel/postMessage/setImmediate,微任務有MutationObsever/Promise.then

想要透徹學習事件迴圈,推薦Jake在JavaScript全球開發者大會的演講,保證講懂!

nextTick原理

派發更新

大家都知道vue的響應式的靠依賴收集和派發更新來實現的。在修改陣列之後的派發更新過程,會觸發setter的邏輯,執行dep.notify():

// src/core/observer/watcher.js
class Dep {
	notify() {
  	//subs是Watcher的例項陣列
  	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執行後又走到了queueWatcher,那就繼續去看看queueWatcher幹啥了(希望不要繼續套娃了:

//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
  //根據id是否重複做優化
  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,watcher)
    }
    
  	if(!waiting){
  		waiting = true
    	//flushSchedulerQueue函式: Flush both queues and run the watchers
    	nextTick(flushSchedulerQueue)
  	}
  }
}

這裡queue在pushwatcher時是根據id和flushing做了一些優化的,並不會每次資料改變都觸發watcher的回撥,而是把這些watcher先新增到⼀個佇列⾥,然後在nextTick後執⾏flushSchedulerQueue

flushSchedulerQueue函式是儲存更新事件的queue的一些加工,讓更新可以滿足Vue更新的生命週期。

這裡也解釋了為什麼for迴圈不能導致頁面更新,因為for是主執行緒的程式碼,在一開始執行資料改變就會將它push到queue裡,等到for裡的程式碼執行完畢後i的值已經變化為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()
 }

首先將傳入的回撥函式cb(上節的flushSchedulerQueue)壓入callbacks陣列,最後通過timerFunc函式一次性解決。

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是在判斷不同的裝置和不同情況下選用哪種特性去實現非同步任務:優先檢測是否原生⽀持Promise,不⽀持的話再去檢測是否⽀持MutationObserver,如果都不行就只能嘗試巨集任務實現,首先是setImmediate,這是⼀個⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的話最後就會降級為 setTimeout 0。

這⾥使⽤callbacks⽽不是直接在nextTick中執⾏回撥函式的原因是保證在同⼀個 tick 內多次執⾏nextTick,不會開啟多個非同步任務,⽽把這些非同步任務都壓成⼀個同步任務,在下⼀個 tick 執⾏完畢。

nextTick使用

nextTick不僅是vue的原始碼檔案,更是vue的一個全域性API。下面來看看怎麼使用吧。

當設定 vm.someData = 'new value',該元件不會立即重新渲染。當重新整理佇列時,元件會在下一個事件迴圈tick中更新。多數情況我們不需要關心這個過程,但是如果你想基於更新後的 DOM 狀態來做點什麼,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發人員使用資料驅動的方式思考,避免直接接觸 DOM,但是有時我們必須要這麼做。為了在資料變化之後等待 Vue 完成更新 DOM,可以在資料變化之後立即使用Vue.nextTick(callback)。這樣回撥函式將在 DOM 更新完成後被呼叫。

官網用例:

<div id="example">{{message}}</div>
var vm = new Vue({
 el: '#example',data: {
  message: '123'
 }
})
vm.message = 'new message' // 更改資料

vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
 vm.$el.textContent === 'new message' // true
})

並且因為$nextTick() 返回一個 Promise 物件,所以也可以使用async/await 語法去處理事件,非常方便。

以上就是詳解Vue的非同步更新實現原理的詳細內容,更多關於vue 非同步更新的資料請關注我們其它相關文章!