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 生命周期经历哪些阶段:
- 总体来说:初始化、运行中、销毁
- 详细来说:开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程
生命周期经历的阶段和钩子函数:
实例化 Vue(组件)对象:
new Vue()
初始化事件和生命周期
init events
和init cycle
beforeCreate 函数:
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
即此时 Vue(组件)对象被创建了,但是 Vue 对象的属性还没有绑定,如 data 属性,computed 属性还没有绑定,即没有值。
此时还没有数据和真实 DOM。
即:属性还没有赋值,也没有动态创建 template 属性对应的 HTML 元素(二阶段的createUI函数还没有执行)
挂载数据(属性赋值)
包括 属性和 computed 的运算
Created 函数:
Vue 对象的属性有值了,但是 DOM 还没有生成,$el 属性还不存在。
此时有数据了,但是还没有真实的 DOM
即:data,computed 都执行了。属性已经赋值,但没有动态创建 template 属性对应的 HTML 元素,所以,此时如果更改数据不会触发 updated 函数
如果:数据的初始值就来自后端,可以发送 ajax,或者 fetch 请求获取数据,但是,此时不会触发 updated 函数
检查
检查是否有 el 属性:检查 vue 配置,即
new Vue{}
里面的 el 项是否存在,有就继续检查 template 项。没有则等到手动绑定调用vm.el
的绑定。检查是否有 template 属性:检查配置中的 template 项,如果没有 template 进行填充被绑定区域,则被绑定区域的 el 对 outerHTML (即 整个 #app DOM对象,包括 和 标签)都作为被填充对象替换掉填充区域。
即:如果 vue 对象中有 template 属性,那么,template 后面的 HTML 会替换 $el 对应的内容。
如果有 render 属性,那么 render 就会替换 template。即:优先关系时: render > template > el
beforeMount 函数:
模板编译(template)、数据挂载(把数据显示在模板里)之前执行的钩子函数
此时
this.$el
有值,但是数据还没有挂载到页面上。即此时页面中的{ { } }
里的变量还没有被数据替换模板编译:用 vue 对象的数据(属性)替换模板中的内容
Mounted 函数:
模板编译完成,数据挂载完毕
即:此时已经把数据挂载到了页面上,所以,页面上能够看到正确的数据了。
一般来说,我们在此处发送异步请求(ajax,fetch,axios等),获取服务器上的数据,显示在 DOM 里。
beforeUpdate 函数:
组件更新之前执行的函数,只有数据更新后,才能调用(触发)beforeUpdate,注意:此数据一定是在模板上出现的数据,否则,不会,也没有必要触发组件更新(因为数据不出现在模板里,就没有必要再次渲染)
数据更新了,但是,vue(组件)对象对应的 dom 中的内部(innerHTML)没有变,所以叫作组件更新前
updated 函数:
组件更新之后执行的函数
vue(组件)对象对应的 dom 中的内部(innerHTML)改变了,所以,叫作组件更新之后
activated 函数:keep-alive 组件激活时调用
deactivated 函数:keep-alive 组件停用时调用
beforeDestroy:vue(组件)对象销毁之前
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 具体的生命周期示意图可以参见如下,理解了整个生命周期各个阶段的操作,关于生命周期相关的面试题就难不倒你了。
父组件可以监听到子组件的生命周期吗
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
1 |
|
以上需要手动通过 $emit
触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook
来监听即可,如下所示:
1 |
|
vue keep-alive
keep-alive 是 Vue 内置的一个组件,使用 <keep-alive></keep-alive>
包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染。
解析: 比如有一个列表和一个详情,那么用户就会经常执行 打开详情=>返回列表=>打开详情…
这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用 <keep-alive></keep-alive>
进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染
作用:
- 它能够把不活动的组件实例保存在内存中,而不是直接将其销毁。
- 它是一个抽象组件,不会被渲染到真实 DOM 中,也不会出现在父组件链中。
使用方式:
- 常用的两个属性
include/exclude
,允许组件有条件的进行缓存。两者都支持字符串或正则表达式,include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高; - 两个生命周期
activated/deactivated
,用来得知当前组件是否处于活跃状态。当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。 - keep-alive 的中还运用了 LRU(Least Recently Used)算法。
- 一般结合路由和动态组件一起使用,用于缓存组件。
原理:
Vue 的缓存机制并不是直接存储 DOM 结构,而是将 DOM 节点抽象成了一个个 VNode 节点,所以,keep-alive 的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。
其实就是将需要缓存的 VNode 节点保存在 this.cache 中,在 render 时,如果 VNode 的 name 符合在缓存条件(可以用 include 以及 exclude 控制),则会从 this.cache 中取出之前缓存的 VNode 实例进行渲染。
Vue 初始化过程
简单聊聊 new Vue 以后发生的事情
- new Vue 会调用 Vue 原型链上的
_init
方法对 Vue 实例进行初始化; - 首先是 initLifecycle 初始化生命周期,对 Vue 实例内部的一些属性(如 children、parent、isMounted)进行初始化;
- initEvents,初始化当前实例上的一些自定义事件(
Vue.$on
); - initRender,解析 slots 绑定在 Vue 实例上,绑定 createElement 方法在实例上;
- 完成对生命周期、自定义事件等一系列属性的初始化后,触发生命周期钩子 beforeCreate;
- initInjections,在初始化 data 和 props 之前完成依赖注入(类似于 React.Context);
- initState,完成对 data 和 props 的初始化,同时对属性完成数据劫持内部,启用监听者对数据进行监听(更改);
- initProvide,对依赖注入进行解析;
- 完成对数据(state 状态)的初始化后,触发生命周期钩子 created;
- 进入挂载阶段,将 vue 模板语法通过 vue-loader 解析成虚拟 DOM 树,虚拟 DOM 树与数据完成双向绑定,触发生命周期钩子beforeMount;
- 将解析好的虚拟 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 |
|