本文同步發表於 個人博客
本文對應 Vue.js v2.6.11.
做者正在看機會,若有合適內推機會,煩請聯繫html
本文僅覆蓋 Vue.js v2
中 watcher
更新以及 nextTick
部分。本文的目標是闡述在最新版本 Vue.js v2.6.11 中全部的 watcher
實例在觸發 update
函數後,是如何藉助 nextTick
特性實現:vue
tick
事件循環 task
完成後,纔會真正執行 watcher.update
函數。<!-- 1. 概述 nextTick 原理,及其背後 event loop 驅動 傳入 nextTick 的回調函數的調用。node
在 vue.js
內部實現中,watcher
不只僅是 vm.$watch API
如此。watcher
本質上扮演了一個訂閱數據更新 topic
的訂閱者 subscriber
。全部的數據更新後的回調觸發邏輯都依賴於 watcher
實例。不一樣類型的 watcher
實例起到不一樣的回調效果。react
每一個視圖組件都依賴於一個惟一與之對應的 renderWatcher
實例,該實例始終接受一個用於視圖更新的 expOrFn
函數做爲 renderWatcher
的 renderWatcher.getter
函數。該函數在 renderWatcher
收到更新後,進行函數調用執行。當 renderWatcher.getter
觸發時,即調用 vnode
的建立函數 vm._render
和 DOM
的 Diff
更新函數 vm._patch
。git
new Watcher(vm, updateComponent)
computed[computed]
對應一個 lazyWatcher
實例,該實例始終接受一個用戶傳入的 lazyWatcher.get
函數來在 取值當前 computed[computed]
才進行惰性計算,並在依賴沒有變化時,始終始終緩存的 lazyWatcher.value
值。由於 lazyWatcher
的計算過程是 同步的當即計算,即不依賴於 nextTick
,那麼本文將忽略此類型 watcher
實例。當開發者調用 vm.$watch
或傳入 vm.$options.watch[watchKey]
時,本質上是建立一個 userWatcher
。github
// src/core/instance/state.js#L355-L356 options.user = true // 定義爲 userWatcher new Watcher(vm, expOrFn, cb, options)
額外的,每一個用戶傳入的 userWatcher
回調,都將做爲 userWatcher.cb
註冊。在每次 userWatcher.get
調用時,觸發 cb
回調函數。web
TL, DR: 在單次事件循環 tick
中,至多僅有一次 UI
更新機會,那麼若在單次事件循環存在的屢次 renderWatcher
更新視圖操做時,就不能當即執行更新視圖操做,而應該藉助去重操做和 nextTick
調度延遲執行視圖更新操做,實現最終僅有最後一次視圖更新生效。vuex
首先,基於現行 html living standard
標準對瀏覽器事件循環章節 event loop processing model 對一次事件循環 tick
的定義:在每次執行完當前 task
並清空其附屬 micro-task queue
後會由於性能 至多執行一次 UI 繪製(是否繪製取決於硬件刷新率)。express
11: update the renderingapi
- Rendering opportunities: Remove from docs all Document objects whose browsing context do not have a rendering opportunity.
A browsing context has a rendering opportunity if the user agent is currently able to present the contents of the browsing context to the user, accounting for hardware refresh rate constraints and user agent throttling for performance reasons, but considering content presentable even if it's outside the viewport.
Browsing context rendering opportunities are determined based on hardware constraints such as display refresh rates and other factors such as page performance or whether the page is in the background. Rendering opportunities typically occur at regular intervals.
即始終有一次事件循環 tick
對應 至多一次 UI 繪製。
renderWatcher
來講,在一次事件循環 tick
中,屢次的 renderWatcher.get
觸發,對應屢次 UI
視圖更新。顯然這在一次事件循環 tick
中是多餘的。對於當次 UI
繪製來講,始終僅有最後一次生效。那麼爲了不屢次無用的回調調用,就必定要在一次事件循環 tick
中保證 至多執行一次 renderWatcher.get
函數,進而始終 至多執行一次 組件視圖更新。userWatcher
來講,在一次事件循環 tick
中,可能存在屢次依賴更新,那麼也會存在如同 renderWatcher
同樣的局面,爲了不屢次無用的調用,故應該在一次事件循環 tick
中始終至多執行一次組件視圖更新。基於以上目標,Vue.js
內部藉助 nextTick
機制實如今當次事件循環 tick
的 task
執行過程當中,收集依賴的變化,但不當即執行回調函數,而是讓 nextTick
延遲迴調函數到一個特定的時機來觸發回調。那麼這就有了處理將屢次高頻調用處理爲僅保留最終調用的機會。
那麼本文要闡述的部分核心點以下:
tick
中,收集的全部待執行 watcher
回調函數。什麼是 nextTick
,其內部核心原理又是什麼?這裏以一次 vm.$nextTick
調用來簡要闡述 nextTick
的核心原理。
API
掛載:在初始化 Vue global API
時,會在 renderMixin
中在 Vue
原型對象上經歷如下 $nextTick
掛載:
Vue.prototype.$nextTick = function(fn: Function) { return nextTick(fn, this) }
那麼如下調用:
vm.$nextTick(() => { // do something you like })
本質上是如下調用:
nextTick(() => { // do something you like }, this) // 此處 this 恆定指向 vue 實例
最終獲得原始的 nextTick
函數實現:
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() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
不難看出 nextTick
本質上是在緩存傳入的 cb
函數。其緩存容器是 es module
模塊詞法做用域內的 callbacks
變量。上文函數的職責以下:
cb
函數;pending
爲 falsy 值時,調用模塊詞法做用域中的 timerFunc
函數;cb
函數,且詞法做用域支持 Promise
構造函數時,nextTick
將返回一個 Promise
實例,而不是 undefined
。那麼咱們要探究的 nextTick
的本質,可抽象爲模塊變量 callbacks
和 timerFunc
函數的功能組合體。在 src/core/util/next-tick.js 中,咱們不可貴到 timerFunc
是根據 JS
運行時進行實現。
如下按照運行時優先級進行排序:
在支持 Promise
時,timerFunc
對應:
timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true
在非 IE
且支持 MutationObserver
時:
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
在支持 setImmediate
時:
timerFunc = () => setImmediate(flushCallbacks)
最後,使用如下實現進行兜底:
timerFunc = () => { setTimeout(flushCallbacks, 0) }
不難看出全部的實現有一個共同點是都包含了 flushCallbacks
函數,以下:
function flushCallbacks() { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
上文函數的主要職責在於取 callbacks
容器淺副本,逐個迭代執行 callbacks
中全部函數。以上全部的 timerFunc
實現核心差別點是使用不一樣的 task source
來實現調用 flushCallbacks
函數,即最終 nextTick
本質上是一個 回調函數調度器,優先借助 micro-task
,不然使用 task
來實現清空 callbacks
列表。
那麼結合 nextTick
的總體,不可貴到如下結論:
processing model
實現回調函數延遲調用;micro-task
,不然回退至 task
來實現。callbacks
是 Array
類型,而不是 Function
類型?A: 在當次事件循環 tick
中,可能存在屢次 nextTick
調用,例如:
// SomeComponent.vue export default { created() { this.$nextTick(doSomething) this.$nextTick(doOneMoreTime) } }
那麼使用 Array
類型來緩存當前事件循環中 tick
多個 傳入 nextTick
函數的回調函數。
前文,筆者已經闡述了 nextTick
函數的核心原理和功能,目的是爲了 批量 延遲函數調用。那麼 watcher
的更新又是如何與 nextTick
關聯的呢?將 watcher
的更新藉助 nextTick
的延遲調用能力,那麼咱們就能夠延遲 watcher
的更新,即有機會 在延遲期間 實現合併屢次更新。下文以與視圖惟一對應的 renderWatcher
爲例。
在 Vue.js
中,每一個視圖組件都關聯一個與組件自身惟一對應的 renderWatcher
實例。
// src/core/instance/lifecycle.js#L141-L213 function mountComponent(/* ... */) { // ... if (/* */) { // ... // 此處爲開發環境的邏輯簡化 updateComponent = () => { vm._update(vm._render(), hydrating) } } else { // 此處爲生產環境邏輯 updateComponent = () => { vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */ ) }
在 new Watcher
實例化一個 renderWatcher
時,在 Watcher
構造函數內部會使得當前 renderWatcher
成爲當前 vm
實列的 vm._watcher
屬性。不然 vm._watcher
爲 null
值ref。
不難看出 updateComponent
就是 renderWatcher.getter
對應的函數,那麼在 renderWatcher.update
調用時,本質上是調用的 renderWatcher.getter
,進而調用 updateComponent
實現視圖更新。updateComponent
中對應功能函數以下:
vm._render 以下:
// src/core/instance/render.js#L69-L128 Vue.prototype._render = function(): VNode { // ... const { render, _parentVnode } = vm.$options // ... try { currentRenderingInstance = vm vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { // ... } finally { currentRenderingInstance = null } // ... return vnode }
不可貴出,vm._render
的核心職責在於調用 $options.render 渲染函數之際,指定調用上下文爲 vm._renderProxy
,並傳參 vm.$createElement
函數,並最終 vm._render
產出 vnode
。
vm._update 以下:
// src/core/instance/lifecycle.js#L59-L88 Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) { //... if (!preVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { vm.$el = vm.__patch__(prevVnode.vnode) } // ... }
vm._update
函數的核心在於以上代碼,其中 vm.__patch__
函數的做用如同 snabbdom 庫的 patch
函數。做用是根據 vnode
最小幅度的建立或修改真實的 DOM
。
那麼咱們在某種程度上可認爲 vm._update
函數的核心功能就是將 vm._render
調用產生的 vnode
經過 diff
最小幅度的建立或修改真實的 DOM nodes
。
由以上對 updateComponent
函數的內涵的探討,因此這是筆者說 renderWatcher.getter
在調用之際是在進行視圖更新的緣由。
那麼,根據前文闡述,咱們將 renderWatcher
的核心職責概括以下:
Watcher
時,傳入最後一個參數 true
,使得當前 watcher
實例爲 renderWatcher
。使得 vm._watcher
值爲 renderWatcher
,而不是 null
。renderWatcher
實例的 epxOrFn
參數定義爲 updateComponent
函數,併成爲 renderWatcher.getter
屬性。updateComponent
函數對應了視圖更新函數。那麼當 renderWatcher.getter
被調用時,便是進行 diff
比對,最終實現 最小幅度範圍 的視圖更新。衆所周知,全部的 vue template
都會被 vue-template-compiler
轉換爲 render
函數。在 render
函數中,全部的模板插值都對應了 vm
上對應的字段。
以下 vue template
:
<div id="app">{{ msg }}</div>
將被編譯爲vue template explorer:
function render() { with (this) { return _c( 'div', { attrs: { id: 'app' } }, [_v(_s(msg))] ) } }
上文中 with
語句起到的做用是在其塊級做用域中拓展了做用域,使得 msg
的取值爲 this.msg
,那麼以上渲染函數等價於:
function render() { return _c( 'div', { attrs: { id: 'app' } }, [_v(_s(this.msg))] ) }
根據以前文章對 data
依賴收集及其觸發原理的分析。咱們不可貴到,在 watcher
所訂閱的依賴更新時,將經過 data[dataKey].__ob__.dep.notify
調用 dep.subs[i].update
方法來實現通知訂閱者。
// src/core/observer#L37-L49 export default class Dep { // ... subs: Array<Watcher> // ... notify() { // ... for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
根據類型定義,全部的 dep.subs[i]
均爲 watcher
實例,那麼 subs[i].update
調用,其實是 watcher.update
調用:
// src/core/observer/watcher.js#L160-L173 export default class Watcher { /** * Subscriber interface. * Will be called when a dependency changes. */ update() { /* istanbul ignore else */ if (this.lazy) { // 此 if 分支對應 lazyWatcher this.dirty = true } else if (this.sync) { // 此分支對應 vuex 的 watcher this.run() } else { // 此分支對應 renderWatcher 或 userWatcher queueWatcher(this) } } }
縱觀整個 vue.js
源碼,this.lazy
僅在定義 computed
的鍵值時,纔會 true
,this.sync
僅對 vuex
的 strict mode
下生效見 vuex v3.3.0 源碼,而剩下的 if
分支是着重須要討論的 watcher
與 nextTick
的協做分支。
queueWatcher
以下,從函數語義來看,該函數就是爲了隊列化須要更新的 renderWatcher
和 userWatcher
:
/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */ export function queueWatcher(watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
queueWatcher
藉助一個 key-value
數據結構和惟一的 watcher.id
作了一件很是重要的屢次 watcher
更新 合併/去重操做。僅在 has[id]
爲 falsy 值時,纔會加入到 queue
中。
在初始時,waiting
標識爲 false
,那麼進入到如下 if
語句中:
if (!waiting) { waiting = true // ... nextTick(flushSchedulerQueue) }
經過 nextTick
函數調度了 flushSchedulerQueue
函數的執行。
函數以下:
/** * Flush both queues and run the watchers. */ function flushSchedulerQueue() { // ... // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { // ... break } } } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // call component updated and activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) }
其職責在於:
根據 watcher
建立的前後順序排列 watcher
,根據如下:
queue.sort((a, b) => a.id - b.id)
由於 Array.prototype.sort 屬於原地排序,當回調函數返回值大於 0
時,會在原數組中原地交換 a
,b
順序,故以上排序結果爲小序在前的升序。又由於 watcher
實例化是 自增 ID。因此前文升序排列 watcher
代表父組件 renderWatcher
始終先於子組件 renderWatcher
。
this.id = ++uid // uid for batching
迭代迭代調用 queue
容器中的 watcher.run
方法,進而實現調用其 watcher.get
函數,進而實現調用 watcher.getter
函數(對應實例化 Watcher
時的 expOrFn
參數):
watcher.run
時可能會觸發其餘 watcher
,故迭代時,不會固定容器長度;renderWatcher
來講,watcher.getter
本質上調用的是的 updateComponent
函數,其本質對應了視圖更新函數—— vm._render
vnode
建立函數和 vm._patch
DOM
更新函數。對於 userWatcher
來講,watcher.getter
對應了 $options.watcher[watcherKey]
的取值函數:
export default class Watcher { // ... constructor(/* ... */) { //... // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // 此分支對應了 $options.watcher[watcherKey as string] this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } } }
即 userWatcher.getter
對應調用得到的返回值是 vm[watcherKey]
的值。
另外在調用 userWatcher.run
時,由於 watcher.user
爲 true
,那麼會額外調用 userWatcher.cb
函數。並將 watcher.getter
的返回值和 watcher.value
做爲新舊值傳入 userWatcher.cb
函數。
export default class Watcher { // ... /** * Scheduler job interface.
*/ run() { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { // !! 調用用戶定義的 $options.watch[watchKey] 回調函數 this.cb.call(this.vm, value, oldValue) } catch (e) { // ... } } else { this.cb.call(this.vm, value, oldValue) } } } } } ```
重置 watcher
隊列標識,表示當前隊列已經由 nextTick
調度獲得調用。
resetSchedulerState()
對於 <keep-alive>
的緩存組件,激活 activated
鉤子。
// call component activated hooks callActivatedHooks(activatedQueue)
對當前隊列 queue
中的 renderWatcher
對應的 vm 實例,調用 updated
鉤子。
// call component updated hooks callUpdatedHooks(updatedQueue)
至此,上文已經解釋了 flushSchedulerQueue
背後的本質原理。
結合前文對 nextTick 概述 和對 watcher
更新鏈路和 flushScheduleQueue 的分析,不可貴出如下結論:
renderWatcher
和 userWatcher
更新調用由 queueWatcher
驅動,此時全部的 watcher
更新並不會在當前事件循環 tick
的 task
執行上下文之上獲得執行。queueWatcher
解決的核心思路是 nextTick(flushSchedulerQueue)
函數調用。nextTick
給予了 flushSchedulerQueue
函數延遲調用的能力。nextTick
基於當前 JS
運行時以 micro-task
至 task
的優先級進行實現。全部的 watcher
更新函數的 調用時機徹底取決於 nextTick
的運行時實現:
nextTick
以 miro-task
實現時,全部的 watcher
更新函數在當前事件循環 tick
的清空 micro-task queue
階段獲得執行。nextTick
以 task
實現時,全部的 watcher
更新函數基於一個全新的 task
(即做爲 setTimeout
的回調函數的 flushSchedulerQueue)獲得執行。flushScheduleQueue
函數是最終當前事件循環 tick
中收集的 watcher
更新的 真正執行者。