Vue2核心原理(简易版)-计算属性computed功能实现

923 阅读3分钟

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

原文链接

如果你在非掘金地址看到这篇文章,这里有原文传送门,您的点赞是对我最大的支持!

完 🎉