Vue 中 computed学习与探索

204 阅读3分钟

之前读过的文章

在没有想自己去看源码的时候,在网上看过几篇文章,总结起来,主要说了以下几个方面:

  1. 计算属性初始化,代码位于 vue\src\core\instance\state.js, 源码如下,在 initState 时如果有 computed 属性则初始化该属性
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
  1. initComputed 的实现,代码位于 vue\src\core\instance\state.js,源码如下,对 cpmputed 对象中每个 key 创建一个 Watcher 监听
function initComputed(vm: Component, computed: Object) {
  const watchers = (vm._computedWatchers = Object.create(null));
  // computed properties are just getters during SSR
  const isSSR = isServerRendering();

  for (const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;
    if (process.env.NODE_ENV !== "production" && getter == null) {
      warn(`Getter is missing for computed property "${key}".`, vm);
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== "production") {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm);
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(
          `The computed property "${key}" is already defined as a prop.`,
          vm
        );
      }
    }
  }
}
  1. Watcher 的实现,代码比较多我这边就不贴了,位于 vue\src\core\observer\watcher.js
  2. Dep 的实现,代码位于 vue\src\core\observer\dep.js

最后,看完这些的结论是:我只知道这些代码在哪里,有什么用,computed 计算会用到这些代码,那为什么 computed 里面 data 变了会重新计算还是不明白...

开始自己的探索

initComputed 的时候,computed 定义的函数通过 getter 参数传给了 Watcher

const getter = typeof userDef === "function" ? userDef : userDef.get;
if (!isSSR) {
  // create internal watcher for the computed property.
  watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions);
}

Watcher 中,getter 被赋值到了实例上

if (typeof expOrFn === "function") {
  this.getter = expOrFn;
}

Watcher 的原型上,有一个 get 方法,是唯一调用这个 getter 的地方

get () {
  pushTarget(this);
  let value;
  const vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`);
    } else {
      throw e;
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value;
};

但是这个 get 方法我有点看不明白,首先调用了 pushTarget,于是看了 pushTarget 的实现

export function pushTarget(_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target);
  Dep.target = _target;
}

Dep.target 又是什么,在这边卡了挺久的,于是我全局搜了下 Dep.target,在 vue\src\core\observer\index.js 搜到了如下代码

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {
    const value = getter ? getter.call(obj) : val;
    if (Dep.target) {
      dep.depend();
      if (childOb) {
        childOb.dep.depend();
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
    }
    return value;
  },
  set: function reactiveSetter(newVal) {
    const value = getter ? getter.call(obj) : val;
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return;
    }
    /* eslint-enable no-self-compare */
    if (process.env.NODE_ENV !== "production" && customSetter) {
      customSetter();
    }
    if (setter) {
      setter.call(obj, newVal);
    } else {
      val = newVal;
    }
    childOb = !shallow && observe(newVal);
    dep.notify();
  }
});

这个不是 vue 为自己的 data 创建 getter/setter 的代码吗?为什么在 get 中用到了 Dep.target,在回头看 Watcherget,先 pushTarget(this),然后 popTarget(),似乎发现了什么。

Watcher.get 的时候(可以认为是第一次 computed 计算),Watcher 告诉执行环境我(this)开始监听了(pushTarget(this)),然后开始调用 getter,也就是我们定义的 computed 方法,在执行过程中,因为 Dep.target 不是 null,所以在 data 属性 get 的时候,就调用了 dep.depend()

depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

Dep.target.addDep(this) 也就是 Watcher 里面添加了这个订阅,所以该值变化的时候,这个 Watcher 就可以知道了,data 属性 set 的时候,会调用 dep.notify()

notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

改属性的每个订阅者都会执行 update()

具体细节还没细抠,可能有些地方表达的不是很准确...

基于以上的一个实验

在这次探索的过程中,我产生了一个疑问,如果计算属性只有 data get 的时候才会被加入监听,那么如果我的计算属性里有 if else 呢,计算属性中有副作用代码导致 if else 变化呢。

于是,我使用 vue-cli@2.x 生成了一个项目来做测试

首先,验证下上面的思路,我把一个 get 写在了不会执行到的 else

<template>
  <div>
    <div @click="aChange">A Change</div>
    <div @click="bChange">B Change</div>
    <h1>{{ num }}</h1>
  </div>
</template>

<script>
export default {
  data () {
    return {
      a: 1,
      b: 2
    }
  },
  computed: {
    num () {
      const a = this.a
      console.log('change')
      if (a) {
        return a
      } else {
        return this.b
      }
    }
  },
  methods: {
    aChange () {
      this.a += 1
    },
    bChange () {
      this.b += 1
    }
  }
}
</script>

最后的结果是,点 A Changenum 递增,点 B Change,没有任何反应,甚至连 log 都没打出来

于是我把 num 函数改成

const a = this.a;
const b = this.b;
console.log("change");
if (a) {
  return a;
} else {
  return b;
}

log 就有打出来了,但是结果是一样的,但是如果遇到了 if (a + window.xx),之类的副作用代码,就会产生问题了,所以建议计算属性的代码尽量纯,如果是纯函数的话,把 get 写在判断中还可以减少计算次数。

有观察到 vueWatcher 的代码与之前版本相比有变化,原来代码只有一个 this.dep, 现在有出现了 this.newDeps,于是我又将代码改成了

export default {
  data() {
    return {
      a: 1,
      b: 2,
      c: 3
    };
  },
  computed: {
    num() {
      const a = this.a;
      console.log("change");
      if (a) {
        return this.b;
      } else {
        return this.c;
      }
    }
  },
  methods: {
    aChange() {
      this.a = !this.a;
    },
    bChange() {
      this.b += 1;
    },
    cChange() {
      this.c += 1;
    }
  }
};

最后的现象就是 A Change 像一个开关,点击了 b c 的监听状态就会切换,不会同时监听 b c