1. 程式人生 > >Vue原始碼閱讀- 批量非同步更新與nextTick原理

Vue原始碼閱讀- 批量非同步更新與nextTick原理

vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社群湧現了一大票vue原始碼閱讀類的文章,在下借這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀原始碼時的想法進行總結,出產一些文章,作為自己思考的總結,本人水平有限,歡迎留言討論~

目標Vue版本:2.5.17-beta.0

宣告:文章中原始碼的語法都使用 Flow,並且原始碼根據需要都有刪節(為了不被迷糊 @[email protected]),如果要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~

1. 非同步更新

上一篇文章我們在依賴收集原理的響應式化方法 defineReactive

中的 setter 訪問器中有派發更新 dep.notify() 方法,這個方法會挨個通知在 depsubs 中收集的訂閱自己變動的watchers執行update。一起來看看 update 方法的實現:

123456789101112131415161718192021222324 // src/core/observer/watcher.js/* Subscriber介面,當依賴發生改變的時候進行回撥 */update(){if(this.computed){// 一個computed watcher有兩種模式:activated lazy(預設)// 只有當它被至少一個訂閱者依賴時才置activated,這通常是另一個計算屬性或元件的render functionif(this.dep.subs.length===0){// 如果沒人訂閱這個計算屬性的變化
// lazy時,我們希望它只在必要時執行計算,所以我們只是簡單地將觀察者標記為dirty// 當計算屬性被訪問時,實際的計算在this.evaluate()中執行this.dirty=true}else{// activated模式下,我們希望主動執行計算,但只有當值確實發生變化時才通知我們的訂閱者this.getAndInvoke(()=>{this.dep.notify()// 通知渲染watcher重新渲染,通知依賴自己的所有watcher執行update})}}elseif(this.sync){// 同步this.run()}else{queueWatcher(this)// 非同步推送到排程者觀察者佇列中,下一個tick時呼叫}}

如果不是 computed watcher 也非 sync 會把呼叫update的當前watcher推送到排程者佇列中,下一個tick時呼叫,看看 queueWatcher

JavaScript
1234567891011121314151617181920212223242526272829 // src/core/observer/scheduler.js/* nextTick的回撥函式,在下一個tick時flush掉兩個佇列同時執行watchers */functionflushSchedulerQueue(){flushing=truelet watcher,idqueue.sort((a,b)=>a.id-b.id)// 排序for(index=0;index<queue.length;index++){// 不要將length進行快取watcher=queue[index]if(watcher.before){// 如果watcher有before則執行watcher.before()}id=watcher.idhas[id]=null// 將has的標記刪除watcher.run()// 執行watcherif(process.env.NODE_ENV!=='production'&&has[id]!=null){// 在dev環境下檢查是否進入死迴圈circular[id]=(circular[id]||0)+1// 比如user watcher訂閱自己的情況if(circular[id]>MAX_UPDATE_COUNT){// 持續執行了一百次watch代表可能存在死迴圈warn()// 進入死迴圈的警告break}}}resetSchedulerState()// 重置排程者狀態callActivatedHooks()// 使子元件狀態都置成active同時呼叫activated鉤子callUpdatedHooks()// 呼叫updated鉤子}

這裡使用了一個 has 的雜湊map用來檢查是否當前watcher的id是否存在,若已存在則跳過,不存在則就push到 queue 佇列中並標記雜湊表has,用於下次檢驗,防止重複新增。這就是一個去重的過程,比每次查重都要去queue中找要文明,在渲染的時候就不會重複 patch 相同watcher的變化,這樣就算同步修改了一百次檢視中用到的data,非同步 patch 的時候也只會更新最後一次修改。

這裡的 waiting 方法是用來標記 flushSchedulerQueue 是否已經傳遞給 nextTick 的標記位,如果已經傳遞則只push到佇列中不傳遞 flushSchedulerQueuenextTick,等到 resetSchedulerState 重置排程者狀態的時候 waiting 會被置回 false 允許 flushSchedulerQueue 被傳遞給下一個tick的回撥,總之保證了 flushSchedulerQueue 回撥在一個tick內只允許被傳入一次。來看看被傳遞給 nextTick 的回撥 flushSchedulerQueue 做了什麼:

