03-面试之前端 Vue 生命周期和钩子函数

谈谈你对 Vue 生命周期的理解?

(1)生命周期是什么?

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom、 渲染 -> 更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。

(2)各个生命周期的作用

生命周期 描述
beforeCreate (初始化界面前)组件实例被创建之初,组件的属性生效之前
created (初始化界面后)组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
beforeMount (渲染界面前)在挂载开始之前被调用:相关的 render 函数首次被调用
mounted (渲染界面后)el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate (更新数据前)组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update (更新数据后)组件数据更新之后
activited keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
beforeDestory (卸载组件前)组件销毁前调用
destoryed (卸载组件后)组件销毁后调用

(3)生命周期示意图

Vue 生命周期经历哪些阶段:

  1. 总体来说:初始化、运行中、销毁
  2. 详细来说:开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程

生命周期经历的阶段和钩子函数:

  1. 实例化 Vue(组件)对象:new Vue()

  2. 初始化事件和生命周期 init eventsinit cycle

  3. beforeCreate 函数:

    在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。

    即此时 Vue(组件)对象被创建了,但是 Vue 对象的属性还没有绑定,如 data 属性,computed 属性还没有绑定,即没有值。

    此时还没有数据和真实 DOM。

    即:属性还没有赋值,也没有动态创建 template 属性对应的 HTML 元素(二阶段的createUI函数还没有执行)

  4. 挂载数据(属性赋值)

    包括 属性和 computed 的运算

  5. Created 函数:

    Vue 对象的属性有值了,但是 DOM 还没有生成,$el 属性还不存在。

    此时有数据了,但是还没有真实的 DOM

    即:data,computed 都执行了。属性已经赋值,但没有动态创建 template 属性对应的 HTML 元素,所以,此时如果更改数据不会触发 updated 函数

    如果:数据的初始值就来自后端,可以发送 ajax,或者 fetch 请求获取数据,但是,此时不会触发 updated 函数

  6. 检查

    1. 检查是否有 el 属性:检查 vue 配置,即 new Vue{} 里面的 el 项是否存在,有就继续检查 template 项。没有则等到手动绑定调用 vm.el 的绑定。

    2. 检查是否有 template 属性:检查配置中的 template 项,如果没有 template 进行填充被绑定区域,则被绑定区域的 el 对 outerHTML (即 整个 #app DOM对象,包括 和 标签)都作为被填充对象替换掉填充区域。
      即:如果 vue 对象中有 template 属性,那么,template 后面的 HTML 会替换 $el 对应的内容。
      如果有 render 属性,那么 render 就会替换 template。即:优先关系时: render > template > el

  7. beforeMount 函数:

    模板编译(template)、数据挂载(把数据显示在模板里)之前执行的钩子函数

    此时 this.$el 有值,但是数据还没有挂载到页面上。即此时页面中的{ { } }里的变量还没有被数据替换

  8. 模板编译:用 vue 对象的数据(属性)替换模板中的内容

  9. Mounted 函数:

    模板编译完成,数据挂载完毕

    即:此时已经把数据挂载到了页面上,所以,页面上能够看到正确的数据了。

    一般来说,我们在此处发送异步请求(ajax,fetch,axios等),获取服务器上的数据,显示在 DOM 里。

  10. beforeUpdate 函数:

    组件更新之前执行的函数,只有数据更新后,才能调用(触发)beforeUpdate,注意:此数据一定是在模板上出现的数据,否则,不会,也没有必要触发组件更新(因为数据不出现在模板里,就没有必要再次渲染)

    数据更新了,但是,vue(组件)对象对应的 dom 中的内部(innerHTML)没有变,所以叫作组件更新前

  11. updated 函数:

    组件更新之后执行的函数

    vue(组件)对象对应的 dom 中的内部(innerHTML)改变了,所以,叫作组件更新之后

  12. activated 函数:keep-alive 组件激活时调用

  13. deactivated 函数:keep-alive 组件停用时调用

  14. beforeDestroy:vue(组件)对象销毁之前

  15. destroyed:vue(组件)销毁后

Vue 的父组件和子组件生命周期钩子函数执行顺序

Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:

  • 加载渲染过程:

    父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted

  • 子组件更新过程:

    父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated

  • 父组件更新过程:

    父 beforeUpdate -> 父 updated

  • 销毁过程:

    父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed

在哪个生命周期内调用异步请求,mounted 还是 created

最常用的是在 created 钩子函数中调用异步请求

一般来说,可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

在什么阶段才能访问操作 DOM

在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。vue 具体的生命周期示意图可以参见如下,理解了整个生命周期各个阶段的操作,关于生命周期相关的面试题就难不倒你了。

1.png

父组件可以监听到子组件的生命周期吗

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

1
2
3
4
5
6
7
// Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
this.$emit("mounted");
}

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},

// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
```

当然 `@hook` 方法不仅仅是可以监听 `mounted`,其它的生命周期事件,例如:`created`,`updated` 等都可以监听。


## vue 中 created 与 mounted 区别

* 在 created 阶段,实例已经被初始化,但是还没有挂载至 el 上,所以我们无法获取到对应的节点,但是此时我们是可以获取到 vue 中 data 与 methods 中的数据的;
* 在 mounted 阶段,vue 的 template 成功挂载在 `$el` 中,此时一个完整的页面已经能够显示在浏览器中,所以在这个阶段,可以调用节点了;

```vue
//以下为测试vue部分生命函数,便于理解
beforeCreate(){ //创建前
console.log('beforecreate:',document.getElementById('first'))//null
console.log('data:',this.text);//undefined
this.sayHello();//error:not a function
},
created(){ //创建后
console.log('create:',document.getElementById('first'))//null
console.log('data:',this.text);//this.text
this.sayHello();//this.sayHello()
},
beforeMount(){ //挂载前
console.log('beforeMount:',document.getElementById('first'))//null
console.log('data:',this.text);//this.text
this.sayHello();//this.sayHello()
},
mounted(){ //挂载后
console.log('mounted:',document.getElementById('first'))//<p></p>
console.log('data:',this.text);//this.text
this.sayHello();//this.sayHello()
}

vue keep-alive

keep-alive 是 Vue 内置的一个组件,使用 <keep-alive></keep-alive> 包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染。

解析: 比如有一个列表和一个详情,那么用户就会经常执行 打开详情=>返回列表=>打开详情… 这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用 <keep-alive></keep-alive> 进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染

作用:

  1. 它能够把不活动的组件实例保存在内存中,而不是直接将其销毁。
  2. 它是一个抽象组件,不会被渲染到真实 DOM 中,也不会出现在父组件链中。

使用方式:

  1. 常用的两个属性 include/exclude,允许组件有条件的进行缓存。两者都支持字符串或正则表达式,include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
  2. 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
  3. keep-alive 的中还运用了 LRU(Least Recently Used)算法。
  4. 一般结合路由和动态组件一起使用,用于缓存组件。

原理:

Vue 的缓存机制并不是直接存储 DOM 结构,而是将 DOM 节点抽象成了一个个 VNode 节点,所以,keep-alive 的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。

其实就是将需要缓存的 VNode 节点保存在 this.cache 中,在 render 时,如果 VNode 的 name 符合在缓存条件(可以用 include 以及 exclude 控制),则会从 this.cache 中取出之前缓存的 VNode 实例进行渲染。

Vue 初始化过程

简单聊聊 new Vue 以后发生的事情

  1. new Vue 会调用 Vue 原型链上的 _init 方法对 Vue 实例进行初始化;
  2. 首先是 initLifecycle 初始化生命周期,对 Vue 实例内部的一些属性(如 children、parent、isMounted)进行初始化;
  3. initEvents,初始化当前实例上的一些自定义事件(Vue.$on);
  4. initRender,解析 slots 绑定在 Vue 实例上,绑定 createElement 方法在实例上;
  5. 完成对生命周期、自定义事件等一系列属性的初始化后,触发生命周期钩子 beforeCreate;
  6. initInjections,在初始化 data 和 props 之前完成依赖注入(类似于 React.Context);
  7. initState,完成对 data 和 props 的初始化,同时对属性完成数据劫持内部,启用监听者对数据进行监听(更改);
  8. initProvide,对依赖注入进行解析;
  9. 完成对数据(state 状态)的初始化后,触发生命周期钩子 created;
  10. 进入挂载阶段,将 vue 模板语法通过 vue-loader 解析成虚拟 DOM 树,虚拟 DOM 树与数据完成双向绑定,触发生命周期钩子beforeMount;
  11. 将解析好的虚拟 DOM 树通过 vue 渲染成真实 DOM,触发生命周期钩子 mounted;

Vue 初始化过程中(new Vue(options))都做了什么?

  • 处理组件配置项;初始化根组件时进行了选项合并操作,将全局配置合并到根组件的局部配置上;初始化每个子组件时做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率;
  • 初始化组件实例的关系属性,比如 parent、children、root、refs 等
  • 处理自定义事件
  • 调用 beforeCreate 钩子函数
  • 初始化组件的 inject 配置项,得到 ret[key]=val 形式的配置对象,然后对该配置对象进行响应式处理,并代理每个 key 到 vm 实例上
  • 数据响应式,处理 props、methods、data、computed、watch 等选项
  • 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
  • 调用 created 钩子函数
  • 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount 方法,反之,没提供 el 选项则必须调用 $mount
  • 接下来则进入挂载阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++

// 如果是Vue的实例,则不需要被observe
vm._isVue = true

if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}

if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}

vm._self = vm

initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props

initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}

03-面试之前端 Vue 生命周期和钩子函数
https://flepeng.github.io/interview-20-开发语言类-21-frontend-03-Vue-03-面试之前端-Vue-生命周期和钩子函数/
作者
Lepeng
发布于
2023年8月8日
许可协议