官网对计算属性的介绍在这里:传送门
计算属性是Vue
中很常用的一个配置项,我们先用一个简单的例子来讲解它的功能:
<div id="app">
{{fullName}}
</div>
<script>
const vm = new Vue({
data () {
return {
firstName: 'Foo',
lastName: 'Bar'
};
},
computed: {
fullName () {
return this.firstName + this.lastName;
}
}
});
</script>
在例子中,计算属性中定义的fullName
函数,会最终处理为vm.fullName
的getter
函数。所以vm.fullName = this.firstName + this.lastName = 'FooBar'
。
计算属性有以下特点:
- 计算属性可以简化模板中的表达式,用户可以书写更加简洁易读的
template
Vue
为计算属性提供了缓存功能,只有当它依赖的属性(例子中的this.firstName
和this.lastName
)发生变化时,才会重新执行属性对应的getter
函数,否则会将之前计算好的值返回。
正是由于computed
的缓存功能,使得用户在使用时会优先考虑它,而不是使用watch
、methods
属性。
在了解了计算属性的用法后,我们通过代码来一步步实现computed
,并让它完成上边的例子。
初始化computed
的逻辑会书写在scr/state.js
中:
function initState (vm) {
const options = vm.$options;
// some code ...
if (options.computed) {
initComputed(vm);
}
}
在initComputed
中,可以通过vm.$options.computed
拿到所有定义的计算属性。对于每个计算属性,需要对其做如下处理:
- 实例化计算属性对应的
Watcher
- 取到计算属性的
key
,通过Object.defineProperty
为vm
实例添加key
属性,并设置它的get/set
方法
function initComputed (vm) {
const { computed } = vm.$options;
// 将计算属性watcher存储到vm._computedWatchers属性中,之后方法直接通过实例vm来获取
const watchers = vm._computedWatchers = {};
for (const key in computed) {
if (computed.hasOwnProperty(key)) {
const userDef = computed[key];
// 计算属性key的值有可能是对象,在对象中会设置它的get set 方法
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// 为每一个计算属性创建一个watcher
watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true });
// 将计算属性的key添加到实例vm上
defineComputed(vm, key, userDef);
}
}
}
计算属性也可以传入set
方法,用于设置值时处理的逻辑,此时计算属性的value
是一个对象:
new Vue({
// ...
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
}
//...
)
在defineComputed
函数中,我们会根据计算属性的类型来确定是否为其定义set
方法:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function defineComputed (target, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key);
} else {
sharedPropertyDefinition.get = createComputedGetter(key);
// 如果是对象,用户会传入set方法
sharedPropertyDefinition.set = userDef.set;
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// 创建Object.defineProperty的get函数
function createComputedGetter (key) {
return function () {
// 通过之前保存的_computedWatchers来取到对应的计算属性watcher
const watcher = this._computedWatchers[key];
if (watcher.dirty) {
// 只有在dirty为true的时候才会重新执行计算属性
watcher.evaluate();
if (Dep.target) {
// 此时,如果栈中有渲染watcher,会为当前计算属性watcher中收集的所有dep再收集渲染watcher
// 在watcher收集的dep对应的属性(this.firstName,this.lastName)更新后,通知视图更新,从而更新页面中的计算属性
watcher.depend();
}
}
return watcher.value;
};
}
在对计算属性取值时,首先会调用它在vm.fullName
上定义的get
方法,也就是上边的createComputedGetter
执行后返回的函数。在函数内部,只有当watcher.dirty
为true
时,才会执行watcher.evaluate
。
下面我们先看下Watcher
中关于计算属性的代码:
import { popTarget, pushTarget } from './dep';
import { nextTick } from '../shared/next-tick';
import { traverse } from './traverse';
let id = 0;
class Watcher {
constructor (vm, exprOrFn, cb, options = {}) {
// some code ...
// 设置dirty的初始值为false
this.lazy = options.lazy;
this.dirty = this.lazy;
if (typeof exprOrFn === 'function') {
this.getter = this.exprOrFn;
}
// some code ...
// 初始化时计算属性的getter不会执行,用到的时候才会执行
this.value = this.lazy ? undefined : this.get();
}
// 执行传入的getter函数进行求值,将其赋值给this.value
// 求值完毕后,将dirty置为false,下次将不会再重新执行求值函数
evaluate () {
this.value = this.get();
this.dirty = false;
}
// 为watcher中的dep,再收集渲染watcher
depend () {
this.deps.forEach(dep => dep.depend());
}
get () {
pushTarget(this);
const value = this.getter.call(this.vm);
if (this.deep) {
traverse(value);
}
popTarget();
return value;
}
update () {
if (this.lazy) { // 依赖的值更新后,只需要将this.dirty设置为true
// 之后获取计算属性的值时会再次执行evaluate来执行this.get()方法
this.dirty = true;
} else {
queueWatcher(this);
}
}
// some code ...
}
watcher.evaluate
中的逻辑便是执行我们在定义计算属性时传入的回调函数(getter
),将其返回值赋值给watcher.value
,并在取值完毕后,将watcher.dirty
置为false
。这样再次取值时便直接将watcher.value
返回即可,而不用再执行回调函数进行重复计算。
当计算属性的依赖属性(this.firstName
和this.lastName
)发生变化后,我们要更新视图,让计算属性重新执行getter
函数获取到最新值。所以代码中判断Dep.target
(此时为渲染watcher
)
是否存在,如果存在会为依赖属性收集对应的渲染watcher
。这样在依赖属性更新时,便会通过渲染watcher
来通知视图更新,获取到最新的计算属性。
用文字来描述:
- 初始化计算属性,为
vm
添加fullName
属性,并设置其get
方法 - 首次渲染页面,
stack
中存储了渲染watcher
。由于页面中用到了fullName
属性,所以在渲染时会触发fullName
的get
方法 fullName
执行get
会通过依赖属性firstName
和lastName
来求值,computed watcher
会进入stack
中- 此时又会触发
firstName
和lastName
的get
方法,收集computed watcher
fullName
求值方法执行完成,computed watcher
出栈,Dep.target
为渲染watcher
- 此时为
fullName
对应的computed watcher
中的dep
(也就是firstName
和lastName
对应的dep
)收集渲染watcher
- 完成
fullName
的取值过程,此时firstName
和lastName
的dep
中分别收集的watcher
为[computed watcher, render watcher]
假设我们更新了依赖,会通知收集的watcher
进行更新:
vm.firstName = 'F'
在firstName
属性更新后,会触发其对应的set
方法,执行dep
中收集的computed watcher
和render watcher
:
computed watcher
: 将this.dirty
设置为true
,fullName
之后取值时需要重新执行用户传入的getter
函数render watcher
: 通知视图更新,获取fullName
的最新值
到这里我们实现的computed
属性便能正常工作了!
文章的源代码在这里:传送门
本文从一个简单的计算属性例子开始,一步步实现了计算属性。并且针对这个例子,详细分析了页面渲染时的整个代码执行逻辑。希望小伙伴们在读完本文后,能够从源码的角度,分析自己代码中对应计算属性相关代码的执行流程,体会一下Vue
的computed
属性到底帮我们做了些什么。