Vue的異步更新實現原理

2020-12-23 前端學習棧

最近面試總是會被問到這麼一個問題:在使用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是基於事件循環的單線程的語言。執行的步驟大致是:

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

我們把主線程執行一次的過程叫一個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.jsclassDep {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是怎麼去更新的:

classWatcher {update() { ...//各種情況判斷之後else{ queueWatcher(this) } }}複製代碼update執行後又走到了queueWatcher,那就繼續去看看queueWatcher幹啥了(希望不要繼續套娃了:

//queueWatcher 定義在 src/core/observer/scheduler.jsconst queue: Array<Watcher> = []let has: { [key: number]: ?true } = {}let waiting = falselet flushing = falselet index = 0exportfunctionqueueWatcher(watcher: Watcher) {const id = watcher.id//根據id是否重複做優化if(has[id] == null){ has[id] = trueif(!flushing){ queue.push(watcher) }else{let i=queue.length - 1while(i > index && queue[i].id > watcher.id){ i-- } queue.splice(i + 1, 0, 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 = falseexportfunctionnextTick (cb?: Function, ctx?: Object) {let _resolve callbacks.push(() => {if (cb) {try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } elseif (_resolve) { _resolve(ctx) } })if (!pending) { pending = true timerFunc() }複製代碼首先將傳入的回調函數cb(上節的flushSchedulerQueue)壓入callbacks數組,最後通過timerFunc函數一次性解決。

let timerFuncif (typeofPromise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks)if (isIOS) setTimeout(noop) } isUsingMicroTask = true} elseif (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) ||// PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]')) {let counter = 1const 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} elseif (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'// falseVue.nextTick(function () { vm.$el.textContent === 'new message'// true})複製代碼並且因為$nextTick() 返回一個 Promise 對象,所以也可以使用async/await 語法去處理事件,非常方便。

相關焦點

  • Vue異步更新機制和nextTick原理
    vue核心  實現之一,在整體流程中預先著手觀看者更新的調度者這一角色。而在微任務執行完後會進入瀏覽器更新渲染階段,所以在更新渲染前使用微任務會比宏任務快一些。為什麼需要初步更新既然異步更新是核心之一,首先要知道它的作用是什麼,解決了什麼問題。
  • 了解vue3.0的異步更新原理
    今天我們簡單了解下vue3.0的異步更新原理,了解一下effect,watchEffect的特點以及最主要queueFlush函數的實現(函數名字本意就是:排隊刷新)effect特點import { effect, reactive } from '.
  • Vue面試必問,異步更新機制和nextTick原理
    異步更新上一篇文章我們在依賴收集原理的響應式化方法 defineReactive 中的 setter 訪問器中有派發更新 dep.notify() 方法,這個方法會挨個通知在 dep 的 subs 中收集的訂閱自己變動的watchers執行update。
  • Vue 進階面試必問,異步更新機制和 nextTick 原理
    異步更新上一篇文章我們在依賴收集原理的響應式化方法 defineReactive 中的 setter 訪問器中有派發更新 dep.notify() 方法,這個方法會挨個通知在 dep 的 subs 中收集的訂閱自己變動的watchers執行update。
  • 全面解析Vue.nextTick實現原理
    這裡我們要思考的是:vue是不是用MO來監聽DOM更新完畢的呢?那就打開vue的源碼看看吧,在實現nextTick的地方,確實能看到這樣的代碼:if (typeof MutationObserver !
  • 什麼是MVVM,MVC和MVVM的區別,MVVM框架VUE實現原理
    它們通過ViewModel來通信,ViewModel通常要實現一個observer觀察者,當數據發生變化,ViewModel能夠監聽到數據的這種變化,然後通知到對應的視圖做自動更新,而當用戶操作視圖,ViewModel也能監聽到視圖的變化,然後通知數據做改動,這實際上就實現了數據的雙向綁定。
  • Vue面試題(3)Vue-Router和Vuex
    2、action 和 mutation 區別mutation 是同步更新數據(內部會進行是否為異步方式更新數據的檢測)內部並不能檢測到是否異步更新,而是實例上有一個開關變量 _committing,只有在 mutation 執行之前才會把開關打開,允許修改 state 上的屬性。並且在 mutation 同步執行完成後立刻關閉。
  • 基於 Laravel + Vue 組件實現文件異步上傳
    我們可以通過 Request 請求實例提供的 file 方法獲取用戶上傳文件,並將其保存到指定目錄從而完成文件上傳,接下來,我們將從前端到後端實現一個完整的用戶上傳文件功能,包括視圖、路由、控制器部分代碼。
  • 結合 Bootstrap + Vue 組件實現 Laravel 異步分頁功能
    關於如何使用 Laravel 自帶的分頁功能進行分頁,可以參考官方文檔中的分頁章節,說的非常清楚,在這篇教程中我們就不再一一演示了,不過 Laravel 自帶的分頁器實現的分頁連結是動態 URL,不利於 SEO,如果你想要實現偽靜態的分頁連結,可以參考這篇教程:通過自定義分頁器實現偽靜態分頁連結以利於 SEO。
  • 在vue項目中使用vuex實現狀態管理的案例
    actions:不能直接改變state的數據,而是提交一個mutations,任意的異步行為都應該在actions中。modules:模塊化管理vuex,實現項目中拆分的每個功能模塊都有各自的 state,getter,mutations,actions。
  • vue基本知識點總結——面試必備
    3、vue中的模板編譯原理答:模板指的就是template。如果我們傳了一個template,我們會把template轉換成一個render函數,然後通過render函數返回虛擬DOM,再把虛擬的DOM變成真正的DOM。4、 響應式數據的原理答:響應式就是當數據變化的時候,可以讓視圖也同步更新。
  • 化身面試官出 20+ Vue 面試題,超級乾貨
    view 表示視圖層, ViewModel 是 View 和 Model 層的橋梁,數據綁定到 viewModel 層並自動渲染到頁面中,視圖變化通知 viewModel 層更新數據。😶 摸底差不多,問基礎了,響應式數據得知道吧,問一問4.vue 是如何實現響應式數據的呢?
  • 前端技術:React&Vue對比
    React和vue的業務邏輯是差不多,vue在react上封裝了更簡潔的方法,使用起來更加的便捷,如:提供了便捷的指令(v-for,v-if,v-model),還提供了更多的屬性(computed,watch),我還是比較喜歡用react的,更接近js原生,更容易於理解它。
  • Vue3.0新特性
    全局API的修改和優化更多的誓言性特性更好的Typescript 支持vue3.0源碼就是ts寫的為什麼要有vue3.0?指向上下文,主要是沒考慮的ts的集成ts+ vue Cli 項目搭建環境1.6使用模塊化實現滑鼠追蹤器效果,點擊滑鼠更新滑鼠位置1)使用ref實現
  • 實現一個簡單的Vue.js
    原文轉自 https://const_white.gitee.io/gitee-blog/blog/vue/mini-vue/Vue響應式原理圖片引自 孟思行 - 圖解 Vue 響應式原理乞丐版 mini-vue
  • 【Vuejs】778- 超全 Vuejs 知識點(基礎到進階)
    層並自動將數據渲染到頁面中,視圖變化的時候會通知viewModel層更新數據了解mvc/mvp/mvvm的區別Vue2.x響應式數據/雙向綁定原理Vue 數據雙向綁定主要是指:數據變化更新視圖,視圖變化更新數據。
  • Vue面試中,經常會被問到的面試題/Vue知識點整理
    切記不要以為背了面試題,就萬事大吉了,最好是理解背後的原理,這樣面試的時候才能侃侃而談。不然,稍微有水平的面試官一看就能看出,是否有真才實學還是剛好背中了這道面試題。(都是一些基礎的vue面試題,大神不用浪費時間往下看)一、對於MVVM的理解?MVVM 是 Model-View-ViewModel 的縮寫。
  • vue題
    2、vue實現雙向數據綁定vue實現數據雙向綁定主要是:採用數據劫持結合發布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數據變動時發布消息給訂閱者,觸發相應監聽回調。
  • Vue全家桶&vue-router原理
    原理 Vue全家桶:vue + vue-router(路由)+ vuex(狀態管理)+ axios(請求)本次主要分析並實現簡版vue-router,從源碼中學習借鑑,更好的理解源碼vue-routerVue Router 是 Vue.js 官⽅的路由管理器。
  • 35道常見的前端vue面試題
    編譯生成的虛擬domd、oldVnode:上一個vnode,只在update和componentUpdated鉤子函數中有效⚠️:如果不需要其他鉤子函數,可以直接簡寫為:directive(「focus」,function(el,binding){})8、vue的實現原理?