# Vue 计算属性(computed)源码解析

<div id="app">{{fullName}}</div>

<script>
  const vm = new Vue({
    el: "#app",
    data: {
      age: 100,
      firstName: "guo",
      lastName: "mei",
    },
    computed: {
      // 相当于 Object.defineProperty 中的 getter,不取值不会执行
      fullName: {
        get() {
          console.log("取值");
          return this.firstName + this.lastName;
        },
        set(newValue) {
          console.log(newValue);
        }
      }
    }
  })

  console.log(vm.fullName)
  console.log(vm.fullName)
  setTimeout(() => {
    vm.firstName = "su";
    console.log(vm.fullName);
  }, 1000);
</script>

computed

1、计算属性默认不会执行,只有获取计算属性的值的时候才会执行,相当于Object.defineProperty中的getter方法。 2、计算属性(computed)会对最终的返回值进行缓存,取值时不是每次取值都重新执行,只有依赖的值产生变化时才会重新执行,获取新的值。

计算属性也可以写成函数,或者写成对象(get、set)的形式

# 计算属性的初始化

// src/state.js
import Watcher from "./observer/watcher";

export function initState(vm) {
  // 状态的初始化
  const opts = vm.$options;
  if (opts.data) {
    initData(vm);
  }
  if (opts.computed) {
    initComputed(vm, opts.computed);
  }
}

function initComputed(vm, computed) {
  const watchers = (vm._computedWatchers = {});
  for (let key in computed) {
    // useDef可能是 函数、对象
    let useDef = computed[key];
    let getter = typeof useDef === "function" ? useDef : useDef.get;
    // 将计算属性watcher存起来,以便后面做缓存的时候使用
    watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true });
    // 将key定义在vm上才能在页面上直接使用 {{fullName}}
    defineComputed(vm, key, useDef);
  }
}

const shardProperty = {
  enumerable: true,
  configurable: true,
  get: () => {},
  set: () => {},
};

function defineComputed(vm, key, useDef) {
  if (typeof useDef === "function") {
    shardProperty.get = createComputedGetter(key);
  } else {
    shardProperty.get = createComputedGetter(key);
    shardProperty.set = useDef.set;
  }
  Object.defineProperty(vm, key, shardProperty);
}

cumputed

initState的时候如果cumputed属性存在,会调用initComputed(vm, cumputed)初始化属性,根据属性创建watcher,将watcher保存在vm实例的_computedWatchers属性上,以便后面做缓存使用。

defineComputed的作用 1、把cumputed的属性都挂载到vm实例上,这样才能在模板上使用,或者通过vm来调用。 2、拦截计算属性的get方法,判断是否需要重新取值。

# createComputedGetter

// src/state.js
function createComputedGetter(key) {
  return function() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      // 如果其他的属性值改变了,则重新取值(其他值改变后,dirty会变为true)
      if (watcher.dirty) {
        watcher.evaluate();
      }

      return watcher.value;
    }
  };
}

createComputedGetter

createComputedGetter是主要的判断计算属性是否需要重新取值的方法,当watcherdirty属性为true,代表计算属性需要重新取值。

# 多个watcher的收集(dep升级)

// src/observer/dep.js

// 最初采用Dep.target来存放watcher,这样只能存一个
Dep.target = null;

// 增加栈结构存放多个watcher
let stack = [];

export function pushTarget(watcher) {
  Dep.target = watcher;
  stack.push(watcher);
}
export function popTarget() {
  stack.pop();
  Dep.target = stack[stack.length - 1]; // 取栈中最后一个watcher(上一个Dep.target的watcher)
}

TIP

最初只有渲染watcher,所以不需要栈结构。当计算属性watcher引入后,计算属性依赖的值不只要收集计算属性watcher,还要收集渲染watcher,以便依赖值更新后通知渲染watcher更新视图对计算属性重新取值。此时就需要引入栈结构来存放多个watcher

# Watcher 类的改造

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

