Skip to content

计算属性computed

计算属性的使用方式

  1. 函数型
js
computed: {
    newName() {
        return this.name + '123'
    }
}
  1. 对象型
js
computed: {
    newName: {
        // get必须有
        get() {
        return this.name + 1231
        },
        // set可以没有
        set(val) {
            // 设置逻辑
        }
    }
}

initComputed

初始化的时候, 会将computed遍历一个个取出来, 先看一下当前计算属性是否和vm身上的属性重名, 若是未重名则取到内部的getter属性, 若是函数则这个函数就是getter, 并且依次创建计算属性watcher, 并将创建的watcher存起来

js
function initComputed(vm) {
  const computed = vm.$options.computed;
  // 用于存储计算属性watcher, 建立key与watcher的映射关系, watcher为vm._computedWatchersd的把别名
  // 将其挂到vm身上的原因是方便后续取用, 因为在vm[key]触发getter, get函数中的this就是vm
  const watchers = (vm._computedWatchers = Object.create(null));
  for (const key in computed) {
    if (!(key in vm)) {
      const userConf = computed[key];

      // 取到get函数
      const getter =
        typeof userConf === "function" ? userConf : userConf.get || noop;
      // 生成watcher,并存到watcher内, 建立与key的关系
      watchers[key] = new Watcher(vm, getter, noop, { lazy: true });

      // 将key写到vm身上(这就是computed的本质)
      defineComputed(vm, key, userConf);
    }
  }
}

计算属性 Watcher

提示

注意: computed不同与watch, 计算属性只有在调用这个计算属性的时候才会调用计算属性内的get函数, 因此在实例化 watcher 的时候不需要执行依次get函数进行依赖收集, 依赖收集是在主动调用get的时候才会进行,因此实例化时候需要给 options 添加一个lazy:true属性告诉 watcher 不用立即执行get

js
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";

let uid = 0;
class Watcher {
  constructor(vm, exprFn, cb, options) {
    this.vm = vm;
    this.cb = cb; // 回调函数
    this.uid = uid++; // 每一个组件都有一个渲染watcher, 这是唯一标识
    this.depsIds = new Set(); // 一个watcher上面有多个dep, 且不重复
    this.deps = [];
    this.options = !!options; // 是否为渲染watcher
    this.user = !!options.user;
    this.lazy = !!options.lazy; 
    this.dirty = !!options.lazy; 

    if (this.user) {
      this.getter = function () {
        let obj = vm;
        const path = exprFn.split(".");
        for (let i = 0; i < path.length; i++) {
          obj = obj[path[i]];
        }
        return obj;
      };
    } else {
      this.getter = exprFn;
    }
    // 若不是计算属性,则首先就让组件渲染函数执行
    this.value = this.get(); 
    this.value = this.lazy ? undefined : this.get(); 
  }

  run() {
    const newVal = this.get();
    if (newVal !== this.value) {
      this.value = newVal;
      this.cb.call(this.vm, newVal, this.value);
    }
  }

  // 添加dep
  addDep(dep) {
    const depId = dep.uid;
    if (!this.depsIds.has(depId)) {
      this.depsIds.add(depId);
      this.deps.push(dep); // 将当前watcher与dep建立1对多的关系
      dep.subs.push(this); // dep与watcher也建立1对多的关系
    }
  }
  // 更新页面
  update() {
    // lazy=true的watcher为计算属性watcher, 计算属性默认没有渲染页面的能力
    // 因此在依赖跟新的时候仅将标志变成true, 等待下一次取值的时候重新计算(执行计算属性的get函数)
    if (this.lazy) { 
      this.dirty = true; 
    } else { 
      queueWatcher(this);
    }
  }

  get() {
    const vm = this.vm;
    pushTarget(this);
    //  这里需要特别注意,需要更改get函数的this指向
    // let val = this.getter();
    let val = this.getter.call(vm, vm); 

    popTarget();
    return val;
  }

  evaluate() {
    this.value = this.get(); //[!code ++]   //执行计算属性取值操作, 取值时会收集计算属性依赖
    this.dirty = false; //[!code ++] // 随后将标志符设为false, 避免再次取值时候重新计算  
  }
}

export default Watcher;

defineComputed 定义计算属性

通过Object.definePropertyvm上定义计算属性, 并且重写get方法,便于后面进行依赖收集

js
function defineComputed(vm, key, userConf) {
  const defaultConf = {};
  if (typeof userConf === "function") {
    defaultConf.get = createComputedGetter(key);
  } else {
    defaultConf.get = createComputedGetter(key);
    defaultConf.set = userConf.set || noop;
  }

  Object.defineProperty(vm, key, defaultConf);
}

createComputedGetter 重写计算属性 get 函数

js
function createComputedGetter(key) {
  return function getter() {
    // 这里的this为vm
    const watcher = this._computedWatchers[key];
    if (watcher.dirty) {
      // 缓存的开关, 默认是true
      watcher.evaluate(); // 等价于watcher.get, 并且会将watcher.get()的值挂到watcher.value上, 且将dirty设置false
    }
    // 计算属性get函数返回值(缓存)
    return watcher.value;
  };
}

计算属性依赖收集 🤣🤣🤣

到这里, 首次渲染的时候, 能将计算属性渲染到页面上了, 但是存在一个问题, 就是再次更新计算属性依赖的值时候, 计算属性也会随之更新, 但是页面不会重新渲染, 为何会这样? 是因为计算属性在进行依赖收集的时候, 仅收集了计算属性的watcher, 并没有收集渲染watcher, 因此在计算属性值更新之后没有主动渲染页面

假设现在有一个计算属性

js
<div>{{fullName}}</div>

computed: {
    fullName() {
        return this.firstname + this.lastname
    }
}

页面首次渲染时候,会首先收集到渲染watcher, 渲染过程中会进行取值操作, 即this.fillName, 首次取值会触发计算属性getter, 由于是首次,watcher.dirty=true, 因此会执行 watcher.evaluate();触发计算属性watcher进行依赖收集, 此时Dep.target为计算属性watcher, 在执行计算属getter时, 还会触发firstname,lastname进行取值, 但是此时的Dep.target依然是计算属性watcher, 因此, firstnamelastnamedep里面都会记录计算属性watcher, 计算属性watcher同时记录了flrstname,lastnamedep

flrstnamelastname 值变化后, 会通过dep来执行watcher.update(), 由于depfirstnamelastnamedep里面都会记录计算属性watcher还有渲染watcher,且在stack中栈顶肯定为计算属性watcher, 栈底为渲染watcher, 所以会先执行计算属性watcher.update()更改this.dirty = true; 随后执行渲染watcher.update()渲染`页面的时候,又会进行计算属性取值操作以及页面渲染依赖收集

因此在计算属性的getter中, 还需要对计算属性依赖的firstnamelastname进行依赖收集, 以便后面他们值更改后进行页面渲染

js
// state.js
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers[key];
    if (watcher.dirty) {
      watcher.evaluate();
    }
    if (Dep.target) { 
      watcher.depend();
    } 

    return watcher.value;
  };
}
js
// watcher.js

// 计算属性依赖收集 Watcher实例方法
depend() {
  let len = this.deps.length
  while(len--) {
    this.deps[len].depend();
  }
}