原發於個人博客。javascript
前一篇文章已經詳細記述了Vue的核心執行過程。至關於已經搞定了主線劇情。後續的文章都會對其中沒有介紹的細節進行展開。html
如今咱們就來說講其餘支線任務:nextTick和microtask。vue
Vue的nextTick api的實現部分是Vue裏比較好理解的一部分,與其餘部分的代碼也很是的解耦,所以這一塊的相關源碼解析文章不少。我原本也不許備單獨寫博客細說這部分,可是最近偶然在別人的文章中瞭解到:
每輪次的event loop中,每次執行一個task,並執行完microtask隊列中的全部microtask以後,就會進行UI的渲染。可是做者彷佛對於這個結論也不是很確定。而我第一反應就是Vue的$nextTick既然用到了MutationObserver(MO的回調放進的是microtask的任務隊列中的),那麼是否是也是出於這個考慮呢?因而我想研究了一遍Vue的$nextTick,就能夠了解是否是出於這個目的,也同時看能不能佐證UI Render真的是在microtask隊列清空後執行的。java
研究以後的結論:我以前對於$nextTick源碼的理解徹底是錯的,以及每輪事件循環執行完全部的microtask,是會執行UI Render的。react
task/macrotask和microtask的概念自從去年知乎上有人提出這個問題以後,task和microtask已經被不少同窗瞭解了,我也是當時看到了microtask的內容,如今已經有很是多的中文介紹博客在介紹這部分的知識,最近這篇火遍掘金、SF和知乎的文章,最後也是考了microtask的概念。若是你沒有看過task/microtask的內容的話,我仍是推薦這篇英文博客,是絕大多數國內博客的內容來源。webpack
先用120秒介紹MutationObserver: MO是HTML5中的新API,是個用來監視DOM變更的接口。他能監聽一個DOM對象上發生的子節點刪除、屬性修改、文本內容修改等等。
調用過程很簡單,可是有點不太尋常:你須要先給他綁回調:var mo = new MutationObserver(callback)
經過給MO的構造函數傳入一個回調,能獲得一個MO實例,這個回調就會在MO實例監聽到變更時觸發。ios
這個時候你只是給MO實例綁定好了回調,他具體監聽哪一個DOM、監聽節點刪除仍是監聽屬性修改,你都尚未設置。而調用他的observer方法就能夠完成這一步:git
var domTarget = 你想要監聽的dom節點 mo.observe(domTarget, { characterData: true //說明監聽文本內容的修改。 })
一個須要先說的細節是,MutationObserver的回調是放在microtask中執行的。github
ok了,如今這個domTarget上發生的文本內容修改就會被mo監聽到,mo就會觸發你在new MutationObserver(callback)
中傳入的callback。web
如今咱們來看Vue.nextTick的源碼:
export const nextTick = (function () { var callbacks = [] var pending = false var timerFunc function nextTickHandler () { pending = false // 之因此要slice複製一份出來是由於有的cb執行過程當中又會往callbacks中加入內容 // 好比$nextTick的回調函數裏又有$nextTick // 這些是應該放入到下一個輪次的nextTick去執行的, // 因此拷貝一份當前的,遍歷執行完當前的便可,避免無休止的執行下去 var copies = callbacks.slice(0) callbacks = [] for (var i = 0; i < copies.length; i++) { copies[i]() } } /* istanbul ignore if */ // ios9.3以上的WebView的MutationObserver有bug, //因此在hasMutationObserverBug中存放了是不是這種狀況 if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) { var counter = 1 // 建立一個MutationObserver,observer監聽到dom改動以後後執行回調nextTickHandler var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(counter) // 調用MutationObserver的接口,觀測文本節點的字符內容 observer.observe(textNode, { characterData: true }) // 每次執行timerFunc都會讓文本節點的內容在0/1之間切換, // 不用true/false多是有的瀏覽器對於文本節點設置內容爲true/false有bug? // 切換以後將新值賦值到那個咱們MutationObserver觀測的文本節點上去 timerFunc = function () { counter = (counter + 1) % 2 textNode.data = counter } } else { // webpack attempts to inject a shim for setImmediate // if it is used as a global, so we have to work around that to // avoid bundling unnecessary code. // webpack默認會在代碼中插入setImmediate的墊片 // 沒有MutationObserver就優先用setImmediate,不行再用setTimeout const context = inBrowser ? window : typeof global !== 'undefined' ? global : {} timerFunc = context.setImmediate || setTimeout } return function (cb, ctx) { var func = ctx ? function () { cb.call(ctx) } : cb callbacks.push(func) // 若是pending爲true, 就其實代表本輪事件循環中已經執行過timerFunc(nextTickHandler, 0) if (pending) return pending = true timerFunc(nextTickHandler, 0) } })()
上面這個函數執行過程後生成的那個函數纔是nextTick。而這個函數的執行過程就是先初始化pending變量和cb變量,cb用來存放須要執行的回調,pending表示是否把清空回調的nextTickHandler函數加入到異步隊列中。
而後就是建立了一個MO,這個MO監聽了一個新建立的文本節點的文本內容變化,同時監聽到變化時的回調就是nextTickHandler。nextTickHandler遍歷cb數組,把須要執行的cb給拿出來一個個執行了。
而最後返回出去做爲nextTick的那個函數就比較簡單了:
function (cb, ctx) { var func = ctx ? function () { cb.call(ctx) } : cb callbacks.push(func) // 若是pending爲true, 就其實代表本輪事件循環中已經執行過timerFunc(nextTickHandler, 0) if (pending) return pending = true timerFunc(nextTickHandler, 0) } }
也就是把傳入的回調放入cb數組當中,而後執行timerFunc(nextTickHandler, 0)
,實際上是執行timerFunc()
,後面傳入的兩參數沒用,在瀏覽器不支持MO的狀況timerFunc纔回退到setTimeout,那倆參數纔有效果。timerFunc就是把那個被MO監聽的文本節點改一下它的內容,這樣我改了文本內容,MO就會在當前的全部同步代碼完成以後執行回調,從而執行數據更新到DOM上以後的任務。
我一開始在看這一段代碼時忘記了MutationObserver的回調是在microtask裏執行的。並且當時也尚未看過Vue的其餘源碼,當時的我大致看懂nextTick代碼流程以後,造成了以下的理解,並且以爲彷佛完美的解釋了代碼邏輯:
watcher監聽到數據變化以後,會立馬去修改dom,接着用戶書寫的代碼裏的nextTick被執行,而nextTick內部也是去修改DOM(textNode),當這個最後修改的textNode修改完成了,觸發了MutationObserver的回調,那就意味着,前面的DOM修改也已經完成了,因此nextTick向用戶保證的DOM更新以後再執行用戶的回調
就得以實現了。
Damn,如今看了Batcher的代碼和認真反思了之後,立馬醒悟,上面的想法完徹底全就是一坨狗屎,totally shit!
首先,一個廣泛的常識是DOM Tree的修改是實時的,而修改的Render到DOM上纔是異步的。根本不存在什麼所謂的等待DOM修改完成,任什麼時候候我在上一行代碼裏往DOM中添加了一個元素、修改了一個DOM的textContent,你在下一行代碼裏必定能立馬就讀取到新的DOM,我知道這個理。可是我仍是搞不懂我怎麼會產生用nextTick來保證DOM修改的完成這樣的怪念頭
。可能那天屎吃得有點多了。
其次,咱們來看看使用nextTick的真正緣由:
Vue在兩個地方用到了上述nextTick:
Vue.nextTick和Vue.prototype.$nextTick都是直接使用了這個nextTick
在batcher中,也就是watcher觀測到數據變化後執行的是nextTick(flushBatcherQueue)
,flushBatcherQueue
則負責執行完成全部的dom更新操做。
Batcher的源碼,我在上一篇文章當中已經詳細的分析了,在這裏我用一張圖來講明它和nextTick的詳細處理過程吧。
假設此時Vue實例的模板爲:<div id="a">{{a}}</div>
仔細跟蹤了代碼執行過程咱們會發現,真正的去遍歷watcher,批處理更新是在microtask中執行的,並且用戶在修改數據後本身執行的nextTick(cb)
也會在此時執行cb,他們都是在同一個microtask中執行。根本就不是我最開始想的那樣,把回調放在之後的事件循環中去執行。
同時,上面這個過程也深切的揭露出Vue nextTick的本質,我不是想要MO來幫我真正監聽DOM更改,我只是想要一個異步API,用來在當前的同步代碼執行完畢後,執行我想執行的異步回調。
之因此要這樣,是由於用戶的代碼當中是可能屢次修改數據的,而每次修改都會同步通知到全部訂閱該數據的watcher,而立馬執行將數據寫到DOM上是確定不行的,那就只是把watcher加入數組。等到當前task執行完畢,全部的同步代碼已經完成,那麼這一輪次的數據修改就已經結束了,這個時候我能夠安安心心的去將對監聽到依賴變更的watcher完成數據真正寫入到DOM上的操做,這樣即便你在以前的task裏改了一個watcher的依賴100次,我最終只會計算一次value、改DOM一次。一方面省去了沒必要要的DOM修改,另外一方面將DOM操做彙集,能夠提高DOM Render效率。
那爲何必定要用MutationObserver呢?不,並無必定要用MO,只要是microtask均可以。在最新版的Vue源碼裏,優先使用的就是Promise.resolve().then(nextTickHandler)
來將異步回調放入到microtask中(MO在IOS9.3以上的WebView中有bug),沒有原生Promise才用MO。
這充分說明了microtask纔是nextTick的本質,MO什麼的只是個備胎,要是有比MO優先級更高、瀏覽器兼容性更好的microtask,那可能就分分鐘把MO拿下了。
那問題又來了,爲何必定要microtask?task能夠嗎?(macrotask和task是一回事哈,HTML5標準裏甚至都沒有macrotask這個詞)。
哈,如今恰好有個例子,Vue一開始曾經改過nextTick的實現。咱們來看看這兩個jsFiddle:jsfiddle1和jsfiddle2。
兩個fiddle的實現如出一轍,就是讓那個絕對定位的黃色元素起到一個fixed定位的效果:綁定scroll事件,每次滾動的時候,計算當前滾動的位置並更改到那個絕對定位元素的top屬性上去。你們本身試試滾動幾下,對比下效果,你就會發現第一個fiddle中的黃元素是穩定不動的,fixed很好。然後一個fiddle中就有問題了,黃色元素上下晃動,彷佛跟不上咱們scroll的節奏,總要慢一點,雖然最後停下滾動時位置是對的。
上述兩個例子實際上是在這個issue中找到的,第一個jsfiddle使用的版本是Vue 2.0.0-rc.6,這個版本的nextTick實現是採用了MO,然後由於IOS9.3的WebView裏的MO有bug,因而尤雨溪更改了實現,換成了window.postMessage
,也就是後一個fiddle所使用的Vue 2.0.0-rc.7。後來尤雨溪瞭解到window.postMessage
是將回調放入的macrotask 隊列。這就是問題的根源了。
HTML中的UI事件、網絡事件、HTML Parsing等都是使用的task來完成,所以每次scroll事件觸發後,在當前的task裏只是完成了把watcher加入隊列和把清空watcher的flushBatcherQueue做爲異步回調傳入nextTick。
若是nextTick使用的是microtask,那麼在task執行完畢以後就會當即執行全部microtask,那麼flushBatcherQueue(真正修改DOM)便得以在此時當即完成,然後,當前輪次的microtask所有清理完成時,執行UI rendering,把重排重繪等操做真正更新到DOM上(後文會細說)。(注意,頁面的滾動效果並不須要重繪哈。重繪是當你修改了UI樣式、DOM結構等等,頁面將樣式呈現出來,別暈了。)
若是nextTick使用的是task,那麼會在當前的task和全部microtask執行完畢以後纔在之後的某一次task執行過程當中處理flushBatcherQueue,那個時候才真正執行各個指令的修改DOM操做,但那時爲時已晚,錯過了屢次觸發重繪、渲染UI的時機。並且瀏覽器內部爲了更快的響應用戶UI,內部多是有多個task queue的:
For example, a user agent could have one task queue for mouse and key events (the user interaction task source), and another for everything else. The user agent could then give keyboard and mouse events preference over other tasks three quarters of the time, keeping the interface responsive but not starving other task queues, and never processing events from any one task source out of order.
而UI的task queue的優先級可能更高,所以對於尤雨溪採用的window.postMessage
,甚至可能已經屢次執行了UI的task,都沒有執行window.postMessage
的task,也就致使了咱們更新DOM操做的延遲。在重CPU計算、UI渲染任務狀況下,這一延遲達到issue觀測到的100毫秒到1秒的級別是徹底課可能的。所以,使用task來實現nextTick是不可行的,而尤雨溪也撤回了這一次的修改,後續的nextTick實現中,依然是使用的Promise.then和MO。
我最近認真閱讀了一下HTML5規範,仍是來講一說task和microtask處理完成以後的UI渲染過程,講一下每次task執行和全部microtask執行完畢後使如何完成UI Render的。
先上HTML標準原文:
比較典型的task有以下這些
Events
Dispatching an Event object at a particular EventTarget object is often done by a dedicated task. Not all events are dispatched using the task queue, many are dispatched during other tasks.Parsing
The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task.Callbacks
Calling a callback is often done by a dedicated task.Using a resource
When an algorithm fetches a resource, if the fetching occurs in a non-blocking fashion then the processing of the resource once some or all of the resource is available is performed by a task.Reacting to DOM manipulation
Some elements have tasks that trigger in response to DOM manipulation, e.g. when that element is inserted into the document.
此外,還包括setTimeout, setInterval, setImmediate, window.postMessage等等。
上述Reacting to DOM manipulation並非說你執行DOM操做時就會把這個DOM操做的執行當成一個task。是那些異步的reacting會被當作task。
HTML5標準:task、microtask和UI render的具體執行過程以下:
An event loop must continually run through the following steps for as long as it exists:
1.Select the oldest task on one of the event loop's task queues, if any, ignoring, in the case of a browsing context event loop, tasks whose associated Documents are not fully active. The user agent may pick any task queue. If there is no task to select, then jump to the microtasks step below.
2.Set the event loop's currently running task to the task selected in the previous step.
3.Run: Run the selected task.
4.Set the event loop's currently running task back to null.
5.Remove the task that was run in the run step above from its task queue.
6.Microtasks: Perform a microtask checkpoint. //這裏會執行全部的microtask
7.Update the rendering: If this event loop is a browsing context event loop (as opposed to a worker event loop), then run the following substeps.
7.1 Let now be the value that would be returned by the Performance object's now() method.
7.2 Let docs be the list of Document objects associated with the event loop in question, sorted arbitrarily except that the following conditions must be met:
7.3 If there are top-level browsing contexts B that the user agent believes would not benefit from having their rendering updated at this time, then remove from docs all Document objects whose browsing context's top-level browsing context is in B.
7.4 If there are a nested browsing contexts B that the user agent believes would not benefit from having their rendering updated at this time, then remove from docs all Document objects whose browsing context is in B.
7.5 For each fully active Document in docs, run the resize steps for that Document, passing in now as the timestamp. [CSSOMVIEW]
7.6 For each fully active Document in docs, run the scroll steps for that Document, passing in now as the timestamp. [CSSOMVIEW]
7.7 For each fully active Document in docs, evaluate media queries and report changes for that Document, passing in now as the timestamp. [CSSOMVIEW]
7.8 For each fully active Document in docs, run CSS animations and send events for that Document, passing in now as the timestamp. [CSSANIMATIONS]
7.9 For each fully active Document in docs, run the fullscreen rendering steps for that Document, passing in now as the timestamp. [FULLSCREEN]
7.10 For each fully active Document in docs, run the animation frame callbacks for that Document, passing in now as the timestamp.
7.11 For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]
7.12 For each fully active Document in docs, update the rendering or user interface of that Document and its browsing context to reflect the current state.
8.If this is a worker event loop (i.e. one running for a WorkerGlobalScope), but there are no tasks in the event loop's task queues and the WorkerGlobalScope object's closing flag is true, then destroy the event loop, aborting these steps, resuming the run a worker steps described in the Web workers section below.
9.Return to the first step of the event loop.
解釋一下:第一步,從多個task queue中的一個queue裏,挑出一個最老的task。(由於有多個task queue的存在,使得瀏覽器能夠完成咱們前面說的,優先、高頻率的執行某些task queue中的任務,好比UI的task queue)。
而後2到5步,執行這個task。
第六步, Perform a microtask checkpoint. ,這裏會執行完microtask queue中的全部的microtask,若是microtask執行過程當中又添加了microtask,那麼仍然會執行新添加的microtask,固然,這個機制好像有限制,一輪microtask的執行總量彷佛有限制(1000?),數量太多就執行一部分留下的之後再執行?這裏我不太肯定。
第七步,Update the rendering:
7.2到7.4,當前輪次的event loop中關聯到的document對象會保持某些特定順序,這些document對象都會執行須要執行UI render的,可是並非全部關聯到的document都須要更新UI,瀏覽器會判斷這個document是否會從UI Render中獲益,由於瀏覽器只須要保持60Hz的刷新率便可,而每輪event loop都是很是快的,因此不必每一個document都Render UI。
7.5和7.6 run the resize steps/run the scroll steps不是說去執行resize和scroll。每次咱們scoll的時候視口或者dom就已經當即scroll了,並把document或者dom加入到 pending scroll event targets中,而run the scroll steps具體作的則是遍歷這些target,在target上觸發scroll事件。run the resize steps也是類似的,這個步驟是觸發resize事件。
7.8和7.9 後續的media query, run CSS animations and send events等等也是類似的,都是觸發事件,第10步和第11步則是執行咱們熟悉的requestAnimationFrame回調和IntersectionObserver回調(第十步仍是挺關鍵的,raf就是在這執行的!)。
7.12 渲染UI,關鍵就在這了。
第九步 繼續執行event loop,又去執行task,microtasks和UI render。
更新:找到一張圖,不過着重說明的是整個event loop,沒有細說UI render。