computed的使用
在理解源码之前,我们可以先回忆一下 computed 的使用方法
js
<template>
<div>
<h2>{{ reverseMsg }}</h2>
<h2>{{ fullName }}</h2>
</div>
</template>
<script>
export default {
data () {
return {
msg: 'hello, vue',
firstName: 'Jack',
lastName: 'Cheng'
}
},
computed: {
reverseMsg () {
return this.msg.split('').reverse().join('')
},
fullName: {
get () {
return this.firstName + ' ' + this.lastName
},
set (newVal) {
[this.firstName, this.lastName] = newVal.split(' ')
}
}
}
}
</script>
computed 是一个对象,属性值可以使函数,也可以是对象,如果是对象的话,需要有一个get方法,获取计算值。 计算属性默认是只读的,一般情况下,我们用不到 set 方法,这里就不过多解释了。
computed源码分析
接下来,我们就根据上面的例子,来探索一下源码实现的过程。
initState 函数
入口是从 initState
函数开始,大家可以在源码中直接搜索这个函数即可。
js
export function initState(vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// Composition API
initSetup(vm)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
const ob = observe((vm._data = {}))
ob && ob.vmCount++
}
// 📢注意:在这里我们去对computed进行初始化,该函数内其它代码可以不用看。
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initComputed 函数
接下来继续看一下 initComputed
这个函数做了些什么。
js
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
// $flow-disable-line
// 📢:这个 watchers 是用来收集每一个 computed 的 watcher 实例
// 📢:这里涉及到了一个浅拷贝,使 vm._computedWatchers 的值和 watchers 的值始终保持一致
const watchers = (vm._computedWatchers = Object.create(null))
// computed properties are just getters during SSR
const isSSR = isServerRendering() // 是否为SSR服务端渲染
// 📢:这里会遍历computed对象上的所有属性,获取属性值 userDef
// 📢:通过上面举得例子,这里的key会依次为:reverseMsg、fullName
for (const key in computed) {
const userDef = computed[key]
// 📢:isFunction 只是判断属性值是否为函数,这里就是为什么我上面的例子会用两种方法获取计算属性值
const getter = isFunction(userDef) ? userDef : userDef.get
// 不用看
if (__DEV__ && getter == null) {
warn(`Getter is missing for computed property "${key}".`, vm)
}
// 如果不是服务端渲染会创建一个Watcher的实例
if (!isSSR) {
// create internal watcher for the computed property.
// 📢注意:这里就是我们要给每一个计算属性创建一个 watcher 实例,
// 后面我们可以具体看一下这个watcher 内部具体做了什么事情
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions // 📢:值为:{ lazy: true }
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
// 📢注意:每一个计算属性都调用了 defineComputed 方法,这里主要就是实现了计算属性的初始化以及判断是否走缓存,后面会具体讲到内部的实现
defineComputed(vm, key, userDef)
} else if (__DEV__) {
// 不用看
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)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(
`The computed property "${key}" is already defined as a method.`,
vm
)
}
}
}
}
总结一下,initComputed
这个函数其实主要就是给每一依赖属性创建一个 watcher 实例,并维护在 vm._computedWatchers
上。同时,每一个依赖属性都调用了一次 defineComputed
方法
Watcher构造函数
由于 Watcher 构造函数上面的代码比较多,不利于分析,这里只把相关代码摘取出来,其余不用看的代码会先删掉,这样可以更直观容易理解。
js
export default class Watcher {
constructor( // 📢:这里定义了参数及类型
vm: Component | null,
expOrFn: string | (() => any),
cb: Function, // 📢:值为:function () {}
options?: WatcherOptions | null, // 📢:值为:{ lazy: true }
isRenderWatcher?: boolean
) {
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = 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.active = true
this.post = false
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn // 📢:给每一个依赖属性添加getter方法
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
// 📢:这里有个小Tip,初始时,lazy为true,所以,不会调用get,实现了懒执行
// 可以理解,创建实例时,如果没有读取 computed 中的计算属性,则不会进行计算
this.value = this.lazy ? undefined : this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
// 📢:这个方法就是依赖收集的核心
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
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
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
// 📢:更改this.value的值,再将this.dirty设置为false
evaluate() {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
在 Watcher
构造函数上,有两个方法:evaluate
和 depend
,这两个方法在 defineComputed
这里会用到。
为了加深理解,我们拿我们之前定义的 computed
中的 reverseMsg
依赖属性举例,通过 Watcher 实例化后,我们得到的是这样的一个对象:
js
{
reverseMsg: {
values: undefined, // 计算所得值
deep: false,
lazy: true,
dirty: true // dirty表示是否从缓存中取值
getter() {
return this.msg.split('').reverse().join('')
},
// 这里主要就是调用this.getter方法,并且将得到的值返回
// 也就是执行 this.msg.split('').reverse().join('')
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
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
},
update() {
// 📢:这里的lazy为true,后面的逻辑不用看
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
},
evaluate() {
this.value = this.get()
this.dirty = false
},
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
}
另外这个Wacher构造函数,除了在计算属性这里会创建 计算watcher实例
(computed watcher),在页面渲染的时候还会创建 渲染watcher实例
(render watcher),具体可以在源码里查一下 mountComponent
这个方法,在这个方法里也进行了new Watcher
。后面讲到依赖收集的时候会涉及到。在监听watch属性上面也创建了用户监听watcher
。
所以总共有三个地方会创建这个watcher实例。
defineComputed 方法
在 initComputed
这个方法中,每一个依赖属性都调用了 defineComputed
方法,这个方法的核心是调用了 createComputedGetter
,后面我们再继续看一下具体做了什么。
js
export function defineComputed(
target: any, // 📢:传值为vm
key: string,
userDef: Record<string, any> | (() => any)
) {
const shouldCache = !isServerRendering()
if (isFunction(userDef)) {
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 (__DEV__ && sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 📢:这里实现的目的是将计算属性绑定在vm上
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter 方法
js
function createComputedGetter(key) {
return function computedGetter() {
// 📢:_computedWatchers这个是不是眼熟,这里就是获取我们通过 Watcher 去实例化的对象,
// 可以回去再看一下之前举得例子, reverseMsg 这个实例化后的对象
// this._computedWatchers[key] => reverseMsg
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 📢:这里的 dirty 用来标记所依赖的值是否发生了变化,
// 如果为 true,则不走缓存,会调用 evaluate 方法,重新计算获取计算属性值
// 如果为 false,则直接取 watcher.value (缓存)中的值
if (watcher.dirty) {
watcher.evaluate()
}
// 📢:Dep.target 上的值具体可以看下面 pushTarget 、popTarget 方法
if (Dep.target) {
if (__DEV__ && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
target: this,
type: TrackOpTypes.GET,
key
})
}
watcher.depend()
}
// 📢:最后将这个计算属性值返回,结束!
return watcher.value
}
}
}
pushTarget 、popTarget方法
在上面的 Watcher
构造函数中,只要调用了get
方法,就会执行这个pushTarget
方法,并且会将当前的watcher
实例作为参数target。
之前有提到过,有渲染watcher,计算watcher以及用户监听watcher,下面就分析一下这个过程:
⭐️ 在渲染的时候,会调用watcher上的get方法,也就会调用 pushTarget 方法,此时
Dep.target = 渲染watcher
,targetStack = [ 渲染watcher ]
;⭐️ 之后读取计算属性时,创建计算watcher,此时
Dep.target = 计算watcher
,targetStack = [ 渲染watcher, 计算watcher ]
⭐️ 在获取计算属性时,会读取它的依赖值,基于Vue的响应式原理,就会执行依赖的getter函数,
Dep.target
上的addDep
方法,则会将该依赖值收集到计算watcher
上的deps
中,同时又将计算watcher
收集到该依赖值的deps
中。⭐️ 读取完依赖值后,会调用
popTarget
方法, 而此时Dep.target = 渲染watcher
,targetStack = [ 渲染watcher ]
⭐️ 再次触发
watcher.depend
,依赖值又收集到了渲染watcher
js
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
总结
Vue 的 computed 是基于 Vue 的响应式原理来实现的。
通过创建 watcher 实例以及结合发布订阅模式实现了依赖收集。 当依赖的数据变化了,就会触发setter
,同时将dirty
变为true
,计算watcher
就会重新调用依赖数据的getter
,获取最新的值,并将计算好的值缓存到watcher.value
中。