文章对应的 github
直接整理总结
- 生命周期日志 - 正常结构和slot结构都是一样的

- 父组件是在
beforeMount创建子组件的 mounted是在最里面的子组件依次往上返回的
- 从props、data、看生命周期
- 在
beforeCreate是访问不到props和data的 beforeCreate访问props上的属性会导致报错- 子组件在
beforeMounted访问不到父组件的DOM节点 - 在
mounted就可以访问 DOM了,不管是自身的还是子组件的。
- 在
- 组件更新
- 父组件数据更新,子组件使用了父组件数据,父子都触发hook了
- 父组件数据更新,子组件没有依赖父组件数据,并不会触发hook
beforeUpdate拿到的是更新前的DOMupdated拿到的是更新后的DOM- 祖先与超两层的组件传数据这种组合,祖先更新,会使整个链条都触发hook
- 只传抵方法且不依赖祖先数据,底层组件触发方法,只有祖先会触发hook
- 上面的触发hook就算是全部触发,实际Repaint只会在对应的节点上。
- 销毁组件
beforeDestroy和destroyed在Chrome和Firefox下并无区别,两者的DOM节点都从页面移除了,但节点引用依然可以访问- 以防万一,使用
beforeDestroy较好
1. 初始化项目
1.1. 使用 @vue/cli 快速生成项目
1.2. 文件结构和代码

代码如下 Root.vue One.vue 和 Two.vue 只是把所有生命周期都在控制台打印一下三个文件,大致相同所以不展示了。
具体可在 相关分支 查看
import One from './One.vue'
export default {
name: 'root',
components: {
One
},
data() {
console.log('root data') // One Two 就是对应修改了 root这个单词而已
return {
root: 'root'
}
},
beforeCreate() {
console.log('root beforeCreate')
},
created() {
console.log('root created')
},
beforeMount() {
console.log('root beforeMount')
},
mounted() {
console.log('root mounted')
},
beforeUpdate() {
console.log('root beforeUpdate')
},
updated() {
console.log('root update')
},
beforeDestroy() {
console.log('root beforeDestroy')
},
destroyed() {
console.log('root destroyed')
}
}
2. 生命周期调用顺序
2.1. 正常父子组件
引用关系
<!--Root-->
<div id="app">
Root: <input type="text" v-model="root" />
<One :root="root" />
</div>
<!-- One -->
<div>
<h1>One: {{root}}</h1>
<input type="text" v-model="one">
<Two :root="root" :one="one" />
</div>
<!-- Two -->
<div>
<h2>Two: {{root}} {{one}}</h2>
<input type="text" v-model="two" />
</div>
2.1.1 运行结果

2.1.2. 结论
- 父组件是在
beforeMount创建子组件的 mounted是在最里面的子组件依次往上返回的
2.2. slot 父子组件
<!-- Root -->
<div id="app">
Root:
<input type="text" v-model="root" />
<One :root="root">
<!-- 作用域插槽 传递One的数据-->
<template v-slot="scope">
<Two :root="root" :one="scope.one" />
</template>
</One>
</div>
<!--One-->
<div>
<h1>One: {{root}}</h1>
<input type="text" v-model="one">
<slot></slot>
</div>
<!--Two -->
<div>
<h2>Two: {{root}} {{one}}</h2>
<input type="text" v-model="two" />
</div>
2.2.1. 运行结果

2.2.2. 结论
- 虽然写法不同了,但是层级关系还是一样的。输出也和之前相同
3. 从props、data、看生命周期
template直接使用slot的代码
// One 变动的代码
<template>
<!--给div 加上id-->
<div id="one">
<h1>One: {{root}}</h1>
<input type="text" v-model="one" />
<slot></slot>
</div>
</template>
beforeCreate() {
console.log('One beforeCreate')
console.log('One beforeCreate data', this.one)
// console.log('One props', this.root) // 如果访问 props上的属性会报错
},
created() {
console.log('One created')
console.log('One created data', this.one)
console.log('One created props', this.root)
},
beforeMount() {
console.log('One beforeMount')
console.log('One beforeMount DOM', window.one) // 直接通过DOM元素的id访问
},
mounted() {
console.log('One mounted')
console.log('One mounted DOM', window.one)
},
3.1. 运行结果

访问props内属性的报错信息

3.2. 结论
- 在
beforeCreate是访问不到props和data的 beforeCreate访问props上的属性导致报错,从报错信息看到已经代理到了undefined上,应该this.root被劫持了,然后 访问其实是this.props['root']就报错了- 子组件在
beforeMounted访问不到父组件的DOM节点 - 在
mounted就可以访问 DOM了,不管是自身的还是子组件的。
这里有一个反模式,通过
this.$children访问自身的子组件,效果是跟 访问data一样的,beforeCreate的时候返回空数组
3.3. 需要解决的疑惑
- 为什么
beforeCreate之后才能访问到data - 为什么 组件 One的
this.root访问会报错 mounted的触发顺序, 直觉是依次出栈然后 触发hook的,尝试揪出源码
3.3.1. beforeCreate 之后才能访问到data
源码debugger
// One
beforeCreate() {
debugger // 添加断点
console.log('One beforeCreate')
console.log('One beforeCreate data', this.one)
// console.log('One props', this.root) // 如果访问 props上的属性会报错
},