let id = 0;
class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    // this.vm = vm;
    // this.exprOrFn = exprOrFn;
    // this.user = !!options.user;
    this.lazy = !!options.lazy; // 是不是计算属性watcher
    this.dirty = this.lazy; //计算属性watcher的更新标识(true:需要更新,false:不更新)
    // this.cb = cb;
    // this.options = options;
    // this.id = id++;
    // this.deps = [];
    // this.depsId = new Set();

    // if (typeof exprOrFn == "string") {
    //   this.getter = function() {
    //     let path = exprOrFn.split(".");
    //     let obj = vm;
    //     for (let i = 0; i < path.length; i++) {
    //       obj = obj[path[i]];
    //     }
    //     return obj;
    //   };
    // } else {
    //   this.getter = exprOrFn;
    // }

    // 如果是计算属性watcher,默认不需要取值
    this.value = this.lazy ? undefined : this.get();
  }
  get() {
    pushTarget(this); // Dep.target = watcher
    const value = this.getter.call(this.vm); // render() 方法会去vm上取值 vm._update(vm._render)
    popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了

    return value;
  }
  update() {
    // 计算属性依赖的值更新了会通知计算属性watcher更新
    // 如果是计算属性watcher,就把dirty改为true  代表计算属性需要重新取值
    if(this.lazy){
      this.dirty = true
    }else{
      queueWatcher(this);
    }
  }
  // 计算属性重新取值方法
  evaluate(){
    this.value = this.get()
    this.dirty = false
  },
  // 计算属性watcher存了依赖的属性的dep,当Dep.target上还有值时,需要依赖的值去收集渲染watcher
  depend(){
    let i = this.deps.length
    while(i--){
      this.deps[i].depend()
    }
  },
  // run() {
  //   let newValue = this.get();
  //   let oldValue = this.value;
  //   this.value = newValue;
  //   if (this.user) {
  //     if (newVal !== oldVal || isObject(newVal)) {
  //       this.cb.call(this.vm, newVal, oldVal);
  //     }
  //   } else {
  //     this.cb.call(this.vm);
  //   }
  // }
  // addDep(dep) {
  //   let id = dep.id;
  //   if (!this.depsId.has(id)) {
  //     this.depsId.add(id);
  //     this.deps.push(dep);
  //     dep.addSub(this);
  //   }
  // }
}

export default Watcher;

Watcher 改造

1、对计算属性watcher(options.lazy = true)增加lazy、dirty属性,lazy表示计算属性watcherdirty表示计算属性watcher是否需要重新取值。计算属性watcher在初始化时是不需要调用this.get取值的 2、计算属性依赖的值更新了会通知计算属性watcher更新(update方法),此时需要把计算属性watcherdirty设为true,这样下次计算属性取值时就会更新值。 3、evaluate:计算属性watcher更新值的方法 4、depend:计算属性watcher存了依赖的属性的dep,当Dep.target上还有值时,需要依赖的属性去收集渲染watcher

# 再次修改 createComputedGetter

// src/state.js
function createComputedGetter(key) {
  return function() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      // if (watcher.dirty) {
      //   watcher.evaluate();
      // }

      // Dep.target存在则说明存在渲染watcher,计算属性依赖的值也需要收集渲染watcher,方便后续值改变了更新视图
      if (Dep.target) {
        watcher.depend();
      }
      // return watcher.value;
    }
  };
}

createComputedGetter

计算属性本身并没有走observe方法进行观测,所以并没有dep,并不会去收集渲染watcher

计算属性依赖的值比如firstName,只会收集计算属性fullNamewatcher,如果firstName没有取过值(get时才会做依赖收集),就不会收集渲染watcher。当firstName发生变化后,只会通知计算属性watcher调用update方法将ditry属性改为true,但是没有收集渲染watcher,不会触发视图更新,计算属性也就不会重新取值。

所以需要让依赖的属性也收集渲染watcher,依赖的值改变了之后不仅要通知计算属性watcherditry改为true,还需要通知渲染watcher更新视图,对计算属性fullName重新取值。

在计算属性首次取值时,会执行劫持后的createComputedGetter包装后的返回函数。对应的watcherdirty属性为true,调用evaluate方法,内部会走get方法。此时依赖值将收集计算属性watcher,之后调用popTargetDep.target设为栈中上一个watcher。此时需要调用watcher.depend让依赖值收集渲染watcher

上次更新: 6/6/2021, 6:35:48 PM