123456789101112131415161718192021222324252627282930 // src/core/observer/scheduler.js/* nextTick的回撥函式,在下一個tick時flush掉兩個佇列同時執行watchers */functionflushSchedulerQueue(){flushing=truelet watcher,idqueue.sort((a,b)=>a.id-b.id)// 排序for(index=0;index// 不要將length進行快取watcher=queue[index]if(watcher.before){// 如果watcher有before則執行watcher.before()}id=watcher.idhas[id]=null// 將has的標記刪除watcher.run()// 執行watcherif(process.env.NODE_ENV!=='production'&&has[id]!=null){// 在dev環境下檢查是否進入死迴圈circular[id]=(circular[id]||0)+1// 比如user watcher訂閱自己的情況if(circular[id]>MAX_UPDATE_COUNT){// 持續執行了一百次watch代表可能存在死迴圈warn()// 進入死迴圈的警告break}}}resetSchedulerState()// 重置排程者狀態callActivatedHooks()// 使子元件狀態都置成active同時呼叫activated鉤子callUpdatedHooks()// 呼叫updated鉤子}複製程式碼

nextTick 方法中執行 flushSchedulerQueue 方法,這個方法挨個執行 queue 中的watcher的 run 方法。我們看到在首先有個 queue.sort() 方法把佇列中的watcher按id從小到大排了個序,這樣做可以保證:

  1. 元件更新的順序是從父元件到子元件的順序,因為父元件總是比子元件先建立。
  2. 一個元件的user watchers(偵聽器watcher)比render watcher先執行,因為user watchers往往比render watcher更早建立
  3. 如果一個元件在父元件watcher執行期間被銷燬,它的watcher執行將被跳過

在挨個執行佇列中的for迴圈中,index < queue.length 這裡沒有將length進行快取,因為在執行處理現有watcher物件期間,更多的watcher物件可能會被push進queue。

那麼資料的修改從model層反映到view的過程:資料更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新檢視

2. nextTick原理

2.1 巨集任務/微任務

這裡就來看看包含著每個watcher執行的方法被作為回撥傳入 nextTick 之後,nextTick 對這個方法做了什麼。不過首先要了解一下瀏覽器中的 EventLoopmacro taskmicro task幾個概念,不瞭解可以參考一下 JS與Node.js中的事件迴圈 這篇文章,這裡就用一張圖來表明一下後兩者在主執行緒中的執行關係:


解釋一下,當主執行緒執行完同步任務後:

  1. 引擎首先從macrotask queue中取出第一個任務,執行完畢後,將microtask queue中的所有任務取出,按順序全部執行;
  2. 然後再從macrotask queue中取下一個,執行完畢後,再次將microtask queue中的全部取出;
  3. 迴圈往復,直到兩個queue中的任務都取完。

瀏覽器環境中常見的非同步任務種類,按照優先順序:

  • macro task :同步程式碼、setImmediateMessageChannelsetTimeout/setInterval
  • micro taskPromise.thenMutationObserver

有的文章把 micro task 叫微任務,macro task 叫巨集任務,因為這兩個單詞拼寫太像了 -。- ,所以後面的註釋多用中文表示~

先來看看原始碼中對 micro taskmacro task 的實現: macroTimerFuncmicroTimerFunc

JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849 // src/core/util/next-tick.jsconstcallbacks=[]// 存放非同步執行的回撥let pending=false// 一個標記位,如果已經有timerFunc被推送到任務佇列中去則不需要重複推送/* 挨個同步執行callbacks中回撥 */functionflushCallbacks(){pending=falseconstcopies=callbacks.slice(0)callbacks.length=0for(leti=0;i<copies.length;i++){copies[i]()}}let microTimerFunc// 微任務執行方法let macroTimerFunc// 巨集任務執行方法let useMacroTask=false// 是否強制為巨集任務,預設使用微任務// 巨集任務if(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){macroTimerFunc=()=>{setImmediate(flushCallbacks)}}elseif(typeofMessageChannel!=='undefined'&&(isNative(MessageChannel)||MessageChannel.toString()==='[object MessageChannelConstructor]'// PhantomJS)){constchannel=newMessageChannel()constport=channel.port2channel.port1.onmessage=flushCallbacksmacroTimerFunc=()=>{port.postMessage(1)}}else{macroTimerFunc=()=>{setTimeout(flushCallbacks,0)}}// 微任務if(typeofPromise!=='undefined'&&isNative(Promise)){constp=Promise.resolve()microTimerFunc=()=>{p.then(flushCallbacks)}}else{microTimerFunc=macroTimerFunc// fallback to macro}

