We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
相信大家对 Vue 有哪些生命周期早就已经烂熟于心,但是对于这些生命周期的前后分别做了哪些事情,可能还有些不熟悉。
本篇文章就从一个完整的流程开始,详细讲解各个生命周期之间发生了什么事情。
注意本文不涉及 keep-alive 的场景和错误处理的场景。
keep-alive
从 new Vue(options) 开始作为入口,Vue 只是一个简单的构造函数,内部是这样的:
new Vue(options)
Vue
function Vue (options) { this._init(options) }
进入了 _init 函数之后,先初始化了一些属性,然后开始第一个生命周期:
_init
callHook(vm, 'beforeCreate')
beforeCreate 之后
beforeCreate
inject
state
props
methods
data
computed
watch
provide
所以在 data 中可以使用 props 上的值,反过来则不行。
然后进入 created 阶段:
created
callHook(vm, 'created')
调用 $mount 方法,开始挂载组件到 dom 上。
$mount
dom
如果使用了 runtime-with-compile 版本,则会把你传入的 template 选项,或者 html 文本,通过一系列的编译生成 render 函数。
runtime-with-compile
template
html
render
ast
对应具体的代码就是:
const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options)
如果是脚手架搭建的项目的话,这一步 vue-cli 已经帮你做好了,所以就直接进入 mountComponent 函数。
vue-cli
mountComponent
那么,确保有了 render 函数后,我们就可以往渲染的步骤继续进行了
渲染
把 渲染组件的函数 定义好,具体代码是:
渲染组件的函数
updateComponent = () => { vm._update(vm._render(), hydrating) }
拆解来看,vm._render 其实就是调用我们上一步拿到的 render 函数生成一个 vnode,而 vm._update 方法则会对这个 vnode 进行 patch 操作,帮我们把 vnode 通过 createElm函数创建新节点并且渲染到 dom节点 中。
vm._render
vnode
vm._update
patch
createElm
dom节点
接下来就是执行这段代码了,是由 响应式原理 的一个核心类 Watcher 负责执行这个函数,为什么要它来代理执行呢?因为我们需要在这段过程中去 观察 这个函数读取了哪些响应式数据,将来这些响应式数据更新的时候,我们需要重新执行 updateComponent 函数。
响应式原理
Watcher
观察
updateComponent
如果是更新后调用 updateComponent 函数的话,updateComponent 内部的 patch 就不再是初始化时候的创建节点,而是对新旧 vnode 进行 diff,最小化的更新到 dom节点 上去。具体过程可以看我的上一篇文章:
diff
为什么 Vue 中不要用 index 作为 key?(diff 算法详解)
这一切交给 Watcher 完成:
new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
注意这里在before 属性上定义了beforeUpdate 函数,也就是说在 Watcher 被响应式属性的更新触发之后,重新渲染新视图之前,会先调用 beforeUpdate 生命周期。
before
beforeUpdate
关于 Watcher 和响应式的概念,如果你还不清楚的话,可以阅读我之前的文章:
手把手带你实现一个最精简的响应式系统来学习Vue的data、computed、watch源码
注意,在 render 的过程中,如果遇到了 子组件,则会调用 createComponent 函数。
子组件
createComponent
createComponent 函数内部,会为子组件生成一个属于自己的构造函数,可以理解为子组件自己的 Vue 函数:
构造函数
Ctor = baseCtor.extend(Ctor)
在普通的场景下,其实这就是 Vue.extend 生成的构造函数,它继承自 Vue 函数,拥有它的很多全局属性。
Vue.extend
这里插播一个知识点,除了组件有自己的生命周期外,其实 vnode 也是有自己的 生命周期的,只不过我们平常开发的时候是接触不到的。
生命周期
生命周期的
那么子组件的 vnode 会有自己的 init 周期,这个周期内部会做这样的事情:
子组件的 vnode
init
// 创建子组件 const child = createComponentInstanceForVnode(vnode) // 挂载到 dom 上 child.$mount(vnode.elm)
而 createComponentInstanceForVnode 内部又做了什么事呢?它会去调用 子组件 的构造函数。
createComponentInstanceForVnode
new vnode.componentOptions.Ctor(options)
构造函数的内部是这样的:
const Sub = function VueComponent (options) { this._init(options) }
这个 _init 其实就是我们文章开头的那个函数,也就是说,如果遇到 子组件,那么就会优先开始子组件的构建过程,也就是说,从 beforeCreated 重新开始。这是一个递归的构建过程。
beforeCreated
也就是说,如果我们有 父 -> 子 -> 孙 这三个组件,那么它们的初始化生命周期顺序是这样的:
父 -> 子 -> 孙
父 beforeCreate 父 create 父 beforeMount 子 beforeCreate 子 create 子 beforeMount 孙 beforeCreate 孙 create 孙 beforeMount 孙 mounted 子 mounted 父 mounted
然后,mounted 生命周期被触发。
mounted
到此为止,组件的挂载就完成了,初始化的生命周期结束。
当一个响应式属性被更新后,触发了 Watcher 的回调函数,也就是 vm._update(vm._render()),在更新之前,会先调用刚才在 before 属性上定义的函数,也就是
vm._update(vm._render())
callHook(vm, 'beforeUpdate')
注意,由于 Vue 的异步更新机制,beforeUpdate 的调用已经是在 nextTick 中了。 具体代码如下:
nextTick
nextTick(flushSchedulerQueue) function flushSchedulerQueue { for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { // callHook(vm, 'beforeUpdate') watcher.before() } } }
然后经历了一系列的 patch、diff 流程后,组件重新渲染完毕,调用 updated 钩子。
updated
注意,这里是对 watcher 倒序 updated 调用的。
watcher
也就是说,假如同一个属性通过 props 分别流向 父 -> 子 -> 孙 这个路径,那么收集到依赖的先后也是这个顺序,但是触发 updated 钩子确是 孙 -> 子 -> 父 这个顺序去触发的。
孙 -> 子 -> 父
function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted) { callHook(vm, 'updated') } } }
至此,渲染更新流程完毕。
在刚刚所说的更新后的 patch 过程中,如果发现有组件在下一轮渲染中消失了,比如 v-for 对应的数组中少了一个数据。那么就会调用 removeVnodes 进入组件的销毁流程。
v-for
removeVnodes
removeVnodes 会调用 vnode 的 destroy 生命周期,而 destroy 内部则会调用我们相对比较熟悉的 vm.$destroy()。(keep-alive 包裹的子组件除外)
destroy
vm.$destroy()
这时,就会调用 callHook(vm, 'beforeDestroy')
callHook(vm, 'beforeDestroy')
之后就会经历一系列的清理逻辑,清除父子关系、watcher 关闭等逻辑。但是注意,$destroy 并不会把组件从视图上移除,如果想要手动销毁一个组件,则需要我们自己去完成这个逻辑。
清理
$destroy
然后,调用最后的 callHook(vm, 'destroyed')
callHook(vm, 'destroyed')
至此为止,Vue 的生命周期我们就完整的回顾了一遍。知道各个生命周期之间发生了什么事,可以让我们在编写 Vue 组件的过程中更加胸有成竹。
希望这篇文章对你有帮助。
1.如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我创作的动力。
2.关注公众号「前端从进阶到入院」即可加我好友,我拉你进「前端进阶交流群」,大家一起共同交流和进步。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
前言
相信大家对 Vue 有哪些生命周期早就已经烂熟于心,但是对于这些生命周期的前后分别做了哪些事情,可能还有些不熟悉。
本篇文章就从一个完整的流程开始,详细讲解各个生命周期之间发生了什么事情。
注意本文不涉及
keep-alive
的场景和错误处理的场景。初始化流程
new Vue
从
new Vue(options)
开始作为入口,Vue
只是一个简单的构造函数,内部是这样的:进入了
_init
函数之后,先初始化了一些属性,然后开始第一个生命周期:beforeCreate被调用完成
beforeCreate
之后inject
state
props
methods
data
computed
watch
provide
所以在
data
中可以使用props
上的值,反过来则不行。然后进入
created
阶段:created被调用完成
调用
$mount
方法,开始挂载组件到dom
上。如果使用了
runtime-with-compile
版本,则会把你传入的template
选项,或者html
文本,通过一系列的编译生成render
函数。template
,生成ast
抽象语法树。ast
,标记静态节点。(渲染过程中不会变的那些节点,优化性能)。ast
,生成render
函数。对应具体的代码就是:
如果是脚手架搭建的项目的话,这一步
vue-cli
已经帮你做好了,所以就直接进入mountComponent
函数。那么,确保有了
render
函数后,我们就可以往渲染
的步骤继续进行了beforeMount被调用完成
把
渲染组件的函数
定义好,具体代码是:拆解来看,
vm._render
其实就是调用我们上一步拿到的render
函数生成一个vnode
,而vm._update
方法则会对这个vnode
进行patch
操作,帮我们把vnode
通过createElm
函数创建新节点并且渲染到dom节点
中。接下来就是执行这段代码了,是由
响应式原理
的一个核心类Watcher
负责执行这个函数,为什么要它来代理执行呢?因为我们需要在这段过程中去观察
这个函数读取了哪些响应式数据,将来这些响应式数据更新的时候,我们需要重新执行updateComponent
函数。如果是更新后调用
updateComponent
函数的话,updateComponent
内部的patch
就不再是初始化时候的创建节点,而是对新旧vnode
进行diff
,最小化的更新到dom节点
上去。具体过程可以看我的上一篇文章:为什么 Vue 中不要用 index 作为 key?(diff 算法详解)
这一切交给
Watcher
完成:注意这里在
before
属性上定义了beforeUpdate
函数,也就是说在Watcher
被响应式属性的更新触发之后,重新渲染新视图之前,会先调用beforeUpdate
生命周期。关于
Watcher
和响应式的概念,如果你还不清楚的话,可以阅读我之前的文章:手把手带你实现一个最精简的响应式系统来学习Vue的data、computed、watch源码
注意,在
render
的过程中,如果遇到了子组件
,则会调用createComponent
函数。createComponent
函数内部,会为子组件生成一个属于自己的构造函数
,可以理解为子组件自己的Vue
函数:在普通的场景下,其实这就是
Vue.extend
生成的构造函数,它继承自Vue
函数,拥有它的很多全局属性。这里插播一个知识点,除了组件有自己的
生命周期
外,其实vnode
也是有自己的生命周期的
,只不过我们平常开发的时候是接触不到的。那么
子组件的 vnode
会有自己的init
周期,这个周期内部会做这样的事情:而
createComponentInstanceForVnode
内部又做了什么事呢?它会去调用子组件
的构造函数。构造函数的内部是这样的:
这个
_init
其实就是我们文章开头的那个函数,也就是说,如果遇到子组件
,那么就会优先开始子组件
的构建过程,也就是说,从beforeCreated
重新开始。这是一个递归的构建过程。也就是说,如果我们有
父 -> 子 -> 孙
这三个组件,那么它们的初始化生命周期顺序是这样的:然后,
mounted
生命周期被触发。mounted被调用完成
到此为止,组件的挂载就完成了,初始化的生命周期结束。
更新流程
当一个响应式属性被更新后,触发了
Watcher
的回调函数,也就是vm._update(vm._render())
,在更新之前,会先调用刚才在before
属性上定义的函数,也就是注意,由于 Vue 的异步更新机制,
beforeUpdate
的调用已经是在nextTick
中了。具体代码如下:
beforeUpdate被调用完成
然后经历了一系列的
patch
、diff
流程后,组件重新渲染完毕,调用updated
钩子。注意,这里是对
watcher
倒序updated
调用的。也就是说,假如同一个属性通过
props
分别流向父 -> 子 -> 孙
这个路径,那么收集到依赖的先后也是这个顺序,但是触发updated
钩子确是孙 -> 子 -> 父
这个顺序去触发的。updated被调用完成
至此,渲染更新流程完毕。
销毁流程
在刚刚所说的更新后的
patch
过程中,如果发现有组件在下一轮渲染中消失了,比如v-for
对应的数组中少了一个数据。那么就会调用removeVnodes
进入组件的销毁流程。removeVnodes
会调用vnode
的destroy
生命周期,而destroy
内部则会调用我们相对比较熟悉的vm.$destroy()
。(keep-alive 包裹的子组件除外)这时,就会调用
callHook(vm, 'beforeDestroy')
beforeDestroy被调用完成
之后就会经历一系列的
清理
逻辑,清除父子关系、watcher
关闭等逻辑。但是注意,$destroy
并不会把组件从视图上移除,如果想要手动销毁一个组件,则需要我们自己去完成这个逻辑。然后,调用最后的
callHook(vm, 'destroyed')
destroyed被调用完成
总结
至此为止,Vue 的生命周期我们就完整的回顾了一遍。知道各个生命周期之间发生了什么事,可以让我们在编写 Vue 组件的过程中更加胸有成竹。
希望这篇文章对你有帮助。
❤️感谢大家
1.如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我创作的动力。
2.关注公众号「前端从进阶到入院」即可加我好友,我拉你进「前端进阶交流群」,大家一起共同交流和进步。
The text was updated successfully, but these errors were encountered: