計算屬性是基於響應式依賴進行緩存的,只有在相關響應式依賴發生改變時纔會從新求值,這種緩存機制在求值消耗比較大的狀況下可以顯著提升性能。
express
Vue 在作數據初始化時,經過 initComputed() 方法初始化計算屬性。
數組
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(`Getter is missing for computed property "${key}".`, vm)
}
if (!isSSR) {
watchers[key] = new Watcher(vm, getter || noop,
noop, computedWatcherOptions)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
複製代碼
首先將變量 watchers 與變量 _computedWatchers 同時指向一個空對象,該對象用來存儲全部計算屬性的觀察者。接着定義變量 isSSR 用來標識是否爲服務器端渲染。
而後對選項 computed 中的屬性遍歷,將屬性賦值給 userDef 變量。計算屬性有兩種寫法:函數、對象。若是是對象的話則必需要有 get(), set() 方法能夠不實現。使用 getter 變量指向可以獲取值的函數。
在服務器端渲染時,使用計算屬性的方式和使用方法基本同樣。在非服務器端渲染時,生成該計算屬性的觀察者添加到變量 watchers 與變量 _computedWatchers 對應的屬性上。
最後檢查 Vue 實例對象上有沒有與計算屬性同名的屬性,由於無論選項 props 、data ,仍是選項 computed 中的數據通過處理最終都會添加到 Vue 實例對象上。若是沒有重複,則調用 defineComputed() 方法將計算屬性處理以後添加到 Vue 實例對象上。
緩存
defineComputed() 函數的功能是將計算屬性轉化爲 Vue 實例對象的訪問器屬性。
服務器
export function defineComputed (target: any, key: string, userDef: Object | Function ) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼
在非服務器端渲染時,若選項中計算屬性是函數形式,則將 set() 方法設爲空;如果對象形式,對象中提供 set() 方法,則採用該方法做爲訪問器屬性的 set() 方法,若是沒有提供,則 set() 方法爲空。最後在 set() 方法爲空的狀況下,重寫該方法,使其在非生產環境下訪問器屬性被設置時提示警告信息。
計算屬性的 get() 方法 爲 createComputedGetter() 的返回值,該返回值爲 computedGetter() 函數。
閉包
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
複製代碼
好比傳入 computed 選項以下:
函數
data () {
retrun {
a: 1
}
}
computed: {
b () {
retrun a * 2
}
}
複製代碼
通過計算屬性初始化處理以後,變成 Vue 實例對象 vm 上的屬性:
oop
vm.b = Object.defineProperty(vm, 'b', {
enumerable: true,
configurable: true,
get () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
},
set () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
})
複製代碼
上面例子中的計算屬性 b 在初始化時建立的觀察者對象 watcherB 以下:
post
watcherB = {
vm: vm,
lazy: true,
deep: false,
user: false,
sync: false,
cb: function () {},
uid: 1,
active: true,
dirty: true,
deps: [],
newDeps: [],
depIds: new Set(),
newDepIds: new Set(),
expression: 'function() {\n return a * 2 \n}',
getter: function() { return a * 2 },
value: undefined
}
複製代碼
在開發中最多見的是在模板中使用計算屬性,該種狀況下會先將模板轉換成渲染函數,而後生成渲染函數觀察者,在此過程當中,會讀取計算屬性。
性能
在計算屬性 b 被讀取時,會調用訪問器屬性的 get() 方法。
ui
get () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
複製代碼
此時,訪問器屬性的 get() 方法中的 watcher 爲 watcherB ,Dep.target 爲渲染函數觀察者,記爲 renderWatcher。函數執行的順序以下:
一、watcherB.dirty 爲 true ,因此先執行 watcherB.evaluate() 。在該方法中首先調用 watcherB.get() 。
二、watcherB.get() 先將 Dep.target 引用變量指向 watcherB ,而後 調用 watcherB.getter() 。
三、watcherB.getter() 會觸發響應式數據 a 的 get() ,在該方法中調用 a 閉包引用的的變量 dep 的 depend() 方法。
四、在 dep.depend() 會調用 watcherB.addDep(dep)。
五、watcherB.addDep(dep)主要作三件事:將 dep.id 添加到 watcherB.newDepIds 集合中、將 dep 添加到 watcherB.newDeps 數組中、調用dep.addSub()方法。
六、dep.addSub() 方法會將 watcherB 添加到響應式數據 a 閉包引用的變量 dep 的 subs 數組中。
七、而後接着執行 watcherB.get(),首先將 Dep.target 的值設置爲 renderWatcher ,再執行 watcherB.cleanupDeps()。
八、watcherB.cleanupDeps() 會將 watcherB.newDepIds、watcherB.newDeps 中的數據分別複製到 watcherB.depIds、watcherB.deps中,而後再清空 watcherB.newDepIds、watcherB.newDeps。
九、而後接着執行 watcherB.evaluate(),將 watcherB.dirty 設置爲 false。
十、而後接着執行計算屬性 b 的 get() 方法,調用 watcherB.depend()。
十一、watcherB.depend() 方法循環 watcherB.deps 數組,調用數據 a 閉包引用的 dep 的 depend() 方法。此時的 Dep.target 的值爲 renderWatcher,dep.depend() 將渲染函數觀察者添加到數據 a 閉包引用的 dep 中。
拋開較爲繁瑣的執行過程不說,當第一次讀取計算屬性時,主要作了如下幾方面的事情:
一、將計算屬性觀察者收集進相關響應式數據的依賴容器中。
二、將使用計算屬性的渲染函數觀察者收集進相關響應式數據的依賴容器中。
三、將計算屬性觀察者的 dirty 屬性設置爲 false。若 dirty 爲 true ,讀取計算屬性時會執行第1條操做。
當計算屬性依賴的響應式數據發生改變時,會觸發依賴容器中的依賴,此時計算屬性以及使用計算屬性的渲染函數都會從新求值。
Vue在數據初始化時會將計算屬性改形成實例對象上的訪問器屬性,同時生成對應的計算屬性觀察者。
在初次讀取計算屬性時,會將計算屬性觀察者、讀取計算屬性的渲染函數觀察者做爲依賴收集到相關響應式數據的依賴容器中。再次讀取計算屬性時,僅僅將讀取計算屬性的渲染函數觀察者收集到依賴容器中,由於計算屬性與響應式數據的依賴關係沒有發生變化,此時計算屬性觀察者已做爲依賴被收集,沒必要重複添加。
當相關響應式數據改變時,對應的計算屬性以及渲染函數都會發生改變。
計算屬性就像響應式數據與渲染函數觀察者之間的「橋樑」。若是渲染函數直接使用響應式數據,則只須要收集渲染函數觀察者,計算屬性存在的意義在於對響應式數據進行操做,並且不用每次都通過計算求值。
如需轉載,煩請註明出處:juejin.im/post/5cc159…