Vue2核心原理(简易版)-计算属性computed功能实现
为什么要用计算属性computed
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。对于任何复杂逻辑,你都应当使用计算属性。
计算属性computed是什么?
计算属性将被混入到 Vue 实例中。所有 getter 和 setter 的 this 上下文自动地绑定为 Vue 实例。计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。文档传送
示例:
// template
<li>{{ fullName }}</li>
// 定义watch
let vm = new Vue({
el: '#app',
data() {
return {
firstName: 'vue',
lastName: '2'
}
},
computed: {
fullName() {
return this.firstName + '' + this.lastName
}
}
})
// 改变name
setTimeout(() => {
vm.lastName = '3'
}, 1000)
// 页面
// <li>vue 2</li> => <li>vue 3</li>
怎么做计算属性computed?
计算属性和watch属性不一样,计算属性的本质是Object.defineProperty。
核心原理是,模版中想要获取在vm实例上计算属性fullName引用,但是vm并没有提供这个属性,所以我们使用Object.defineProperty把计算属性定义的getter绑定成defineProperty的getter,这样就相当于是取了getter里面的内容
this.firstName + '' + this.lastName,根据前面的课程我们知道了firstName和lastName是在vm实例上能找到的,并且是被响应式监听的。那么我们就可以拿到实时的this.firstName + '' + this.lastName计算过后的值,也就是fullName了。
这样的话,实现起来贼简单,代码如下
// state.js
function initComputed() {
const vm = this;
let computeds = vm.$options.computed;
for (let key in computeds) {
// 校验
const userDef = computeds[key];
defineComputed.call(vm, key, userDef);
}
}
function defineComputed(key, userDef) {
const vm = this;
let sharedProperty = {};
if (typeof userDef === "function") {
sharedProperty.get = userDef;
} else if (typeof userDef === "object") {
sharedProperty.get = userDef.get;
sharedProperty.set = userDef.set;
}
Object.defineProperty(vm, key, sharedProperty);
}
可是这样的计算数据是没有缓存的,他不管依赖的property是否变化,都会重新计算。我们需要对它进行改进。
我们用一个watcher把计算属性的值缓存起来,把依赖的property设为watcher的依赖,当依赖项变化的时候,我们才让watcher重新update,才使得计算属性重新计算。我们可以给watcher添加一个dirty属性,当数据需要计算(依赖项变化)的时候,dirty变为true,执行evalute方法,当计算完成dirty变为false。
代码如下:
// state.js
sharedProperty.get = createComputedGetter(key);
function createComputedGetter(key) {
return function computedGetter() { // 取计算属性的值 走的是这个函数
// this._computedWatchers 包含着所有的计算属性
// 通过key 可以拿到对应watcher,这个watcher中包含了getter
let watcher = this._computedWatchers[key]
// 脏就是 要调用用户的getter 不脏就是不要调用getter
if(watcher.dirty){ // 根据dirty属性 来判断是否需要重新求职
watcher.evaluate();// this.get()
}
// 如果当前取完值后 Dep.target还有值 需要继续向上收集
if(Dep.target){
// 计算属性watcher 内部 有两个dep firstName,lastName
watcher.depend(); // watcher 里 对应了 多个dep
}
return watcher.value
}
}
// observer/watcher.js
constructor(vm, exprOrFn, cb, options) {
this.lazy = !!options.lazy; // 计算属性第一遍不需要get,因为一开始数据就是dirty的
this.dirty = options.lazy; // 如果是计算属性,那么默认值lazy:true, dirty:true
// ...
this.value = this.lazy ? undefined : this.get(); // 默认初始化 要取值
}
update() { // vue中的更新操作是异步的
if(this.lazy){
this.dirty = true;
}else{
queueWatcher(this); // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
}
}
evaluate(){
this.dirty = false; // 为false表示取过值了
this.value = this.get(); // 用户的getter执行
}
// observer/dep.js
Dep.target = null; // 一份
let stack = [];
export function pushTarget(watcher) {
Dep.target = watcher;
stack.push(watcher);
console.log(stack)
}
export function popTarget() {
stack.pop();
Dep.target = stack[stack.length - 1];
}
如此这般,计算属性就具有缓存的功能啦!
完整代码
github.com/Sotyoyo/do-… 分支do-computed
原文链接
如果你在非掘金地址看到这篇文章,这里有原文传送门,您的点赞是对我最大的支持!