blog
blog copied to clipboard
Vue源码探秘(计算属性computed)
引言
计算属性(computed
)是 Vue
中比较强大又十分重要的功能 ,它具有 分离逻辑
、缓存值
、双向绑定
等作用或功能。
本节我就带大家一起来看下计算属性 computed
部分的源码。
computed
官方文档对于计算属性的使用时机有一个很清晰的说明:如果模板内的逻辑过于复杂,那就应该考虑使用计算属性来代替。
这里引用官方的示例来说明计算属性的用法:
// App.vue
<template>
<div id="app">
{{ reverseName }}
</div>
</template>
<script>
export default {
data() {
return {
name: 'forest'
}
},
computed: {
reverseName: function() {
return this.name.split('').reverse().join();
}
}
}
</script>
大致了解了computed
的用法后,和之前一样,为了更清晰的了解源码的执行过程,我下面将会结合一个例子来分析源码。
从一个简单示例开始
看下这个例子:
// App.vue
<template>
<div id="app">
<p>{{ name }}</p>
<button @click="handleToggleShow">toggleShow</button>
<button @click="changeName">change</button>
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'jack',
lastName: 'cool',
isShow: false
}
},
computed: {
name() {
return this.isShow ? `${this.firstName}, ${this.lastName}` : 'please click the toggleShow button';
}
},
methods: {
handleToggleShow() {
this.isShow = true;
},
changeName() {
this.lastName = 'rose';
}
}
}
</script>
在这个例子中,计算属性 name
依赖了三个响应式数据 firstName
、lastName
和 isShow
。
需要注意计算属性依赖的数据必须是响应式的,否则依赖的数据发生变化并不会触发计算属性的变化。
接下来会先介绍这个例子的初始化到渲染的整个过程,然后再介绍点击 toggleShow
、change
按钮时对应源码的执行过程。
计算属性的渲染过程
在前面分析组件化的时候,我们知道组件实例化前要先通过 Vue.extend
函数来创建组件构造函数:
// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
// ...
if (Sub.options.computed) {
initComputed(Sub);
}
// ...
};
Sub
也就是我们例子中 App
组件的构造函数,这里 Vue.extend
函数判断如果组件 options
中有 computed
,则执行 initComputed
函数,并且将 Sub
传进去:
// 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]);
}
}
initComputed
函数拿到 computed
对象然后遍历每一个计算属性调用 defineComputed
方法,将组件原型,计算属性和对应的值传入。来看 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);
}
首先定义了 shouldCache
表示是否需要缓存值。接着对 userDef
是函数或者对象分别处理。这里有一个 sharedPropertyDefinition
,我们来看它的定义:
// src/core/instance/state.js
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};
sharedPropertyDefinition
其实就是一个属性描述符,这个在之前的章节,我们也有分析这块。
回到 defineComputed
函数。如果 userDef
是函数的话,就会定义 getter
为调用 createComputedGetter(key)
的返回值。
因为
shouldCache
是true
而 userDef
是对象的话,非服务端渲染并且没有指定 cache
为 false
的话,getter
也是调用 createComputedGetter(key)
的返回值,setter
则为 userDef.set
或者为空。
所以 defineComputed
函数的作用就是定义 getter
和 setter
,并且在最后调用 Object.defineProperty
给计算属性添加 getter/setter
,当我们访问计算属性时就会触发这个 getter
。
对于计算属性的
setter
来说,实际上是很少用到的,除非我们在使用computed
的时候指定了set
函数。
无论是userDef
是函数还是对象,最终都会调用createComputedGetter
函数,我们来看createComputedGetter
的定义:
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;
}
};
}
可以看到,createComputedGetter
返回了一个 computedGetter
函数,也就是说计算属性的 getter
就是这个 computedGetter
函数。
我们知道访问计算属性时才会触发这个
getter
,对应就是computedGetter
函数被执行。所以这块逻辑在被调用时再来分析。
在创建了组件构造函数后,就会进行组件实例化。经过前面的学习,我们知道在组件实例化时会调用各种 init
函数做初始化工作,在执行 initState
的时候:
// src/core/instance/state.js
export function initState(vm: Component) {
// ...
const opts = vm.$options;
if (opts.computed) initComputed(vm, opts.computed);
// ...
}
这里opts.computed
是存在的,所以会执行initComputed
函数:
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) {
// ...
}
}
这里 initComputed
函数的 computed
参数就是我们组件中的 computed
对象。函数首先定义了两个常量,watchers
指向 vm._computedWatchers
,是一个空对象,而 isSSR
表示服务端渲染,这里为 false
。
接着就是遍历 computed
对象,我们分段来分析:
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);
}
// ...
}
这个 getter
是我们自己编写的 computed
中的函数,也就是例子中的 name()
函数。
从源码可以看到,computed
有两种写法,一种是直接写一个函数,一种是一个对象,同时有一个 get
属性作为 getter
。如果拿不到 getter
的话就抛出警告。继续往下看:
for (const key in computed) {
// ...
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// ...
}
这里判断如果不是服务端渲染就会给计算属性创建一个 computed Watcher
实例赋值给watchers[key]
(对应就是vm._computedWatchers[key]
)。
这也说明了计算属性是通过
Watcher
来实现。
我们来看 computed Watcher
的实例化过程是怎么样的,回顾 Watcher
的定义:
// src/core/observer/watcher.js
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
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.active = true
this.dirty = this.lazy // for lazy 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
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
前面创建 computed Watcher
传入的四个参数分别是:vm
、getter
、noop
和computedWatcherOptions
({ lazy: true }
)。
因为这里是computed Watcher
,所以 if (isRenderWatcher)
不会执行,另外 this.dirty = this.lazy = true
。接着会将 getter
也就是我们编写的 name
函数赋值给 this.getter
。
最后 this.value = undefined
,也就是说不会像渲染 Watcher
一样实例化时就执行get
函数调用 getter
求值。
回到 initComputed
函数的 for
循环,还有最后一段代码:
// src/core/instance/state.js
for (const key in computed) {
// ...
// 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);
}
}
}
最后一段逻辑实际上是不会执行的。if
逻辑判断的是当前计算属性有没有定义在我们的 App
组件实例上,而前面在创建 App
组件构造函数的时候,已经调用 defineComputed
函数把计算属性定义在组件原型上了,所以每个组件实例都能访问到计算属性。
而 else if
逻辑是判断 key
有没有定义在 data
和 props
上,因为计算属性是不能和 data
、props
重名的,如果重名会抛出警告。这样整个 initComputed
函数的逻辑我们就分析完了。
接着 App
组件来到渲染阶段,我们知道渲染阶段会执行 render
函数创建 VNode
,而这个过程会访问到计算属性,这样就触发了计算属性的 getter
也就是前面提到的 computedGetter
函数:
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;
}
};
computedGetter
函数首先通过 this._computedWatchers[key]
拿到前面实例化组件时创建的 computed Watcher
并赋值给 watcher
。接着有两个 if
判断,首先调用 evaluate
函数:
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
首先调用 this.get()
将它的返回值赋值给 this.value
,回顾 get
函数:
// 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) {
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
函数第一步是调用 pushTarget
将 computed Watcher
传入:
// src/core/observer/dep.js
export function pushTarget(target: ?Watcher) {
targetStack.push(target);
Dep.target = target;
}
可以看到 computed Watcher
被 push 到 targetStack
同时将 Dep.target
置为 computed Watcher
。而 Dep.target
原来的值是渲染 Watcher
,因为正处于渲染阶段。回到 get
函数,接着就调用了 this.getter
。
中间具体过程之前的章节已经介绍了,这里不在赘述 。最后 get
函数会执行 popTarget()
将 Dep.target
重新恢复为渲染 Watcher
,然后将 value
返回出去。
回到 evaluate
函数:
evaluate () {
this.value = this.get()
this.dirty = false
}
执行完get
函数,将dirty
置为false
。
回到computedGetter
函数,接着往下进入另一个if
判断,执行了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()
}
}
这里的逻辑就是让 Dep.target
也就是渲染 Watcher
订阅了 this.dep
也就是前面实例化 computed Watcher
时候创建的 dep
实例,渲染 Watcher
就被保存到 this.dep
的 subs
中。
在执行完 evaluate
和 depend
函数后,computedGetter
函数最后将 evaluate
的返回值返回出去,也就是计算属性最终计算出来的值,这样页面就渲染出来了。
计算属性的修改过程
在例子中,当我们点击 toggleShow
按钮时,会修改 data
中的 isShow
,就会触发 isShow
的 setter
:
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();
// ...
const setter = property && property.set;
// ...
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
},
set: function reactiveSetter(newVal) {
// ...
dep.notify();
},
});
}
setter
的执行流程在 Vue源码探秘(派发更新)
那一节也有介绍过, 这里重点看 setter
的最后一步,也就是执行 dep.notify()
派发更新:
// src/core/observer/dep.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()
}
}
notify
函数的主要逻辑就是遍历 subs
中的 Watcher
执行 update
方法。而前面也分析了 computed Watcher
订阅了计算属性依赖的 data
的变化,所以这里的 subs
存放的就是 computed Watcher
,执行了 computed 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)
}
}
渲染 Watcher
的 update
方法走的是 else
逻辑执行 queueWatcher
方法。queueWatcher
函数的具体执行流程在Vue源码探秘(派发更新)
那一节已经分析过了,这里就不再分析。最终页面会重新渲染。
总结
这一节我们学习了计算属性的初始化渲染
过程和依赖改变时重新渲染
的过程,了解到了计算属性本质上就是一个 computed watcher
。
下一节,我将带大家一起探秘侦听器(watch
)部分的源码。