可以看到调用完 beforeCreate 这个钩子,才在 initState(vm) 把data和props绑定在对应的组件上的
3.3.2. 子组件props代理时机(即直接访问this.key获取的是this.props.key)
以One为例。 在 render One组件的时候(Root beforeMonut hook调用 之后)
- 创建
One的 virtualDOM - 根据 virtualDOM 创建Vue组件
function createComponent (Ctor,data,context,children,tag) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base; // 这里指的就是 Vue的这个构造函数
// 普通选项对象:将其转换为构造函数 (plain options object: turn it into a constructor)
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
// ... 其他代码
}
Vue.extend = function (extendOptions) {
// ... 其他代码
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps$1(Sub);
}
// ... 其他代码
}
function initProps$1 (Comp) {
var props = Comp.options.props;
for (var key in props) {
proxy(Comp.prototype, "_props", key);
}
}
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
所以在组件初始化之前(_init方法调用前),root已经挂到 One的原型上了
4. 组件更新
4.1. 父子组件 - 子组件渲染了父组件的数据
把data、props的打印清除掉,并且先不用 Two组件,现在我们只有Root,One
<!--Root-->
<div id="app">
Root:
<input type="text" v-model="root" />
<One :root="root">
<!-- <template v-slot="scope">
<Two :root="root" :one="scope.one" />
</template> -->
</One>
</div>
<!--One-->
<div id="one">
<h1>One: {{root}}</h1>
<input type="text" v-model="one" />
</div>
在 hook 中打印组件的HTML
beforeUpdate() {
console.log('root beforeUpdate', window.one.innerHTML)
},
updated() {
console.log('root update', window.one.innerHTML)
},

我们在One中使用了Root的数据
现在修改root的数据

打印结果

符合预期的两个都触发hook了
beforeUpdate拿到的是更新前的DOM
updated拿到的是更新后的DOM
只修改One 当然只会更新 One组件;
4.2. 父子组件 - 子组件的数据和父组件无关
代码就不展示了
即使One 的 props接收了Root的数据,没有依赖props,更新root也不会使 One 触发hook
4.3. 祖先和孙子元素 - solt组合
One依然不使用Root的数据
<!--Root-->
<div id="app">
Root:
<input type="text" v-model="root" />
<One :root="root">
<template v-slot="scope">
<Two :root="root" :one="scope.one" />
</template>
</One>
</div>
我们修改 Root的数据

One 也跟着触发hook了
4.3. 祖先和孙子元素 - 正常组合
代码,One不使用slot直接自身引入Two,中转Root的数据。One自身不渲染Root的数据

结论
- 祖先与超两层的组件传数据这种组合,祖先更新,会使整个链条都触发hook
4.4 父子组件 - 不传数据只传方法
<!--Root-->
<div id="app">
Root:
<input type="text" v-model="root" />
<One @root="handleRoot"></One>
</div>
<!--
handleRoot() {
this.root = 'one'
}
-->
<!--One-->
<div id="one">
<h1 @click="$emit('root')">One:</h1>
<input type="text" v-model="one" />
</div>
点击One的 h1.

只有root触发hook
4.5 祖孙组件 - 不传数据只传方法
<!--One-->
<div id="one">
<h1>One:</h1>
<input type="text" v-model="one" />
<Two @root="$emit('root')" />
</div>
<!--Two-->
<div id="two">
<h2 @click="$emit('root')">Two: </h2>
<input type="text" v-model="two" />
</div
点击 Two的h2

这里,组件没有数据变动,就没有触发hook,数据驱动更新。。。
4.6 Updated 的总结
这里的更新是指 hook的触发
而DOM的Repaint 只有在数据变动的节点出现
所以多层组件传值得消耗只有在js层面(创建组件,触发hook等)
destroyed
直接移除One,Two v-if 版
<!--Root-->
<div id="app">
Root:
<input type="text" v-model="root" />
<One v-if="root" :root="root"></One>
</div>
// One
data() {
console.log('One data')
return {
one: null
}
},
mounted() {
console.log('One mounted')
this.one = window.one
},
beforeDestroy() {
console.log('One beforeDestroy this.one', this.one.innerHTML)
console.log('One beforeDestroy window.one', window.one)
},
destroyed() {
console.log('One destroyed this.one', this.one.innerHTML)
console.log('One destroyed window.one', window.one)
}
// Two
props: {
root: String,
one: HTMLDivElement
},
data() {
console.log('Two data')
return {
two: 'two'
}
},
beforeDestroy() {
console.log('Two beforeDestroy this.one', this.one.innerHTML)
console.log('Two beforeDestroy this.two', this.two)
console.log('Two beforeDestroy window.two', window.two)
},
destroyed() {
console.log('Two destroyed this.one', this.one.innerHTML)
console.log('Two destroyed this.two', this.two)
console.log('Two destroyed window.two', window.two)
}
清空root

root是更新,子组件销毁完最后才触发 updated
在beforeDestroy DOM已经从页面移除了,但是还在组件的引用还在
这里分不出区别。。。
两个hook依然可以访问this,Two的 destroyed 也还能访问One
官网- destroyed 的说法是
Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
测试一下 One能不能在 destroyed 访问到Two
。。。

万脸懵逼,可能是浏览器(Chrome)的锅,经测试Firefox也是一样。
以防万一还是在beforeDestroy 做该做的吧