前言
上文還漏了一些重要的諸如異步更新授段、computed
等細節(jié)耀找,本文補齊
正文
異步更新
上文講到Watcher
里的this.sync
是來控制同步與異步觸發(fā)依賴更新的。同步的話缺點很明顯洋幻,試想一下如下例子
new Vue({
data: {
a: 1,
b: 2
},
template: `<div @click="change">
{{a}}{讲竿}
</div>`,
methods: {
change() {
this.a = 2
this.b = 3
}
}
}).$mount('#app')
同時改動了this.a、this.b
庄涡,因為這倆屬性都收集了renderWatcher
量承,若是同步的話那么就會執(zhí)行倆遍渲染函數(shù),這是不明智的穴店,所以若是異步的話可以將其更新回調(diào)放入異步更新隊列撕捍,就可以一次遍歷觸發(fā),現(xiàn)在我們看看異步的邏輯泣洞,即
update() {
if (this.computed) {
// ...
} else if (this.sync) {
// ...
} else {
queueWatcher(this)
}
}
queueWatcher
方法在scheduler .js
里忧风,具體看下文
computed
首先看看initComputed,從此我們可得知initComputed
就是劫持computed
球凰,將其轉(zhuǎn)化為響應(yīng)式對象
計算屬性其實就是惰性求值
watcher
狮腿,它觀測get
里的響應(yīng)式屬性(若是如Date.now()
之類非響應(yīng)式是不會觸發(fā)變化的),一旦其變化這個get
就會觸發(fā)(get
作為Watcher
的expOrFn
)呕诉,如此一來該計算屬性的值也就重新求值了
和普通
watcher
區(qū)別就是它不會立即求值只有在被引用觸發(fā)其響應(yīng)式屬性get
才會求值蚤霞,而普通watcher
一旦創(chuàng)建就會求值
new Vue({
data: {
a: 1
},
computed: {
b: {
get() {
return this.a + 1
},
set(val) {
this.a = val - 1
}
}
},
template: `<div @click="change">
{{a}}{}
</div>`,
methods: {
change() {
this.a = 2
}
}
}).$mount('#app')
以此為例义钉,轉(zhuǎn)化結(jié)果如下
Object.defineProperty(target, key, {
get: function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
},
set: function set(val) {
this.a = val - 1
}
})
從這可見只有get
被觸發(fā)才會開始其依賴收集昧绣,這和普通watcher
創(chuàng)建就求值從而開始依賴收集是不一樣的
依賴收集
如此例所示,
b
是依賴a
的捶闸,所以a的dep
得收集到b
的watcher
(這樣子a
變化可以通知b
重新求值)夜畴,模板渲染依賴b
,所以b的dep
得收集到renderWatcher
也就是當計算屬性b
被讀壬咀场(在此是模板引用{{ b }}
)贪绘,該get
會被執(zhí)行
首先其定義了watcher
變量來存儲this._computedWatchers[key]
,通過前文我們知道在initComputed
里遍歷vm.$options.computed
給每個都new Watcher
央碟,所以該watcher
就是計算屬性b
的觀察者對象
watcher <==> new Watcher(vm, b.get, noop, { computed: true })
若該對象存在的話就會執(zhí)行該對象的depend税灌、evalute
首先我們看Watcher.depend
depend() {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
這個很簡單,就是判斷下this.dep && Dep.target
,存在的話就調(diào)用this.dep.depend()
菱涤。這個this.dep
就是計算屬性的dep
苞也,它初始化在Watcher constructor
constructor() {
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
我們可見非計算屬性watcher
是直接執(zhí)行this.get
來觸發(fā)響應(yīng)式屬性的get
從而收集依賴,計算屬性watcher
就是初始化了this.dep
也就是該響應(yīng)式計算屬性對應(yīng)的dep
前者直接開始求值粘秆,后者只有在訪問到的時候才求值
回到this.dep.depend()
方法如迟,我們看上訴提到的a的dep
、b的dep
如何收集依賴
- 我們知道這個就是收集依賴攻走,那么我們得知道
Dep.target
是什么殷勘,這個其實是renderWatcher
,因為計算屬性b
被renderWatcher
依賴昔搂,也就是這b.get
是render
觸發(fā)訪問的玲销,這就完成了b的dep
收集
watcher.depend()
完了之后還有return watcher.evaluate()
evaluate() {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
首先判斷dirty
,我們之前就有說過true
為未求值摘符、false
為已求值贤斜,這個就是computed
緩存來源
未求值的話就是執(zhí)行this.get()
求值,其實這相當于執(zhí)行b.get
议慰。注意這里Dep.target
已經(jīng)變成了計算屬性b
的watcher
get() {
return this.a + 1
}
- 關(guān)鍵到了,這里訪問了
this.a
就觸發(fā)了a.get
奴曙,這樣子就會導(dǎo)致a的dep
收集到計算屬性b
的watcher
如此我們就完成了依賴收集
依賴觸發(fā)
我們現(xiàn)在觸發(fā)了例子里的change
函數(shù)别凹,也就是修改this.a
的值(其實修改this.b
也一樣內(nèi)在還是修改this.a
)
我們知道
a.dep.subs <===> [renderWatcher, bWatcher]
那么修改a
就會觸發(fā)這倆個watcher.update
update() {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
// 這里是個優(yōu)化
queueWatcher(this)
}
}
這里除了computed
我們都有講過,所以我們這里講computed
this.dep <==> b.dep
this.dep.subs.length <==> b.dep.subs.length <==> [renderWatcher].length === 1
首先判斷下這個this.dep.subs.length === 0
洽糟,我們知道dirty === true
是未求值炉菲,所以在該屬性未被依賴的時候(未被引用)將dirty
置為true
(其實就是重置默認值),這樣子當被依賴的時候(被引用)evaluate
就會重新求值(dirty === false
的話evaluate
不會重新求值)
此例來看this.dep.subs.length === 1
坤溃,所以走else
分支拍霜,調(diào)用getAndInvoke
方法重新求值設(shè)置新值之后執(zhí)行this.dep.notify()
,通知訂閱了b
變化的更新(也就是通知renderWatcher
更新)
scheduler.js
這個文件里存儲的都是異步執(zhí)行更新的相關(guān)方法
queueWatcher
就是個watcher
入隊的方法
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
/**
* 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)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
就像這個注釋所言該方法就是push
一個watcher
到觀察者隊列薪介,總體來看就是入隊列祠饺,然后調(diào)用nextTick
在下一個Tick
執(zhí)行flushSchedulerQueue
,也就是在下一個Tick
之前會入隊完畢汁政,接下來我我們看如何入隊的
首先就是獲取這個入隊的watcher.id
道偷,我們定義了has
這個對象用于紀錄入隊的watcher
。先判斷下這個watcher
是否已經(jīng)入隊记劈,已入隊的話就啥也不干勺鸦,未入隊的話就給此watcher
標記在has
上
然后就是判斷下這個flushing
。它是用于判斷是否執(zhí)行更新中目木,也就是更新隊列是否正在被執(zhí)行换途,默認是false
。
- 若是未執(zhí)行更新中自然就是簡單入隊即可
- 若是在執(zhí)行更新中卻有觀察者要入隊那么就得考慮好這個要入隊的
watcher
插在哪,也就是得插入到正在執(zhí)行的watcher
后面军拟,假設(shè)已經(jīng)有倆[{id: 1}, {id: 2}]
剃执,假設(shè)已經(jīng)循環(huán)到{id: 1}
這個watcher
,那么這時候index
還是0吻谋,我們要插入的位置也是{id: 1}
后面
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
很明顯這個尋找插入點是倒序查找忠蝗,這里判斷queue[i].id > watcher.id
是因為flushSchedulerQueue
里對queue
做了針對id
的排序
最后就是判斷下waiting
,這個就是個防止多次觸發(fā)nextTick(flushSchedulerQueue)
的一個標志漓拾,算是個小技巧
這個方法包含倆部分:
- 觀察者入隊
- 下一個
tick
執(zhí)行更新隊列
flushSchedulerQueue
export const MAX_UPDATE_COUNT = 100
const activatedChildren: Array<Component> = []
let circular: { [key: number]: number } = {}
function flushSchedulerQueue() {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
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
}
}
}
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
這個方法就是具體的執(zhí)行更新隊列的所在阁最,首先就是將flushing
置為true
,然后就是將queue
隊列按照watcher.id
從小到大排列這是有門道的主要是以下三點:
- 組件更新是父到子的骇两,先創(chuàng)建父然后是子速种,所以需要父在前
-
userWatch
在renderWatch
之前,因為userWatch
定義的更早 - 若是一個組件在父組件的
watcher
執(zhí)行期間被銷毀低千,那么子組件的watcher
自然也得跳過配阵,所以父組件的先執(zhí)行
for (index = 0; index < queue.length; index++) {
// ...
}
這里就是存儲執(zhí)行更新時當前watcher
的索引index
的地方,這里有個點需要注意的是不存儲queue.length
示血,因為在執(zhí)行更新中queue
可能會變化
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
這里就是before
鉤子所在
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */ )
就像這個renderWatcher
就有傳入before
這就是
beforeUpdate
所在
然后就是給當前watcher
移出has
這個記錄表棋傍,表示這個id
的watcher
已經(jīng)處理了,可以繼續(xù)入隊难审,因為執(zhí)行更新中也可能有watcher
入隊瘫拣,然后執(zhí)行watcher.run()
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
}
}
這段很重要,就是在開發(fā)環(huán)境下對無限循環(huán)的一個提示告喊。因為watcher.run
(watcher
回調(diào))可能會導(dǎo)致has[id]
有值麸拄,如下所示:
new Vue({
data: {
a: 1
},
watch: {
'a': function aCallback(nVal, oVal) {
this.a = Math.random()
}
},
template: `<div @click="change">
{{a}}
</div>`,
methods: {
change() {
this.a = 2
}
}
}).$mount('#app')
在執(zhí)行到flushSchedulerQueue
時,queue
會有倆個watcher
:a:userWatcher
黔姜、renderWatcher
首先是userWatcher
拢切,在watcher.run()
(也就是aCallback
這個回調(diào)函數(shù))之前has[id] = null
,然后執(zhí)行了這個aCallback
又給this.a
賦值秆吵,這樣子就是在執(zhí)行更新中watcher
入隊(set() -> dep.notify() -> watcher.update() -> queueWatcher(this)
)
如此一來執(zhí)行到queue
下一個項其實還是當前這個userWatcher
淮椰,就沒完沒了了
所以這里定了個規(guī)矩,就是這同一個watcher
執(zhí)行了MAX_UPDATE_COUNT
也就是100次那說明這個有問題纳寂,可能就是無限循環(huán)了实苞。circular
就是這么個標識變量
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
這里我們先看updatedQueue
,它是queue
的淺拷貝對象烈疚。這是因為緊隨其后調(diào)用了resetSchedulerState
黔牵,若不淺拷貝的話queue
就被置空了,這也杜絕了queue
被影響
// 重置scheduler狀態(tài)
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0
has = {}
// 若是開發(fā)環(huán)境爷肝,那么就每輪更新執(zhí)行之后置空這個無限循環(huán)檢測標志
// 這是因為下面檢測也是開發(fā)環(huán)境下檢測的
// 也就是默認生存環(huán)境下不會出現(xiàn)這種糟糕的代碼
if (process.env.NODE_ENV !== 'production') {
circular = {}
}
waiting = flushing = false
}
他就是重置scheduler
里這么些方法所用到的標識變量
這里只在非生產(chǎn)環(huán)境重置了
circular
猾浦,這就代表生存環(huán)境下不會出現(xiàn)這種糟糕的代碼
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
然后就是callUpdatedHooks
// 執(zhí)行updated鉤子
function callUpdatedHooks(queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
// 要是當前這個watcher是渲染watcher陆错,而且已經(jīng)掛載了,那么觸發(fā)updated鉤子
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
這里就是統(tǒng)一調(diào)用update
鉤子金赦,這個和before
非統(tǒng)一調(diào)用不一樣音瓷,這里通過watcher
獲取到vm
也就是當前的實例對象,這樣子就可以取到vm. _watcher
也就是renderWatcher
夹抗,也可以取到vm._isMounted
绳慎。有了這倆個條件就可以調(diào)用生命周期update
了
這就是
updated
所在
if (devtools && config.devtools) {
devtools.emit('flush')
}
這里是開發(fā)者工具的事件傳遞flush
queueActivatedComponent
這個其實是入隊激活的組件,類似queueWatcher
入隊watcher
export function queueActivatedComponent(vm: Component) {
// setting _inactive to false here so that a render function can
// rely on checking whether it's in an inactive tree (e.g. router-view)
vm._inactive = false
activatedChildren.push(vm)
}
這里就是將入隊的實例的激活狀態(tài)(_inactive
)置為激活漠烧,然后入隊到activatedChildren
以待后用
function callActivatedHooks(queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
這個其實就是統(tǒng)一遍歷調(diào)用actibated
鉤子杏愤,給每一項實例的激活狀態(tài)(_inactive
)置為未激活(從未激活到激活)
注意這里activateChildComponent
傳入的第二參數(shù)是true
,在講到keep-alive
詳解