Vue 源码分析 - initComputed

1,033 阅读3分钟

背景

有这样一个 demo,父组件 computed.date 依赖 data.dateRange,并且 data.dateRange 在 created 中被改变了,computed.date 传递给子组件,那么首次传递给子组件的值是基于 this.dateRange 改变后还是改变前计算得出的?

<html>
  <body>
    <div id="app">
      <div>I am Parent Component : name is {{ name }}</div>
      <div>------------------------------------------</div>
      <comp :date="date" />
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
      const comp = {
        template: `<div>I am Child Component : name is Child</div>`,
        props: {
          date: {
            type: Array,
          },
        },
        data() { 
          return {
            name: 'Child',
          }
        },
        beforeCreate() {
          debugger
          console.log('子beforeCreate')
        },
        created() {
          debugger
          console.log('子created')
        },
        mounted() {
          debugger
          console.log('子mounted')
        }
      }
      const vm = new Vue({
        el: '#app',
        components: {
          comp
        },
        data: {
          name: 'Parent',
          dateRange: {},
        },
        computed: {
          date() {
            // console.log('date...')
            const { startTime, endTime, shortcut } = this.dateRange
            return [
              startTime,
              endTime,
              shortcut,
            ]
          },
        },
        beforeCreate() {
          debugger
          console.log('父beforeCreate')
        },
        created() {
          debugger
          console.log('父created')
          this.dateRange = { startTime: 1, endTime: 2, shortcut: 3 } // 同步修改
          // setTimeout(() => { // 异步修改
          //   this.dateRange = { startTime: 1, endTime: 2, shortcut: 3 }
          // })
        },
        mounted() {
          debugger
          console.log('父mounted')
        }
      })
    </script>
  </body>
</html>

源码分析

initComputed 相关源码的简写如下:

// 初始化 computed
function initComputed (vm, computed) {
  vm._computedWatchers = Object.create(null); // 创建一个全空对象,用于存储 computed watcher

  for (var key in computed) {
    var userDef = computed[key]; // 此例的 computed.date 是一个函数
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    vm._computedWatchers[key] = new Watcher( // 为每一个计算属性新建一个 Watcher
      vm,
      getter || noop,
      noop, // 空函数:function() {}
      computedWatcherOptions  // { lazy: true }
    );

    defineComputed(vm, key, userDef); // 将 date 属性添加到 vm 上
  }
}
var sharedPropertyDefinition = {
  configurable: true,
  enumerable: true,
  get: noop
  set: noop
}
function defineComputed (target, key, userDef) { // 省略了 cache 的判断逻辑
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache // shouldCache: true
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
      ? createComputedGetter(key) : createGetterInvoker(userDef.get);
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  if (sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        ("Computed property \"" + key + "\" was assigned to but it has no setter."),
        this
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

function createComputedGetter (key) {
  return function computedGetter () { // 备注1,后面有提到
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) { // computedWatcher 的 dirty 为 true,表示还未计算过
        watcher.evaluate(); // 这个方法会调用 watcher.get,从而调用计算属性的方法,返回的值保存在 watcher.value 中
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}
function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

主要做了两件事:

  • 为计算属性新建 Watcher 实例,又称 computedWatcher - new Watcher()
  • 将计算属性代理到 vm 上 - defineComputed

具体步骤如下:

  1. 创建一个全空对象 vm._computedWatchers = Object.create(null),用于存储 computedWatcher
  2. 遍历 computed 中的每一个属性,它的值可能是函数或者对象:
    1. 将函数或对象的 get 函数赋值给 getter。
    2. 新建 Watcher 实例,其 lazy 属性为 true (注:只有 computed watcherlazy 属性才为 true )。
    3. 根据值是函数还是对象对属性描述对象 sharedPropertyDefinitionget/set 赋值,然后将计算属性代理到 vm 上。

整体流程

Vue.prototype._init = function(options) {
  var vm = this;
  vm.$options = options;
  ...
  callHook(vm, 'beforeCreate');
  initState(vm);
  callHook(vm, 'created');
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el); // 调用 vm.$mount 开始渲染
  }
}

created 调用之后,调用 vm.$mount 开始渲染,先生成渲染函数(render),然后调用 beforeMount 钩子,接着定义一个 updateComponent 函数,然后为其新建一个 Watcher(又称 renderWatcher),内部调用 this.get(),执行 updateComponent() -> vm._render() -> render(),由于 render 内部使用了 date 变量,所以会调用 date 的 get 函数,即上面的备注1,然后调用

date() {
  // console.log('date...')
  const { startTime, endTime, shortcut } = this.dateRange
  return [
    startTime,
    endTime,
    shortcut,
  ]
},

针对本次 demo,此时 this.dateRange 已经是改变后的值,所以首次传递给子组件的值是基于 this.dateRange 改变后计算得出的。如果在 created 中对 this.dateRange 的改变是异步的,那么执行上面这个函数的时候,this.dateRange 还未改变,所以首次传递给子组件的值是基于 this.dateRange 还未改变计算出来的。

Vue.prototype.$mount = function(el) {
  vm.$options.render = function anonymous() {
    with(this) {
      return _c('div', {attrs:{"id": "app"}}, [
                _c('div', [_v("I am Parent Component : name is "+_s(name))]),
                _v(" "),
                _c('div', [_v("------------------------------------------")]),
                _v(" "),
                _c('comp', {attrs:{"date": date}})
             ], 1)
    }
  } // 生成 render 函数
  callHook(vm, 'beforeMount'); // 调用 beforeMount 钩子
  updateComponent = function () { // 定义 updateComponent 函数
    vm._update(vm._render(), hydrating); // 此时 hydrating: false
  };
  new Watcher(vm, updateComponent, noop, { // 新建 Watcher
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */)
  callHook(vm, 'mounted');
}
Vue.prototype._render = function () {
  ...
  var render = vm.$options.render;
  vnode = render.call(vm._renderProxy, vm.$createElement);
}

// Watcher 定义
function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
    this.before = options.before;
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  this.dirty = this.lazy // for lazy watchers
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  }
  this.value = this.lazy ? undefined : this.get();
}
Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (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
  };

template 与 render 函数的对应

// template
<div id="app">
  <div>I am Parent Component : name is {{ name }}</div>
  <div>------------------------------------------</div>
  <comp :date="date" />
</div>
// render
(function anonymous() {
  with(this) {
    return _c('div', {attrs:{"id": "app"}}, [
              _c('div', [_v("I am Parent Component : name is "+_s(name))]),
              _v(" "),
              _c('div', [_v("------------------------------------------")]),
              _v(" "),
              _c('comp', {attrs:{"date": date}})
           ], 1)
  }
})

debugger

debugger 的时候,当断点停到父组件的 created,此时在 chrome Scope 可以看到一开始 date 的值是 ...,鼠标浮动上去,会出现 invoke property getter 的提示,点击 ... 会触发 date 的 getter 函数。