如上图所示,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; - 特殊的,对于
default为function类型(非原始值的默认值),则会将该方法通过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>
子组件中的 msg1 和 msg3 是响应式的,而 msg2 为空("")。这其实就是 this 指向的问题:
msg1:箭头函数的this是由创建时的环境决定的,也即this == ParentVm(父组件实例),所以getMsg() == "parent msg"。msg2:正常的函数调用的this由当前的执行环境决定,也即this == ChildVm(子组件实例),因为子组件上没有定义msg,所以getMsg2() == undefined,通过{{}}渲染到页面上就是""(空字符串)。msg3:根据结果论,getMsg和getMsg3的this都指向ParentVm,这里就涉及到methods的初始化了,在下面initMethods章节里会有详细说明。
initState
initProps
props 的使用请参考官方文档。
在解析模板生成 render 函数的阶段,会将解析到的
props数据传递给子组件,在子组件中使用props中的参数,会触发对应参数的getter,然后将子组件中对应的Watcher放入当前参数的依赖中,从而实现在父组件更新值后,子组件也会同步更新。在父组件模板中给子组件传递属性,既可以以短杠的形式
user-name,也可以用小驼峰的形式userName,不过在子组件中注册到props中,只能以userName的形式接收。只有在当前是根组件时,才会将
props中的数据转换为响应式数据。因为从父组件传入的props都已经在父组件中定义成响应式了,子组件只是引入,并在使用的时候,再往props里添加对应的依赖,所以不需要转为响应式。特殊的,对于父组件没有提供,且定义了default的prop,需要将默认值转为响应式。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同时存在于props和methods上,那么最后使用时,使用的是methods上的)。 - 挂载时会通过
bind修改methods的this为当前实例。
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 年有人提了一个 Issue :
computed依赖的值发生了变化,但computed的值没有改变,仍然会触发render。官方虽然在后续也重新解决了,不过我最近用最新版的 Vue2.7 试了试,貌似还存在这个问题(戳这里)。
粗略地看了看,尝试解释一下。比如下面这个:sum 依赖于 a 和 b ,其中 sum 的 Watcher 有一个:render Watcher,按理说 a 和 b 的 Watcher 应该只有一个 computed Watcher,那就是当 a 或 b 发生变化后,通知计算属性 sum 重新计算,但是这样无法通知 sum 的 Watcher 去更新。所以 sum 的每个依赖里都会把 sum 的 Watcher 存一份,也即 a 和 b 的 Watcher 里还有一份 render Watcher。然后就导致,就算 a 和 b 的值同时改变了,但是 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 使用的是同一个方法。参数类型可以为 String、Function、Object 和 Array 这四种类型。
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为当前实例后,再将执行结果返回。