flushCallbacks 這個方法就是挨個同步的去執行callbacks中的回撥函式們,callbacks中的回撥函式是在呼叫 nextTick 的時候新增進去的;那麼怎麼去使用 micro taskmacro task 去執行 flushCallbacks 呢,這裡他們的實現 macroTimerFuncmicroTimerFunc 使用瀏覽器中巨集任務/微任務的API對flushCallbacks 方法進行了一層包裝。比如巨集任務方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) },這樣在觸發巨集任務執行的時候 macroTimerFunc() 就可以在瀏覽器中的下一個巨集任務loop的時候消費這些儲存在callbacks陣列中的回調了,微任務同理。同時也可以看出傳給 nextTick 的非同步回撥函式是被壓成了一個同步任務在一個tick執行完的,而不是開啟多個非同步任務。

注意這裡有個比較難理解的地方,第一次呼叫 nextTick 的時候 pending 為false,此時已經push到瀏覽器event loop中一個巨集任務或微任務的task,如果在沒有flush掉的情況下繼續往callbacks裡面新增,那麼在執行這個佔位queue的時候會執行之後新增的回撥,所以 macroTimerFuncmicroTimerFunc 相當於task queue的佔位,以後 pending 為true則繼續往佔位queue裡面新增,event loop輪到這個task queue的時候將一併執行。執行 flushCallbackspending 置false,允許下一輪執行 nextTick 時往event loop佔位。

可以看到上面 macroTimerFuncmicroTimerFunc 進行了在不同瀏覽器相容性下的平穩退化,或者說降級策略

  1. macroTimerFuncsetImmediate -> MessageChannel -> setTimeout。首先檢測是否原生支援 setImmediate,這個方法只在 IE、Edge 瀏覽器中原生實現,然後檢測是否支援 MessageChannel,如果對 MessageChannel 不瞭解可以參考一下這篇文章,還不支援的話最後使用 setTimeout; 為什麼優先使用 setImmediateMessageChannel 而不直接使用 setTimeout 呢,是因為HTML5規定setTimeout執行的最小延時為4ms,而巢狀的timeout表現為10ms,為了儘可能快的讓回撥執行,沒有最小延時限制的前兩者顯然要優於 setTimeout
  2. microTimerFuncPromise.then -> macroTimerFunc 。首先檢查是否支援 Promise,如果支援的話通過 Promise.then 來呼叫 flushCallbacks 方法,否則退化為 macroTimerFunc ; vue2.5之後 nextTick 中因為相容性原因刪除了微任務平穩退化的 MutationObserver 的方式。

2.2 nextTick實現

最後來看看我們平常用到的 nextTick 方法到底是如何實現的:

12345678

相關推薦

Vue原始碼閱讀- 批量非同步更新nextTick原理

vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社群湧現了一大票vue原始碼閱讀類的文章,在下借這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀原始碼時的想法進行總結,出產一些文章,作為自己思考的總

Vue.js 內部執行機制(六)---- 批量非同步更新策略及 nextTick 原理

之前我們學到了 Vue 更新資料是如何更新檢視的。 簡單回顧 資料更新(setter)-> 通知依賴收集集合(Dep) -> 呼叫所有觀察者(Watcher) -> 比對節點樹(patch) -> 檢視 在更新檢視這一步,使用非同步更新策略 為

vue非同步更新佇列 $nextTick

<div id="box"> <div id="div" v-if="showDiv">這是一段文字</div> <button @click="getText">獲取div的內容</button> &l

vue 非同步更新佇列 $nextTick

Vue 非同步執行 DOM 更新。只要觀察到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料改變。如果同一個 watcher 被多次觸發,只會被推入到佇列中一次。如果你想在 DOM

Vue.js非同步更新nextTick

寫在前面 前段時間在寫專案時對nextTick的使用有一些疑惑。在查閱各種資料之後,在這裡總結一下Vue.js非同步更新的策略以及nextTick的用途和原理。如有總結錯誤的地方,歡迎指出! 本文將從以下3點進行總結: 為什麼Vue.js要非同步更新檢視? Jav

Vue原始碼閱讀--過濾器

