在平时的项目开发中,经常都会使用到计算属性 computed 和侦听属性 watch,那么在使用 Vue 提供的 API 的同时,是否有考虑过它们是如何实现的呢?理解它们的原理,能让我们在不同的场景采用不同的属性,使用自如。本文先来分析计算属性 computed 是如何实现的?
computed 初始化
在 Vue 框架的实现过程中,对计算属性 computed 初始化有两种方式:
- 初始化 Vue 实例对其初始化
- 在定义子组件构造函数过程中对其初始化
那么下文将对这两种方式进行分析,先来看第一种初始化方式。
在初始化 Vue 实例时,函数 initState 对计算属性 computed 做了初始化操作,位于 src/core/instance/init.js,具体实现如下:
export function initState (vm: Component) {
...
if (opts.computed) initComputed(vm, opts.computed)
...
}
先判断传进来的 vm 实例是否有属性 computed,如果存在,则调用函数 initComputed,否则执行后续逻辑,具体实现如下:
// src/core/instance/state.js
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)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
函数接收两个参数:
vm:Vue 实例computed:计算属性对象
首先通过 Object.create(null) 创建空对象,赋值给 vm._computedWatchers 和 watchers。
接着,对 computed 进行遍历,通过 key 获取每个计算属性。而 compted 中每个属性可以是函数,也可以是对象;如果是对象的话,则必须包含 get。
获取到计算属性后,通过判断其数据类型获取 getter;如果 getter 为 null,则会在开发环境中抛出告警,此情况存在于 computed 属性是对象。
然后,为每个计算属性创建 computed watcher,即实例化 watcher,需要传入四个参数:
vm:Vue 实例expOrFn:数据类型可以是字符串,也可以是函数,在这里传入是函数,即getter或者空函数noop,作为回调被执行cb:回调函数,传入的是空函数noopoptions:可选项,传入的是一个对象{ lazy: true },表示其是一个computed watcher
在实例化 watcher 时,需要注意的是 getter 没有被立即执行,具体实现如下:
// src/core/observer/watcher.js
export default class Watcher {
lazy: boolean;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
// options
if (options) {
this.lazy = !!options.lazy
} else {
this.deep = this.user = this.lazy = this.sync = false
}
...
this.value = this.lazy
? undefined
: this.get()
}
}
最后判断 key 是否在 vm 实例上,如果不存在,则调用函数 defineComputed 将其转换为响应式属性,并添加到 vm 实例上。需要注意的是在创建子组件时,已经对其 computed 属性定义在组件原型上,此时无需再定义;而我们只需要定义那些在这里实例化的 computed 属性。
除此之外,对于已经定义的 computed 属性,还对其 key 进行校验,即 key 存在于 data 或者 props 或者 methods 时,则在开发环境中抛出告警。
接下来,我们来看下 defineComputed 具体是如何实现的?
// src/core/instance/state.js
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
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) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
函数接收三个参数:
targetVue 实例key:computed计算属性键名- ·
userDef:computed计算属性定义的回调
核心逻辑是利用 ES5 Object.defineProperty 为计算属性对应的 key 定义 getter 和 setter。
在定义 getter 时,根据 shouldCache 值来配置不同 getter,如果 shouldCache 为 true 时,getter 则为 createComputedGetter,否则为 createGetterInvoker。那么 getter 的触发时机是在执行 render 函数时被调用,这里先知道其触发时机,稍后再对其进行分析。
而对于其 setter,只是简单将其设置为空函数 noop。
第一种初始化方式已分析完,接着来分析第二种初始化方式。
在执行 render 函数,即将 Vue 实例渲染为 VNode,其中会调用 createElement 函数创建 VNode,那么在创建过程中,如果传入的是组件,则会调用 createComponent 函数,具体实现如下:
// src/core/vdom/create-component.js
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
...
}
核心代码在 Ctor = baseCtor.extend(Ctor),具体实现如下:
// src/core/global-api/extend.js
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
...
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
...
return Sub
}
从代码实现可看出,如果存在 computed,则会调用 initComputed,具体实现如下:
// src/core/global-api/extend.js
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
最终还是调用 defineComputed,上文已分析过。
createComputedGetter 触发时机
为了理解 computed watcher 触发过程,下面通过一个例子来分析,具体如下:
const vm = new Vue({
data: {
username: 'Test',
age: 18
},
computed: {
message () {
return `${this.username}-${this.age}`
}
}
})
当在执行 render 函数访问到属性 message 时,就会触发计算属性 message getter 函数,即 createComputedGetter 被调用,具体实现如下:
// src/core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
首先,通过初始化时缓存计算属性变量 _computedWatchers 获取到当前 computed watcher。此时 watcher.dirty 值为 true,调用 computed watcher 方法 evaluate ,具体实现如下:
// src/core/observer/watcher.js
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
evalutate 调用 get 求值,获取计算属性的值,再 computer watcher 属性 dirty 设置为 false。那么,我们来看下是如何求值的?
// src/core/observer/watcher.js
/**
* 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) {
...
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
调用 pushTarget 将当前 computed watcher 压入栈,并设置 Dep.target 为当前 computed watcher,如下:
// src/core/observer/dep.js
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
接着会执行 value = this.getter.call(vm, vm),即调用计算属性定义的 getter 函数,在我们这个例子就是执行
return `${this.username}-${this.age}`
由于 username 和 age 是响应式对象,从而会触发它们各自的 getter 函数。
根据之前的分析可知,当前 computed watcher 会订阅它们,即将它们各自持有的 dep 添加到当前 computed watcher ;作用是当 username 和 age 发生变化时,可以通知订阅它们的计算属性更新值。
求值完后,调用 popTarget 、cleanupDeps 做一些清理工作,通过 return value 将值返回;需要注意的是此时 Dep.tagert 是指向渲染 watcher。
分析完 evaluate 实现,回到 createComputedGetter 继续后面逻辑分析。
由于此时 Dep.target 指向渲染 watcher,因此,进入 if 逻辑,即调用 computed watcher 实例方法 depend,具体实现如下:
// src/core/observer/watcher.js
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
遍历数组 deps,调用 dep 实例方法 depend ,具体实现如下:
// src/core/observer/dep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
渲染 watcher 订阅 computed watcher,即 computed watcher dep 收集渲染 watcher 依赖,作用是当 computed watcher 发生变化时,通知渲染 watcher 作出响应的更新。
最后,将获取到的计算属性值返回。
计算属性更新机制
沿着上面所举的例子,进一步来分析计算属性是如何更新的。
当计算属性所依赖的属性发生变化时,由于所依赖的属性是响应式对象,那么,自然而然地就会触发其 setter 函数,关键代码如下:
// src/core/observer/index.js
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
...
dep.notify()
}
从上面的分析可知,计算属性对其所依赖的属性进行订阅,此时会触发其值做出更新;除此之外,渲染 watcher 也订阅响应式属性,那么当响应式属性发生变化时,自然而然地就会通知渲染 watcher 更新视图。
// src/core/observer/index.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
此时,调用各自 watcher 方法 update 进行更新操作,具体如下:
// src/core/observer/watcher.js
/**
* 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)
}
}
对于 computed wathcer,则将其属性 dirty 设置为 true,在下一个 tick 对其进行求值;而对于渲染 watcher ,则调用 queueWatcher 来执行更新操作(在《响应式原理三:派发更新》已分析过),在这过程中会执行 render 函数,由于需要获取计算属性的值,从而触发 createComputedGetter 函数被执行,实现对计算属性进行求值。
那么,关于计算属性 computed 相关知识点先分析到这里。