浅谈Vue中计算属性computed的实现原理
虽然目前的技术栈已由Vue转到了React,但从之前使用Vue开发的多个项目实际经历来看还是非常愉悦的,Vue文档清晰规范,api设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用Vue比React开发效率更高,之前也有断断续续研读过Vue的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对Vue的理解,那么今天要写的便是Vue中最常用到的API之一computed
的实现原理。
基本介绍
话不多说,一个最基本的例子如下:
<div id="app">
<p>{{fullName}}</p>
</div>
new Vue({
data: {
firstName: 'Xiao',
lastName: 'Ming'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
Vue中我们不需要在template里面直接计算{{this.firstName + ' ' + this.lastName}}
,因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而computed
的设计初衷也正是用于解决此类问题。
对比侦听器watch
当然很多时候我们使用computed
时往往会与Vue中另一个API也就是侦听器watch
相比较,因为在某些方面它们是一致的,都是以Vue的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
从vue官方文档对watch
的解释我们可以了解到,使用 watch
选项允许我们执行异步操作 (访问一个API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。
下面还另外总结了几点关于computed
和watch
的差异:
-
computed
是计算一个新的属性,并将该属性挂载到vm(Vue实例)上,而watch
是监听已经存在且已挂载到vm
上的数据,所以用watch
同样可以监听computed
计算属性的变化(其它还有data
、props
) -
computed
本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问computed
属性,才会计算新的值,而watch
则是当数据发生变化便会调用执行函数 - 从使用场景上说,
computed
适用一个数据被多个数据影响,而watch
适用一个数据影响多个数据;
以上我们了解了computed
和watch
之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。
原理分析
言归正传,回到文章的主题computed
身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索Vue源码中关于它的实现原理吧。
在分析computed
源码之前我们先得对Vue的响应式系统有一个基本的了解,Vue称其为非侵入性的响应式系统,数据模型仅仅是普通的JavaScript对象,而当你修改它们时,视图便会进行自动更新。
当你把一个普通的 JavaScript 对象传给 Vue 实例的data
选项时,Vue 将遍历此对象所有的属性,并使用Object.defineProperty
把这些属性全部转为getter/setter
,这些getter/setter
对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的watcher
实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter
被调用时,会通知watcher
重新计算,从而致使它关联的组件得以更新。
Vue响应系统,其核心有三点:observe
、watcher
、dep
:
-
observe
:遍历data
中的属性,使用 Object.defineProperty 的get/set
方法对其进行数据劫持 -
dep
:每个属性拥有自己的消息订阅器dep
,用于存放所有订阅了该属性的观察者对象 -
watcher
:观察者(对象),通过dep
实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应
对响应式系统有一个初步了解后,我们再来分析计算属性。
首先我们找到计算属性的初始化是在src/core/instance/state.js
文件中的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初始化
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
调用了initComputed
函数(其前后也分别初始化了initData
和initWatch
)并传入两个参数vm
实例和opt.computed
开发者定义的computed
选项,转到initComputed
函数:
const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
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]
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.
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)) {
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)
}
}
}
}
从这段代码开始我们观察这几部分:
-
获取计算属性的定义
userDef
和getter
求值函数const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get
定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加
set
和get
方法的对象形式,所以这里首先获取计算属性的定义userDef
,再根据userDef
的类型获取相应的getter
求值函数。 -
计算属性的观察者
watcher
和消息订阅器dep
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions )
这里的
watchers
也就是vm._computedWatchers
对象的引用,存放了每个计算属性的观察者watcher
实例(注:后文中提到的“计算属性的观察者”、“订阅者”和watcher
均指代同一个意思但注意和Watcher
构造函数区分),Watcher
构造函数在实例化时传入了4个参数:vm
实例、getter
求值函数、noop
空函数、computedWatcherOptions
常量对象(在这里提供给Watcher
一个标识{computed:true}
项,表明这是一个计算属性而不是非计算属性的观察者,我们来到Watcher
构造函数的定义:class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { if (options) { this.computed = !!options.computed } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { } finally { popTarget() } return value } update () { if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value } depend () { if (this.dep && Dep.target) { this.dep.depend() } } }
为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
观察Watcher
的constructor
,结合刚才讲到的new Watcher
传入的第四个参数{computed:true}
知道,对于计算属性而言watcher
会执行if
条件成立的代码this.dep = new Dep(),
而dep
也就是创建了该属性的消息订阅器。export default class Dep { static target: ?Watcher; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null
dep
同样精简了部分代码,我们观察Watcher
和dep
的关系,用一句话总结watcher
中实例化了dep
并向dep.subs
中添加了订阅者,dep
通过notify
遍历了dep.subs
通知每个watcher
更新。 -
defineComputed
定义计算属性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) } }
因为
computed
属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性,defineComputed
传入了三个参数:vm
实例、计算属性的key
以及userDef
计算属性的定义(对象或函数)。
然后继续找到defineComputed
定义处: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) }
在这段代码的最后调用了原生
Object.defineProperty
方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition
,初始化为:const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
随后根据
Object.defineProperty
前面的代码可以看到sharedPropertyDefinition
的get/set
方法在经过userDef
和shouldCache
等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition
的get
函数也就是createComputedGetter(key)
的结果,我们找到createComputedGetter
函数调用结果并最终改写sharedPropertyDefinition
大致呈现如下:sharedPropertyDefinition = { enumerable: true, configurable: true, get: function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } }, set: userDef.set || noop }
当计算属性被调用时便会执行
get
访问函数,从而关联上观察者对象watcher
。
分析完以上步骤,我们再来梳理下整个流程:
- 当组件初始化的时候,
computed
和data
会分别建立各自的响应系统,Observer
遍历data
中每个属性设置get/set
数据拦截 -
初始化
computed
会调用initComputed
函数- 注册一个
watcher
实例,并在内实例化一个Dep
消息订阅器用作后续收集依赖(比如渲染函数的watcher
或者其他观察该计算属性变化的watcher
) - 调用计算属性时会触发其
Object.defineProperty
的get
访问器函数 - 调用
watcher.depend()
方法向自身的消息订阅器dep
的subs
中添加其他属性的watcher
- 调用
watcher
的evaluate
方法(进而调用watcher
的get
方法)让自身成为其他watcher
的消息订阅器的订阅者,首先将watcher
赋给Dep.target
,然后执行getter
求值函数,当访问求值函数里面的属性(比如来自data
、props
或其他computed
)时,会同样触发它们的get
访问器函数从而将该计算属性的watcher
添加到求值函数中属性的watcher
的消息订阅器dep
中,当这些操作完成,最后关闭Dep.target
赋为null
并返回求值函数结果。
- 注册一个
- 当某个属性发生变化,触发
set
拦截函数,然后调用自身消息订阅器dep
的notify
方法,遍历当前dep
中保存着所有订阅者wathcer
的subs
数组,并逐个调用watcher
的update
方法,完成响应更新。