本文已参与「新人创作礼」活动,一起开启掘金创作之路。
关于Vue侦听属性的原理我们在另一篇文章中已经进行了解析,想了解的可以点击这里。下面我们从源码角度来分析Vue computed的原理。
Computed
computed属性是在initState阶段初始化的,我们直接看源码:
计算属性初始化
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 定义了computed,执行initComputed
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initComputed
const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
// 新增_computedWatchers属性为空对象
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 遍历computed
for (const key in computed) {
// 取值
const userDef = computed[key]
// computed可以设置为函数类型,也可以设置为对象设置get属性(如果是对象必须定义get属性)
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) {
// create internal watcher for the computed property.
// 非SSR环境,给每个计算属性实例化一个computed Watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// 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)) {
// 在vm上未定义,执行defineComputed函数,否则在开发环境报出警告
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)
}
}
}
}
我们继续看difineComputed函数:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
// 定义的属性为函数类型
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
// 定义的属性为对象类型
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.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属性在设置了时候才会添加,否则就是个空函数,get属性定义的函数是createComputedGetter:
function createComputedGetter (key) {
return function computedGetter () {
// 拿到属性对应的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
// 如果watcher存在
if (watcher) {
// 收集依赖
watcher.depend()
return watcher.evaluate()
}
}
}
到这里计算属性的初始化过程就完成了,另外我们再看下计算属性初始化过程中实例化的Watcher是怎么样的:
export default class Watcher {
......
constructor (
vm: Component,
expOrFn: string | Function, // computed中定义的函数或是对象中的get函数
cb: Function, // noop空函数
options?: ?Object, // { computed: true }
isRenderWatcher?: boolean // false
) {
this.vm = vm
......
vm._watchers.push(this)
// options
if (options) {
......
this.computed = !!options.computed // true
......
}
......
this.cb = cb // noop
......
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
// 传入的函数赋值给getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
// 实例化Dep,value定义为undefined
if (this.computed) {
this.value = undefined
this.dep = new Dep()
}
......
}
}
可以看到,computed Watcher实例化的时候,并没有求值,而且实例化了一个Dep。
小结
计算属性初始化的过程中,实例化了计算属性Watcher, 并给计算属性定义了一个getter;实例化计算属性Watcher的时候,没有求值,同时实例化了一个Dep,用来收集订阅者。
计算属性被访问的时候发生了什么:
下面我们列举一个场景,看看computed是如何运行的:
<template>
<div>{{ message }}</div>
</template>
export default {
data() {
return {
firstName: 'jue'
}
},
computed: {
message() {
return this.firstName + 'jin'
}
}
}
上面这个组件渲染的时候,会触发render函数,从而访问到计算属性message,这样就触发了计算属性的getter,
// 拿到属性对应的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
// 如果watcher存在
if (watcher) {
// 收集依赖
watcher.depend()
return watcher.evaluate()
}
计算属性的依赖收集
执行watcher.depend():
/**
* Depend on this watcher. Only for computed property watchers.
* 为计算属性watcher而生
*/
depend () {
// this指向当前的计算属性Watcher,Dep.target是当前的render Watcher
if (this.dep && Dep.target) {
// 计算属性Watcher收集依赖
this.dep.depend()
}
}
收集订阅者:
depend () {
// Dep.target是当前的render Watcher
if (Dep.target) {
Dep.target.addDep(this)
}
}
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)) {
// 当前的计算属性Watcher收集了订阅者render Watcher,计算属性有变化会通知render Watcher做更新
dep.addSub(this)
}
}
}
在当前的情景下计算属性Watcher收集了订阅者render Watcher,当计算属性有变化的时候会通知render Watcher做更新。
计算属性的计算
watcher.evaluate()
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
* 为计算属性Watcher准备
*/
evaluate () {
// this.dirty = this.computed = true
// 第一次触发计算属性getter的时候,evaluate进入该逻辑,作为订阅者订阅通知
// 当页面渲染中第二次取到计算属性的值,不会重新进行计算,而是直接返回之前计算的结果
if (this.dirty) {
// 执行get方法 存放在this.value上
this.value = this.get()
this.dirty = false
}
// 返回计算结果
return this.value
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// this指向计算属性Watcher,将当前的Dep.target赋值为computed Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 执行计算属性中定义的函数,当前情景下是function() { return this.firstName + 'jin' },
// 这时候会触发响应式数据firstName的getter
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
}
上面的代码可以看出,这些逻辑会计算出当前计算属性的值,除此之外,当前的Dep.target的值会赋值为计算属性watcher,当计算属性的函数逻辑中涉及到其他的响应式数据的时候(例如本场景中,涉及到了响应式数据firstName),就需要获取该响应式数据(firstName)的值,也就会触发该响应式数据(firstName)的getter方法,从而该响应式数据(firstName)会收集计算属性Watcher。
所以后续当响应式数据(firstName)发生变化的时候,会通知computed Watcher做更新,computed Watcher更新的时候,会通知render Watcher做更新,从而让页面重新渲染。
计算属性缓存,计算结果不变,不会刷新页面
在这里计算属性还有一个优化点,我们继续以上述场景为例子,当我们改变firstName的值的时候,我们继续去看派发更新的源码:
// dep.notify
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 通知Computed watcher做更新
subs[i].update()
}
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
// 计算属性watcher 进入该逻辑
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// 计算属性监视器有两种模式:惰性模式和激活模式。
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
// 默认情况下,它初始化为惰性模式,只有在至少一个订阅者依赖于它时才被激活,
// 这通常是另一个计算属性或组件的渲染函数。
if (this.dep.subs.length === 0) {
// 惰性模式,没有订阅者的时候进入该逻辑
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
// 在惰性模式下,除非必要,否则我们不想计算,所以我们简单地把观察者标记为dirty。
// 实际的计算是在访问computed属性时在this.evaluate()中实时执行的。
// dirty在初始化computed Watcher的时候会赋值为true,另一个赋值为true的地方就是这儿
this.dirty = true
} else {
// 有订阅者的时候进入该逻辑
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
// 在激活模式下,我们希望主动执行计算,但只在值确实发生变化时通知订阅者。
this.getAndInvoke(() => {
this.dep.notify()
})
}
}
......
}
继续看getAndInvoke:
{
getAndInvoke (cb: Function) {
// 执行get方法求值
const value = this.get()
if (
// 如果这个值与之前的值相等,则不会进入下面的逻辑,也就是计算属性计算的最终值结果不变的情况下,
// 不会让通知页面进行更新!
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
// 值发生变化了,则执行dep.notify,通知render Watcher更新
cb.call(this.vm, value, oldValue)
}
}
}
}
总结
- 计算属性初始化
- 计算属性初始化的时候实例化了一个Dep,用来收集依赖;
- 计算属性初始化的时候没有求值;
- 计算属性被访问
- 计算属性被访问的时候会收集订阅者(依赖);
- 计算属性被访问的时候会作为订阅者订阅通知;
- 计算属性初次被访问的时候会求值,求值后进入惰性模式;
- 计算属性有缓存
- 当计算属性依赖的响应式数据变化,而计算出的结果不变的情况下,不会通知订阅者更新,也就不会刷新页面