如上图所示,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
为当前实例后,再将执行结果返回。