過濾器 作用 : 用於一些常見的文字格式化 使用方式: 過濾器可以用在兩個地方:雙花括號插值和 v-bind 表示式 (後者從 2.1.0+ 開始支援)。過濾器應該被新增在 JavaScript 表示式的尾部,由“管道”符號指示: <!-- 在雙花括號中 --> {{ message |

vue 原始碼閱讀記錄

1. 入口>建構函式 >定義各類方法 > return vue; function Vue (options) { if ("development" !== 'production' && !(this instanceof Vue) ) {

Vue原始碼閱讀

vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社群湧現了一大票vue原始碼閱讀類的文章,在下借這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀原始碼時的想法進行總結,出產一些文章,作為自己思考的總

Vue 父元件ajax非同步更新資料,子元件props獲取不到

當父元件  axjos  獲取資料,子元件使用  props  接收資料時,執行  mounted  的時候  axjos  還沒有返回資料,而且  mounted 只執行一次,這時   props  中接收的資料為空解決方案:在對應元件中判斷資料的長度

原始碼分析Dubbo非同步呼叫事件回撥機制

   本文將詳細分析Dubbo服務非同步呼叫與事件回撥機制。    1、非同步呼叫與事件回撥機制    1.1 非同步回撥    1.2 事件回撥    2、原始碼分析非同步呼叫與事件回撥機制    在Dubbo中,引入特定的過

Curator原始碼閱讀 - ConnectionState的管理監聽

看看Curator框架 為實現對 連線狀態ConnectionState的監聽,都是怎麼構造框架的。後面我們也可以應用到業務的各種監聽中。 Curator2.13實現 介面 Listener Listener介面,給使用者實現stateChange()傳入新的狀態,使用者實現對這新的狀態要做什麼邏輯處理。 p

JVM原始碼閱讀-本地庫載入流程和原理

前言 本文主要研究OpenJDK中JVM原始碼中涉及到native本地庫的載入流程和原理的部分。主要目的是為了瞭解本地庫是如何被載入到虛擬機器,以及是如何找到並執行本地庫裡的本地方法,以及JNI的 JNI_OnLoad 和 JNI_OnUnLoad是何時被呼叫的 。​

dubbo原始碼分析22 -- consumer 傳送接收原理

在前面的文章中,我們分析了 dubbo 從 provider 進行服務暴露,然後把服務資訊註冊到註冊中心上面解耦 consumer 與 provider 的呼叫。consumer 通過 javassist 建立代理物件引用遠端服務。當通過代理物件呼叫遠端服務的時候,講到進行真

dubbo原始碼分析23 -- provider 接收傳送原理

在前面一篇部落格中分享了 dubbo 在網路通訊當中的 consumer 的傳送以及接收原理。通過叢集容錯最終選擇一個合適的 Invoke 通過 netty 直聯呼叫 provider 的服務。眾所周知, netty 是基於 Java Nio 的 Reactor 模型的非同步

Vue.js原始碼非同步更新DOM策略及nextTick

寫在前面 因為對Vue.js很感興趣,而且平時工作的技術棧也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js原始碼,並做了總結與輸出。 文章的原地址:https://github.com/answershuto/learnVue。 在學習過程中,為Vue加上了中文的註釋https:/

Vue.js原始碼解析(八)【Vue.js非同步更新DOM策略及nextTick

操作DOM 在使用vue.js的時候,有時候因為一些特定的業務場景,不得不去操作DOM,比如這樣: <template> <div> <div ref="test">{{test}}</div>

Vuejs中nextTick()非同步更新佇列原始碼解析

vue官網關於此解釋說明如下: vue2.0裡面的深入響應式原理的非同步更新佇列 官網說明如下: 只要觀察到資料變化,Vue 將開啟一個佇列,並緩衝在同一事件迴圈中發生的所有資料改變。如果同一個 watcher 被多次觸發,只會一次推入到佇列中。這種在緩衝

Vue源碼終筆-VNode更新diff算法初探

ack 處理 劫持 sun 副本 容易 add 來講 method   寫完這個就差不多了,準備幹新項目了。   確實挺不擅長寫東西,感覺都是羅列代碼寫點註釋的感覺,這篇就簡單闡述一下數據變動時DOM是如何更新的,主要講解下其中的diff算法。   先來個正常的html

JDK原始碼閱讀:InterruptibleChannel可中斷IO,ig牛逼

Java傳統IO是不支援中斷的,所以如果程式碼在read/write等操作阻塞的話,是無法被中斷的。這就無法和Thead的interrupt模型配合使用了。JavaNIO眾多的升級點中就包含了IO操作對中斷的支援。InterruptiableChannel表示支援中斷的Channel。我們常用的FileCha

mysql 批量更新批量更新多條記錄的不同值實現方法

批量更新 mysql更新語句很簡單,更新一條資料的某個欄位,一般這樣寫: UPDATE mytable SET myfield = 'value' WHERE other_field = 'other_value';   如果更新同一欄位為同一個值,mysql也很簡單,修改