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】
前端develop
网友评论