之前介紹過初始化時 Vue 對數(shù)據(jù)的響應(yīng)式處理是利用了Object.defifineProperty()
敛惊,通過定義對象屬性 getter
方法攔截對象屬性的訪問,進(jìn)行依賴的收集凸克,依賴收集的作用就是在數(shù)據(jù)變更的時候能通知到相關(guān)依賴進(jìn)行更新。
通知更新
setter
當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變更時闷沥,會觸發(fā)攔截的 setter 函數(shù)萎战,先來看看 setter :
// src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// ...
// 劫持修改操作
set: function reactiveSetter (newVal) {
// 舊的 obj[key]
const value = getter ? getter.call(obj) : val
// 如果新舊值一樣,則直接 return舆逃,無需更新
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// setter 不存在說明該屬性是一個只讀屬性蚂维,直接 return
if (getter && !setter) return
// 設(shè)置新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 對新值進(jìn)行觀察,讓新值也是響應(yīng)式的
childOb = !shallow && observe(newVal)
// 依賴通知更新
dep.notify()
}
})
}
dep.notify()
// src/core/observer/dep.js
// 通知更新
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
遍歷 dep
中存儲的 watcher
路狮,執(zhí)行 watcher.update()
虫啥。
watcher.update()
// src/core/observer/watcher.js
export default class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
// 懶執(zhí)行時走這里,比如 computed watcher
// 將 dirty 置為 true奄妨,計算屬性的求值就會重新計算
this.dirty = true
} else if (this.sync) {
// 同步執(zhí)行涂籽,在使用 vm.$watch 或者 watch 選項時可以傳一個 sync 選項,
// 當(dāng)為 true 時在數(shù)據(jù)更新時該 watcher 就不走異步更新隊列砸抛,直接執(zhí)行 this.run方法進(jìn)行更新
// 這個屬性在官方文檔中沒有出現(xiàn)
this.run()
} else {
// 更新時一般都這里评雌,將 watcher 放入 watcher 隊列
queueWatcher(this)
}
}
}
queueWatcher
// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
* 將 watcher 放入 queue 隊列
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果 watcher 已經(jīng)存在,則跳過
if (has[id] == null) {
// 緩存 watcher.id直焙,用于判斷 watcher 是否已經(jīng)入隊
has[id] = true
if (!flushing) {
// 當(dāng)前沒有處于刷新隊列狀態(tài)景东,watcher 直接入隊
queue.push(watcher)
} else {
// 正在刷新隊列,這時用戶可能添加新的 watcher,就會走到這里
// 從后往前找奔誓,找到第一個 watcher.id 比當(dāng)前隊列中 watcher.id 大的位置斤吐,然后將自己插入到該位置。保持隊列是有序的。
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// waiting 保證了 nextTick 的調(diào)用只有一次
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 直接刷新調(diào)度隊列
// 一般不會走這兒曲初,Vue 默認(rèn)是異步執(zhí)行,如果改為同步執(zhí)行杯聚,性能會大打折扣
flushSchedulerQueue()
return
}
// nextTick => vm.$nextTick臼婆、Vue.nextTick
nextTick(flushSchedulerQueue)
}
}
}
nextTick
等會再看,它的作用主要就是把 flushSchedulerQueue
使用異步任務(wù)去執(zhí)行幌绍,先嘗試用微任務(wù)颁褂,不支持的情況再用宏任務(wù)去執(zhí)行。
那么先看看 flushSchedulerQueue
的作用:
flushSchedulerQueue
// src/core/observer/scheduler.js
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 對隊列做了從小到大的排序傀广,目的:
// 1. 組件的更新由父到子,因為父組件在子組件之前被創(chuàng)建,所以 watcher 的創(chuàng)建也是先父后子颁独,執(zhí)行順序也應(yīng)該保持先父后子。
// 2. 一個組件的用戶 watcher 先于渲染 watcher 執(zhí)行伪冰,以為用戶 watcher 創(chuàng)建先于渲染 watcher誓酒。
// 3. 如果一個組件在父組件的 watcher 執(zhí)行期間被銷毀,那么它對應(yīng)的 watcher 執(zhí)行都可以被跳過贮聂,所以父組件的 watcher 應(yīng)該先執(zhí)行靠柑。
queue.sort((a, b) => a.id - b.id)
// 在遍歷的時候每次都會對 queue.length 求值,因為在 watcher.run() 的時候吓懈,很可能用戶會再次添加新的 watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
// 執(zhí)行 beforeUpdate 生命周期鉤子歼冰,在 mount 階段創(chuàng)建 Watcher 時傳入
if (watcher.before) {
watcher.before()
}
// 將緩存的 watcher 清除
id = watcher.id
has[id] = null
// 執(zhí)行 watcher.run,最終觸發(fā)更新函數(shù)
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) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// 在重置狀態(tài)之前保留隊列的副本
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
//重置刷新隊列狀態(tài)
resetSchedulerState()
// keep-alive 組件相關(guān)
callActivatedHooks(activatedQueue)
// 執(zhí)行 updated 生命周期鉤子
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
/**
* 把這些控制流程狀態(tài)的一些變量恢復(fù)到初始值耻警,把 watcher 隊列清空隔嫡。
*/
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
/**
* 由子組件到父組件依次執(zhí)行 updated 生命周期鉤子
*/
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
上面代碼可以看出 flushSchedulerQueue
的作用就是執(zhí)行更新隊列。通過 watcher.run()
觸發(fā)最終的更新甘穿。
watcher.run()
// src/core/observer/watcher.js
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.cb = cb
}
run () {
if (this.active) {
// 調(diào)用 this.get 方法
const value = this.get()
if (
value !== this.value || // 新舊值不相等
isObject(value) || // 新值是對象
this.deep // deep模式
) {
// 更新舊值為新值
const oldValue = this.value
this.value = value
if (this.user) {
// 如果是用戶 watcher
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
// 渲染 watcher腮恩,this.cb = noop,一個空函數(shù)
this.cb.call(this.vm, value, oldValue)
}
}
}
}
}
這里有兩種情況扒磁,當(dāng) this.user
為 true
的時候代表用戶 watcher
庆揪,在之前介紹過也就是 user watcher
, 否則執(zhí)行渲染 watcher
的邏輯。
- user watcher
invokeWithErrorHandling
接收的第一個參數(shù)就是我們自定義偵聽屬性的回調(diào)函數(shù)妨托,在初始化偵聽屬性 initWatch
方法過程中缸榛,實例化 new Watcher(vm, expOrFn, cb, options)
的時候傳入。
第三個參數(shù)就是 [value, oldValue]
(新值和舊值)兰伤,這也就是為什么在偵聽屬性的回調(diào)函數(shù)中能獲得新值和舊值内颗。
// src/core/util/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
// 利用 try catch 做一些錯誤處理
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
- 渲染 watcher
如果是渲染 watcher
則執(zhí)行 this.cb.call(this.vm, value, oldValue)
。渲染 Wather
的實例化是在掛載時 mountComponent
方法中執(zhí)行的:
// src/core/instance/lifecycle.js
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
export function noop (a?: any, b?: any, c?: any) {}
是一個空函數(shù)敦腔,所以 this.cb.call(this.vm, value, oldValue)
均澳,就是在執(zhí)行一個空函數(shù)。
渲染 watcher
在執(zhí)行 watcher.run
會調(diào)用 this.get()
,也就會執(zhí)行 this.getter.call(vm, vm)
找前。this.getter
實際就是實例化時傳入的第二個參數(shù) updateComponent
糟袁。
// src/core/instance/lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
所以這就是當(dāng)我們?nèi)バ薷慕M件相關(guān)的響應(yīng)式數(shù)據(jù)的時候,會觸發(fā)組件重新渲染的原因躺盛,接著就會進(jìn)入 patch
的過程项戴。
nextTick
前面介紹了 flushSchedulerQueue
的作用就是去執(zhí)行更新隊列,那么我們看看 queueWatcher
中的這段代碼是怎么回事:
nextTick(flushSchedulerQueue)
nextTick
// src/core/util/next-tick.js
const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 用 callbacks 數(shù)組存儲經(jīng)過包裝的 cb 函數(shù)
callbacks.push(() => {
if (cb) {
// 用 try catch 包裝回調(diào)函數(shù)槽惫,便于錯誤捕獲
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
第一個參數(shù)是一個回調(diào)函數(shù)周叮,這里的回調(diào)函數(shù)對應(yīng)的就是 flushSchedulerQueue
了。通過 try catch
將回調(diào)函數(shù)包裝界斜,用于錯誤捕獲仿耽,然后將其放入 callbacks
中。
這里使用 callbacks
而不是直接在 nextTick
中執(zhí)行回調(diào)函數(shù)的原因是保證在同一個 tick 內(nèi)多次執(zhí)行 nextTick
各薇,不會開啟多個異步任務(wù)项贺,而把這些異步任務(wù)都壓成一個同步任務(wù),在下一個 tick 執(zhí)行完畢得糜。
接下來當(dāng) pending
為 false
的時候執(zhí)行 timerFunc
敬扛,pending
為 true
,表示正在將任務(wù)放入瀏覽器的任務(wù)隊列中朝抖;pending
為 false
啥箭,表示任務(wù)已經(jīng)放入瀏覽器任務(wù)隊列中了。
最后治宣,nextTick
在沒有傳入 cb
回調(diào)函數(shù)的時候急侥,會返回 promise
,提供了一個 .then
的調(diào)用侮邀。
nextTick().then(() => {})
timerFunc
// src/core/util/next-tick.js
// 可以看到 timerFunc 的作用很簡單坏怪,就是將 flushCallbacks 函數(shù)放入瀏覽器的異步任務(wù)隊列中
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
// 首選 Promise
p.then(flushCallbacks)
/**
* 在有問題的UIWebViews中,Promise.then不會完全中斷绊茧,但是它可能會陷入怪異的狀態(tài)铝宵,
* 在這種狀態(tài)下,回調(diào)被推入微任務(wù)隊列华畏,但隊列沒有被刷新鹏秋,直到瀏覽器需要執(zhí)行其他工作,例如處理一個計時器亡笑。
* 因此侣夷,我們可以通過添加空計時器來“強制”刷新微任務(wù)隊列。
*/
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 然后使用 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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 然后 setImmediate仑乌,宏任務(wù)
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最后 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
flushCallbacks
// src/core/util/next-tick.js
/**
* 1百拓、將 pending 置為 false
* 2琴锭、清空 callbacks 數(shù)組
* 3、執(zhí)行 callbacks 數(shù)組中的每一個函數(shù)(比如 flushSchedulerQueue衙传、用戶調(diào)用 nextTick 傳遞的回調(diào)函數(shù))
*/
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
不管是全局 API
Vue.nextTick
决帖,還是實例方法vm.$nextTick
,最后都是調(diào)用next-tick.js
中的nextTick
方法蓖捶。
相關(guān)鏈接
如果覺得還湊合的話古瓤,給個贊吧!O傺簟!也可以來我的個人博客逛逛 https://www.mingme.net/