手写Vue源码(九) - 计算属性
source link: https://zhuanlan.zhihu.com/p/345208305
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
手写Vue源码(九) - 计算属性
官网对计算属性的介绍在这里:传送门
计算属性是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
来通知视图更新,获取到最新的计算属性。
依赖属性更新
以文章开始时的demo
为例,首次执行时的逻辑如下图:
用文字来描述:
- 初始化计算属性,为
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
属性到底帮我们做了些什么。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK