Vue原始碼閱讀- 批量非同步更新與nextTick原理
vue已是目前國內前端web端三分天下之一,同時也作為本人主要技術棧之一,在日常使用中知其然也好奇著所以然,另外最近的社群湧現了一大票vue原始碼閱讀類的文章,在下借這個機會從大家的文章和討論中汲取了一些營養,同時對一些閱讀原始碼時的想法進行總結,出產一些文章,作為自己思考的總結,本人水平有限,歡迎留言討論~
目標Vue版本:2.5.17-beta.0
宣告:文章中原始碼的語法都使用 Flow,並且原始碼根據需要都有刪節(為了不被迷糊 @[email protected]),如果要看完整版的請進入上面的github地址,本文是系列文章,文章地址見底部~
1. 非同步更新
上一篇文章我們在依賴收集原理的響應式化方法 defineReactive
setter
訪問器中有派發更新 dep.notify()
方法,這個方法會挨個通知在 dep
的 subs
中收集的訂閱自己變動的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){// 如果沒人訂閱這個計算屬性的變化 |
如果不是 computed watcher
也非 sync
會把呼叫update的當前watcher推送到排程者佇列中,下一個tick時呼叫,看看 queueWatcher
:
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到佇列中不傳遞 flushSchedulerQueue
給 nextTick
,等到 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從小到大排了個序,這樣做可以保證:
- 元件更新的順序是從父元件到子元件的順序,因為父元件總是比子元件先建立。
- 一個元件的user watchers(偵聽器watcher)比render watcher先執行,因為user watchers往往比render watcher更早建立
- 如果一個元件在父元件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
對這個方法做了什麼。不過首先要了解一下瀏覽器中的 EventLoop
、macro task
、micro task
幾個概念,不瞭解可以參考一下 JS與Node.js中的事件迴圈 這篇文章,這裡就用一張圖來表明一下後兩者在主執行緒中的執行關係:
解釋一下,當主執行緒執行完同步任務後:
- 引擎首先從macrotask queue中取出第一個任務,執行完畢後,將microtask queue中的所有任務取出,按順序全部執行;
- 然後再從macrotask queue中取下一個,執行完畢後,再次將microtask queue中的全部取出;
- 迴圈往復,直到兩個queue中的任務都取完。
瀏覽器環境中常見的非同步任務種類,按照優先順序:
macro task
:同步程式碼、setImmediate
、MessageChannel
、setTimeout/setInterval
micro task
:Promise.then
、MutationObserver
有的文章把 micro task
叫微任務,macro task
叫巨集任務,因為這兩個單詞拼寫太像了 -。- ,所以後面的註釋多用中文表示~
先來看看原始碼中對 micro task
與 macro task
的實現: macroTimerFunc
、microTimerFunc
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 task
與 macro task
去執行 flushCallbacks
呢,這裡他們的實現 macroTimerFunc
、microTimerFunc
使用瀏覽器中巨集任務/微任務的API對flushCallbacks
方法進行了一層包裝。比如巨集任務方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) }
,這樣在觸發巨集任務執行的時候 macroTimerFunc()
就可以在瀏覽器中的下一個巨集任務loop的時候消費這些儲存在callbacks陣列中的回調了,微任務同理。同時也可以看出傳給 nextTick
的非同步回撥函式是被壓成了一個同步任務在一個tick執行完的,而不是開啟多個非同步任務。
注意這裡有個比較難理解的地方,第一次呼叫 nextTick
的時候 pending
為false,此時已經push到瀏覽器event loop中一個巨集任務或微任務的task,如果在沒有flush掉的情況下繼續往callbacks裡面新增,那麼在執行這個佔位queue的時候會執行之後新增的回撥,所以 macroTimerFunc
、microTimerFunc
相當於task queue的佔位,以後 pending
為true則繼續往佔位queue裡面新增,event loop輪到這個task queue的時候將一併執行。執行 flushCallbacks
時 pending
置false,允許下一輪執行 nextTick
時往event loop佔位。
可以看到上面 macroTimerFunc
與 microTimerFunc
進行了在不同瀏覽器相容性下的平穩退化,或者說降級策略:
macroTimerFunc
:setImmediate -> MessageChannel -> setTimeout
。首先檢測是否原生支援setImmediate
,這個方法只在 IE、Edge 瀏覽器中原生實現,然後檢測是否支援 MessageChannel,如果對MessageChannel
不瞭解可以參考一下這篇文章,還不支援的話最後使用setTimeout
; 為什麼優先使用setImmediate
與MessageChannel
而不直接使用setTimeout
呢,是因為HTML5規定setTimeout執行的最小延時為4ms,而巢狀的timeout表現為10ms,為了儘可能快的讓回撥執行,沒有最小延時限制的前兩者顯然要優於setTimeout
。microTimerFunc
:Promise.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 非同步更新佇列 $nextTickVue 非同步執行 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也很簡單,修改 |