如上图所示,Vue 在 beforeCreate -> created 阶段,按这个步骤进行初始化操作。按照这个顺序,也就意味着后者初始化的时候,可以使用前者已经初始化的变量。例如:在 data 中可以使用 methods 里定义的方法,也可以用 props 引入的属性进行初始化。

initInjections

inject 的使用请参考官方文档

  • 先获取当前实例上注册的 inject,读取每一个 key

  • 然后自底向上不断循环获取父组件的 provide 中是否有提供该 key

    // 自底向上循环,获取父组件的 provide
    let curr = this
    while (curr) {
      if (curr.provide && key in curr.provide) {
        // ...
        break
      }
      curr = curr.$parent
    }
    • 若找到了,则跳出循环,并返回结果;
    • 若未找到,则使用配置的默认值 default
    • 特殊的,对于 defaultfunction 类型(非原始值的默认值),则会将该方法通过 call 修改 this 为当前实例后,再将执行结果返回;
    • 如果既没找到,也没设置 default,则抛出一个 非生产警告
  • 然后对于收集到的结果,会遍历每一个 key,然后通过 defineReactive 注册到当前实例上,而在 defineReactive 前会有一个 “取消响应式” 的操作:

    observerState.shouldConvert = false

    这一步就是通知 defineReactive 不要将接下来挂载的数据转换成响应式数据,这也就印证了官方说的 inject 注入的内容不是响应式数据。其实就是把提供的 provide 的内容在引入 inject 的实例上,重新复制了一份。如果是基本数据类型,只是值复制,那么自然而然不是响应式数据,但是如果是引用类型,还是引用的同一个地址,如果源数据是响应式的,那么 inject 引入的也是响应式数据。

    如果想实现基础数据类型的响应式呢?换个角度,如果我们把 provide 提供的属性的 this 绑定在原实例上,那么是否就能通过原实例的 this 访问到原实例上的一些响应式的数据?那么 inject 注入的内容是不是就是响应式的呢?

    看一个例子:

    • 父组件

      // parent
      <template>
        <div>
          <input v-model="msg"></input>
        </div>
      </template>
      
      <script>
      export default {
        provide() {
          return {
            getMsg: () => this.msg,
            getMsg2() { return this.msg },
            getMsg3: this.getMsg3,
          };
        },
        data() {
          return {
            msg: "parent msg",
          };
        },
      };
      </script>
    • 子组件

      // child
      <template>
        <div>
          <p>getMsg: {{ msg1 }}</p>
          <p>getMsg2: {{ msg2 }}</p>
          <p>getMsg3: {{ msg3 }}</p>
        </div>
      </template>
      
      <script>
      export default {
        inject: ["getMsg", "getMsg2", "getMsg3"],
        computed: {
          msg1() {
            return this.getMsg()
          },
          msg2() {
            return this.getMsg2()
          },
          msg3() {
            return this.getMsg3()
          }
        }
      }
      </script>

子组件中的 msg1msg3 是响应式的,而 msg2 为空("")。这其实就是 this 指向的问题:

  • msg1:箭头函数的 this 是由创建时的环境决定的,也即 this == ParentVm(父组件实例),所以 getMsg() == "parent msg"
  • msg2:正常的函数调用的 this 由当前的执行环境决定,也即 this == ChildVm(子组件实例),因为子组件上没有定义 msg,所以 getMsg2() == undefined,通过 {{}} 渲染到页面上就是 ""(空字符串)
  • msg3:根据结果论,getMsggetMsg3this 都指向 ParentVm,这里就涉及到 methods 的初始化了,在下面 initMethods 章节里会有详细说明。

initState

initProps

