上一章 Vue2非同步更新和nextTick原理,我們介紹了 JavaScript 執行機制是什麼?nextTick原始碼是如何實現的?以及Vue是如何非同步更新渲染的?
本章目標
在 Vue初始化範例的過程中,如果使用者 options選項中存在計算屬性時,則初始化計算屬性
// 初始化狀態
export function initState(vm) {
const opts = vm.$options // 獲取所有的選項
// 初始化資料
if (opts.data) { initData(vm) }
// 初始化計算屬性
if (opts.computed) { initComputed(vm) }
}
我們給每個計算屬性都建立了一個 Watcher範例,標識為lazy:true
, 在初始化watcher時不會立即執行 get方法(計算屬性方法)
並將計算屬性watcher 都儲存到了 Vue範例上,讓我們可以在後續的 getter方法中通過 vm獲取到當前的計算屬性watcher
然後使用Object.defineProperty
去劫持計算屬性
// 初始化計算屬性
function initComputed(vm) {
const computed = vm.$options.computed
const watchers = (vm._computedWatchers = {}) // 將每個計算屬性對應的watcher 都儲存到 vm上
for (let key in computed) {
let userDef = computed[key]
// 相容不同寫法 函數方式 和 物件getter/setter方式
let fn = typeof userDef === 'function' ? userDef : userDef.get
// 給每個計算屬性都建立一個 watcher,並標識為 lazy,不會立即執行 get-fn
watchers[key] = new Watcher(vm, fn, { lazy: true })
// 劫持計算屬性getter/setter
defineComputed(vm, key, userDef)
}
}
// 劫持計算屬性
function defineComputed(target, key, userDef) {
const setter = userDef.set || (() => {})
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter,
})
}
當我們劫持到計算屬性被存取時,根據 dirty 值去決定是否更新 watcher快取值
然後讓自己依賴的屬性(準確來說是訂閱的所有dep)都去收集上層watcher,即 Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
// 劫持計算屬性的存取
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key] // this就是 defineProperty 劫持的targer。獲取到計算屬性對應的watcher
// 如果是髒的,就去執行使用者傳入的函數
if (watcher.dirty) {
watcher.evaluate() // 重新求值後 dirty變為false,下次就不求值了,走快取值
}
// 當前計算屬性watcher 出棧後,還有渲染watcher 或者其他計算屬性watcher,我們應該讓當前計算屬性watcher 訂閱的 dep,也去收集上一層的watcher 即Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
if (Dep.target) {
watcher.depend()
}
// 返回watcher上的值
return watcher.value
}
Dep.target
:當前渲染的 watcher,靜態變數stack
:存放 watcher 的棧。 利用 pushTarget、popTarget 這兩個方法做入棧出棧操作// 當前渲染的 watcher
Dep.target = null
// 存放 watcher 的棧
let stack = []
// 當前 watcher 入棧, Dep.target 指向 當前 watcher
export function pushTarget(watcher) {
stack.push(watcher)
Dep.target = watcher
}
// 棧中最後一個 watcher 出棧,Dep.target指向棧中 最後一個 watcher,若棧為空,則為 undefined
export function popTarget() {
stack.pop()
Dep.target = stack[stack.length - 1]
}
在初始化Vue範例時,我們會給每個計算屬性都建立一個對應watcher(我們稱之為計算屬性watcher,除此之外還有 渲染watcher 和 偵聽器watcher ),他有一個 value 屬性用於快取計算屬性方法的返回值。
預設標識 lazy: true,懶的,代表計算屬性watcher,建立時不會立即執行 get方法
預設標識 dirty: true,髒的,當我們劫持到計算屬性存取時,如果是髒的,我們會通過watcher.evaluate
重新計算 watcher 的 value值 並將其標識為乾淨的;如果是乾淨的,則直接取 watcher 快取值
depend 方法,會讓計算屬性watcher 訂閱的dep去收集上層watcher,可能是渲染watcher,也可能是計算屬性watcher(計算屬性巢狀的情況),實現洋蔥模型的核心方法
update 方法,當計算屬性依賴的物件發生變化時,會觸發dep.notify
派發更新 並 呼叫 update 方法,只需更新 dirty 為 true即可。我們會在後續的渲染watcher 更新時,劫持到計算屬性的存取操作,並通過 watcher.evaluate
重新計算其 value值
class Watcher {
constructor(vm, fn, options) {
// 計算屬性watcher 用到的屬性
this.vm = vm
this.lazy = options.lazy // 懶的,不會立即執行get方法
this.dirty = this.lazy // 髒的,決定重新讀取get返回值 還是 讀取快取值
this.value = this.lazy ? undefined : this.get() // 儲存 get返回值
}
// 重新渲染
update() {
console.log('watcher-update')
if (this.lazy) {
// 計算屬性依賴的值發生改變,觸發 setter 通知 watcher 更新,將計算屬性watcher 標識為髒值即可
// 後面還會觸發渲染watcher,會走 evaluate 重新讀取返回值
this.dirty = true
} else {
queueWatcher(this) // 把當前的watcher 暫存起來,非同步佇列渲染
// this.get(); // 重新渲染
}
}
// 計算屬性watcher為髒時,執行 evaluate,並將其標識為乾淨的
evaluate() {
this.value = this.get() // 重新獲取到使用者函數的返回值
this.dirty = false
}
// 用於洋蔥模型中計算屬性watcher 訂閱的dep去 depend收集上層watcher 即Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
計算屬性是基於它們的響應式依賴進行快取的。只在相關響應式依賴發生改變時它們才會重新求值。 快取原理如下:
在初始化計算屬性時,我們使用Object.defineProperty
劫持了計算屬性,並做了一些 getter/setter操作
計算屬性watcher 有一個 dirty髒值屬性,預設為 true
當我們劫持到計算屬性被存取時,如果 dirty 為 true,則執行 evaluate 更新 watcher 的 value值 並 將 dirty 標識為 false;如果為 false,則直接取 watcher 的快取值
當計算屬性依賴的屬性變化時,會通知 watcher 呼叫 update方法,此時我們將 dirty 標識為 true。這樣再次取值時會重新進行計算
在初始化Vue範例時,我們會給每個計算屬性都建立一個對應的懶的watcher,不會立即呼叫計算屬性方法
當我們存取計算屬性時,會通過watcher.evaluate()
讓其直接依賴的屬性去收集當前的計算屬性watcher,並且還會通過watcher.depend()
讓其訂閱的所有 dep都去收集上層watcher,可能是渲染watcher,也可能是計算屬性watcher(如果存在計算屬性巢狀計算屬性的話)。這樣依賴的屬性發生變化也可以讓檢視進行更新
讓我們一起來分析下計算屬性巢狀的例子
<p>{{fullName}}</p>
computed: {
fullAge() {
return '今年' + this.age
},
fullName() {
console.log('run')
return this.firstName + ' ' + this.lastName + ' ' + this.fullAge
},
}
stack:[渲染watcher]
computed watcher1.evaluate()
,watcher1
入棧
stack:[渲染watcher, watcher1]
watcher1
的 get方法時,其直接依賴的 firstName 和 lastName 會去收集當前的 watcher1;然後又存取 fullAge 並執行computed watcher2.evaluate()
,watcher2
入棧
watcher1:[firstName, lastName]
stack:[渲染watcher, watcher1, watcher2]
watcher2
的 get方法時,其直接依賴的 age 會去收集當前的 watcher2
watcher2:[age]
watcher2
出棧,並執行watcher2.depend()
,讓watcher2
訂閱的 dep再去收集當前watcher1
stack:[渲染watcher, watcher1]
watcher1:[firstName, lastName, age]
watcher1
出棧,執行watcher1.depend()
,讓watcher1
訂閱的 dep再去收集當前的渲染watcher
stack:[渲染watcher]
渲染watcher:[firstName, lastName, age]