Vue 的 computed 經常會用到,其中包含以下兩個重點:
1、 computed
的計算結果會進行緩存;
2、只有在響應式依賴發生改變時纔會重新計算結果。
接下從源碼的出發,看看能不能驗證這兩個重點。爲了能更好理解 computed 的實現,文章字數會比較多,請耐心閱讀。
源碼分析
// vue/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 初始化 props
if (opts.props) initProps(vm, opts.props)
// 初始化 methods
if (opts.methods) initMethods(vm, opts.methods)
// 初始化 data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
從初始化狀態的順序可以看出,在翻轉字符串的例子中會先初始化 data
,再進行初始化 computed
。
data 初始化
先看看初始化 data
做了什麼,initData
源碼如下:
// vue/src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
// 兼容 對象或函數返回對象的寫法
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 判斷 data 是否爲普通對象
if (!isPlainObject(data)) {
// data 不是普通對象,重新賦值爲空對象,並在輸出警告
data = {}
...
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
// data 的屬性不能與 methods、 props 的屬性重複
if (process.env.NODE_ENV !== 'production') {
// 重複 key,輸出警告
...
} else if (!isReserved(key)) {
// 將每個 key 掛載到實例上,在組件內就可以用 this.key 取值
proxy(vm, `_data`, key)
}
}
// 監聽 data
// observe data
observe(data, true /* asRootData */)
}
初始化 data,主要做了 3 點,1、屬性名重複的判斷;2、將屬性掛載到 vm 上;3、監聽 data。
接下來看看 observe
的實現,源碼如下:
// vue/src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 非對象 或者是 VNode,直接 return
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 存在 '__ob__' 屬性,表示已經監聽
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 開始創建監聽
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
接下來則到了 Observer
類,源碼如下:
// vue/src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 將 '__ob__' 掛載到 value 上,避免重複監聽
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 將對象每個屬性添加 getter、 setter
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 對數組的每一項進行監聽
observe(items[i])
}
}
}
接下來會調用 defineReactive
,源碼如下:
// vue/src/core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// dep 用於依賴收集
const dep = new Dep()
...
// data 的值有可能包含數組、對象,在這裏 data 的值進行監聽
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// Dep.target 是一個靜態屬性
// 給 data 的屬性添加 getter 時,target 爲 undefined,不會進行依賴收集
// 當 computed 用了 data中的屬性時時將會進行依賴收集,先跳過這部分,等到了 computed 再回來看
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
...
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 當值發生變化時,通知所有訂閱者進行更新
dep.notify()
}
})
}
defineReactive
中用到了 Dep
用來進行依賴收集,接下來看看 Dep
的源碼:
// vue/src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加訂閱者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 刪除訂閱者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 將 Dep 實例傳遞給目標 Watcher 上,目標 Watcher 再通過 addSub 進行訂閱
depend () {
// 只有目標 Watcher 存在纔可以進行訂閱
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知訂閱者
notify () {
// 根據 Watcher id 進行排序,通知更新
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++) {
// 調用訂閱 Watcher 的 update 方法進行更新
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
// 添加目標 Watcher,並將 Dep.target 指向最新的 Wathcer
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
// 移除目標 Watcher,並將 Dep.target 指向 targetStack 的最後一個
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Dep
其實就是一個訂閱發佈模式,說明一下最主要的兩個地方
1、pushTarget
、popTarget
這兩個方法中用到了 targetStack 堆棧,這樣做就可以進行嵌套,比如在給某個 Watcher 收集依賴的時候,發現了新的 Watcher 需要收集依賴,這樣就可以 target 指向新的 Watcher,先把新的 Watcher 收集完再 popTarget,再進行上一個 Watcher 的收集。
2、depend
depend
執行的是 Watcher 的 addDep
方法,看看 addDep
怎麼寫的
// vue/src/core/observer/watcher.js
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
addDep
做了一些判斷避免重複訂閱,再調用 addSub
添加訂閱。
再回過頭來看看 initData
。
// vue/src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
}
當 data
是一個函數時,會調用 getData
獲取 data
函數的返回值,看看 getData
的實現。
// vue/src/core/instance/state.js
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
可以看到在執行 data
函數前後,執行了 pushTarget
和 popTarget
的操作,因爲 data
的屬性並不依賴其他響應式變量、在設置 getter
和 setter
時,因爲 dep.target
爲 undefined
所以並不會收集依賴。
data 的初始化到這裏就差不多了,接下來看看 computed 的初始化。
computed 初始化
同樣的,先從 initComputed
方法開始
// vue/src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// 創建空對象,綁定到 vm._computedWatchers 上
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
// computed 如果是函數就當成 getter,如果是對象則取 get 方法
const getter = typeof userDef === 'function' ? userDef : userDef.get
// getter 不存在時,輸出警告
...
if (!isSSR) {
// 爲 computed 的每個屬性創建 Watcher
// Watcher 是引用變量,vm._computedWatchers 也會被修改
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 此時 key 還沒掛載到 vm
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
// key 在 data 或者 props 存在,輸出警告
}
}
}
initComputed
會給 computed 的每個屬性創建 Watcher(服務端渲染不會創建 Watcher), 然後調用 defineComputed
。先看看 new Watcher
的構造函數做了什麼
// vue/sct/core/observe/watcher.js
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
// 渲染 Watcher
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.dirty = this.lazy // for lazy watchers
// 還有其他屬性的賦值
...
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 解析表達式,得到 getter 函數
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
// getter 爲空時,輸出警告
...
}
}
// lazy 爲 true 時,將 value 賦值爲 undefined,否則調用 get 函數計算 value
this.value = this.lazy
? undefined
: this.get()
}
看看 defineComputed
傳了哪些參數給這個構造函數。
const computedWatcherOptions = { lazy: true }
...
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
...
可以從上面看到 computed 屬性創建 Watcher
時,lazy
爲 true
,也就是在 computed 中聲明瞭屬性也不使用,那麼將不會計算該屬性的結果,value 爲 undefined。
順便看下 Watcher 的 get
方法
// vue/sct/core/observe/watcher.js
get () {
// 將該 Watcher push 到 Dep 的 targetStack 中,開啓依賴收集的模式
pushTarget(this)
let value
const vm = this.vm
try {
// 執行 computed 中的 get 函數
// 如果函數內使用了 data 中的屬性,那麼就會觸發 defineProperty 中 get
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 完成依賴收集
popTarget()
this.cleanupDeps()
}
return value
}
這裏就可以看到在調用 get 函數時,會將當前的 Watcher 指定爲 Dep.target,然後開始執行 computed 屬性的 get 函數,如果 computed 屬性的 get 函數內使用了 data 中的屬性,那麼就會觸發 defineProperty 中的 getter。這就驗證了開頭說的第二點:只有在響應式依賴發生改變時纔會重新計算結果。
// vue/src/core/observer/index.js
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 這個時候 target 爲 computed 屬性的 Watcher,然後將 data 屬性的 dep 收集到 computed 屬性的 Watcher 中
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: {
...
// data 的屬性發生變化,通知訂閱者進行更新
dep.notify()
}
})
從這裏可以看出 Vue 設計的非常巧妙,通過執行 computed 屬性的 get 函數,就可以完成所有依賴的收集,當這些依賴發生變化時,又會通知 computed 屬性的 Watcher 進行更新。
接着看回 defineComputed
// vue/src/core/instance/state.js
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// 客戶端渲染時,shouldCache 爲 true,也就是對計算結果進行緩存。
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) {
// 開發環境 computed 屬性的 set 函數爲空函數時,替換爲輸出警告的函數
...
}
// 將 computed 的屬性掛載到 vm 上,這樣就可以用 this.key 調用 computed 的屬性
Object.defineProperty(target, key, sharedPropertyDefinition)
}
從這裏可以看到,當對計算結果需要緩存,則會調用 createComputedGetter
,如果計算結果不需要緩存,則會調用 createGetterInvoker
。
官方彩蛋
從這裏還可以看到一個可以在開發時的小技巧,當 computed 的屬性爲對象時,還可以自定義是否需要緩存。
官方文檔好像沒提到這一點,可能是覺得不緩存就和 methods
一樣,就沒有提到,這可能就是彩蛋吧。
computed: {
noCacheDemo: {
get () { ... },
set () { ... },
cache: false
}
}
回到正題,看看 createComputedGetter
做了什麼。
// vue/src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
// watcher 爲 initComputed 中創建的 watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// watcher 初始化時,dirty 的值與 lazy 相同,都爲 true
// 那麼第一次獲取 computed 屬性的值將會執行 watcher.evaluate()
// evaluate 中會將 dirty 置爲 false
if (watcher.dirty) {
watcher.evaluate()
}
// 如果處於收集依賴的模式,調用 watcher 的 depend 進行依賴收集
if (Dep.target) {
watcher.depend()
}
// 返回 watcher.value,而不是執行 computed 屬性的 get 函數計算結果
return watcher.value
}
}
}
再看下 watcher 的 evaluate 函數
// vue/sct/core/observe/watcher.js
evaluate () {
this.value = this.get()
this.dirty = false
}
這裏可以看到,如果 computed 的計算結果需要緩存時,在第一次使用 computed 屬性時會執行 watcher 的 get 函數,在執行 computed 屬性的函數的過程中完成依賴的收集,並將計算結果賦值給 watcher的 value 屬性。
之後再調用 computed 的屬性則會取 watcher.value 的值,而不用執行 computed 屬性的 get 函數,就這樣做到了緩存的效果。也就驗證了開頭提到的第一點:computed
的計算結果會進行緩存。
最後再看看不使用緩存時的做法,createGetterInvoker
函數
// vue/sct/core/instance/state.js
function createGetterInvoker(fn) {
return function computedGetter () {
return fn.call(this, this)
}
}
其實做法非常簡單,就是每次調用就執行 computed 屬性的 get 函數。
總結
總結一下 computed 的實現過程,主要有以下幾個方面:
1、給 computed 的每個屬性創建 Watcher
2、第一個使用 computed 的屬性時,將會執行該屬性的 get 函數,並完成依賴收集,完後將結果保存在對應 Watcher 的 value 中,對計算結果進行緩存。
3、當依賴發生變化時,Dep 會發布通知,讓訂閱的 Watcher 進行更新的操作。
最後感謝各位小夥伴看到這裏,Vue computed 的實現過程都過了一遍,希望能夠對各位小夥伴有所幫助。
如果有講的不對的地方,可以評論指出哦。如果還有不瞭解的地方,歡迎關注我的公衆號給我留言哦。
如果你喜歡我的文章,希望可以關注我的公衆號【前端develop】