props 的使用请参考官方文档

  • 在解析模板生成 render 函数的阶段,会将解析到的 props 数据传递给子组件,在子组件中使用 props 中的参数,会触发对应参数的 getter,然后将子组件中对应的 Watcher 放入当前参数的依赖中,从而实现在父组件更新值后,子组件也会同步更新。

  • 在父组件模板中给子组件传递属性,既可以以短杠的形式 user-name,也可以用小驼峰的形式 userName,不过在子组件中注册到 props 中,只能以 userName 的形式接收。

  • 只有在当前是根组件时,才会将 props 中的数据转换为响应式数据。因为从父组件传入的 props 都已经在父组件中定义成响应式了,子组件只是引入,并在使用的时候,再往 props 里添加对应的依赖,所以不需要转为响应式。特殊的,对于父组件没有提供,且定义了 defaultprop,需要将默认值转为响应式。

  • Boolean 类型 prop 的处理:以下四种情况都会将子组件的 prop 设置为 true

    <child name />
    <child name="name" />
    <child userName="user-name" />
    <child user-name="user-name" />

    比对时,会将 key 进行驼峰转化,即 userName -> user-name,如果与提供的值相等,那么也设置为 true

  • 最后,遍历 props 中的每一项,然后将不在当前 vm 实例上的,设置一层代理,然后挂载在 vm 实例上。

initMethods

  • 遍历 $options.methods,先检验是否有与 props 中的有重复的,如果有,则抛出一个 非生产警告。注意这只是一个警告,如果没有修正,那么依然还会将当前 methods 挂载在当前实例上(也即,如果某一个 key 同时存在于 propsmethods 上,那么最后使用时,使用的是 methods 上的)。
  • 挂载时会通过 bind 修改 methodsthis 为当前实例。

initData

  • 特殊的,data 必须是一个对象,或者是一个返回对象的函数。

  • 遍历 data 的校验阶段:

    • 先判断,如果属性已存在于 methods 中,则抛出一个 非生产警告
    • 再判断,如果属性已存在于 props 中,抛出一个 非生产警告,但是不会挂载,只有当前属性不以 $_ 开头时,才会代理到实例上。

Vue 内部使用的代理方法

const noop = _ => _
const sharePropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

function proxy(target, sourceKey, key) {
  sharePropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }

  sharePropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }

  Object.defineProperty(target, key, sharePropertyDefinition)
}

initComputed

computed 的使用请参考官方文档

  • 先检验,如果当前的属性名已存在 vm 上,则不会处理这个属性。
  • 收集当前 getter 中所有用到的属性的依赖,当这些属性变化时,通知 computed Watcher 去更新。

在 18 年有人提了一个 Issuecomputed 依赖的值发生了变化,但 computed 的值没有改变,仍然会触发 render。官方虽然在后续也重新解决了,不过我最近用最新版的 Vue2.7 试了试,貌似还存在这个问题(戳这里)。

粗略地看了看,尝试解释一下。比如下面这个:sum 依赖于 ab ,其中 sumWatcher 有一个:render Watcher,按理说 abWatcher 应该只有一个 computed Watcher,那就是当 ab 发生变化后,通知计算属性 sum 重新计算,但是这样无法通知 sumWatcher 去更新。所以 sum 的每个依赖里都会把 sumWatcher 存一份,也即 abWatcher 里还有一份 render Watcher。然后就导致,就算 ab 的值同时改变了,但是 sum 的结果没变,还是会触发 render Watcher,重新执行一次 render 。虽然在 diff 阶段,最新的一次 render 没有任何变化,但还是会造成性能的浪费。(个人愚见哈 😂)

<template>
  <div>{{ sum }}</div>
</template>
<script>
export default {
  computed: {
    sum() {
      return this.a + this.b
    }
  }
}
</script>

initWatch

watch 的使用请参考官方文档

初始化时,创建 Watcher$watch 使用的是同一个方法。参数类型可以为 StringFunctionObjectArray 这四种类型。

watch: {
  a: function(newVal, oldVal) {},
  b: 'handlerWatchB',
  c: {
    handler(newVal, oldVal) {},
    deep: false,
    immediate: false
  },
  d: [
    function handlerWatchD1(newVal, oldVal){},
    function handlerWatchD2(newVal, oldVal){},
  ]
}

鉴于参数的特殊性,需要对类型为 Array 的特殊处理,批量创建 Watcher

// 创建 Watcher
function createWatcher(vm, expOrFn, handler, options) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === "string") {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

initProvide

provide 的使用请参考官方文档

  • 如果是 object 类型,则直接挂载在当前实例上;
  • 如果是 function 类型,则通过 call 修改 this 为当前实例后,再将执行结果返回。