Vue.js的运行机制与生命周期

前言

前面第二章,我们讲述了 Reactive 响应式数据是如何去实现双向绑定的,但是却没有去叙述Effect最终是如何去完成视图更新的,

这里我们将结合 Vue.js 的生命周期,从应用构建开始去更加完整的讲述一个 Vue.js 应用是如何完成构建并运行的。

本章我们较为详细的介绍了 Vue.js 在运行中所遇到的一些核心模块功能,全文较为冗长,还请慢品o(╯□╰)o。

Vue.js应用的生命周期

首先,我们来看下 Vue 官方所给出的生命周期示意图,这里我们将整个生命周期简单的分为四个阶段:

  1. 应用构建阶段

  2. 模板编译阶段

  3. 运行阶段

  4. 卸载阶段

本篇文章,我们将去讲述 Vue.js 在应用构建阶段与运行阶段的一些操作与细节,以期为读者对 Vue 的运行机制带来一个更加深入的了解。

createApp创建应用

Vue.js 3.0Vue 使用createApp函数来替代了原有的构造函数来创建应用,那么让我们首先来看下 Vue 在这里做了那些事情:

构建应用上下文环境

在这里,我们得到了一个 Vue.js 应用的基本骨架,它包含基本的应用、配置、component等属性。

创建应用主体

createApp函数的第二步,主要是导出了mount函数,在这里 Vue 首先为组件构造了 vnode,这是整个Virtual DOM构建的的第一步:

“创建vnode节点”,之后我们开始执行render函数正式开始我们的应用构建。

BeforeCreate与Created生命周期

Vue 将原有的 Vue 2.0 的构造函数移动到applyOptions函数中,以兼容原有的功能 API ,在这里 Vue 首先会调用 beforeCreate 钩子,

在完成数据响应式处理、函数包装、计算属性以及观察器等参数初始化之后, Vue 完成了应用组件的创建,

开始调用created钩子函数完成整个应用的初始化操作。

patch与virtual dom构建

什么是patch?

Vue.js 完成了应用的构建之后,我们得到了组件应用的virtual dom,众所周知virtual domMVVM框架中是十分重要的一环,

virtual dom是应用组件向实际HTML节点渲染的中间体,也是MVVM框架对于整个模板渲染性能优化的核心,

而其中patch则是负责virtual dom的基础节点Vnode的创建与更新,patch具有如下功能:

  1. 创建或删除Vnode节点

  2. 更新Vnode节点

patch源码一览

在这里patch接收了两个node节点,n1是旧节点、n2是新节点,当新旧节点不同时,旧的节点将会被直接卸载,

当处于BAIL模式时,性能优化将会被关闭,因为此时是初次进入,接下来patch将会依照不同的类型执行对应的操作。

当处理元素节点时,patch会去执行两步操作:

  1. 比较元素节点

  2. 更新节点数据数据

对于元素的更新将会分为以下几种具体情况:

当执行完patchVue 将会调用beforeMount钩子,并开始进行模板编译。

diff算法

Vuediff算法是用来决定vnode节点是否需要更新的判断逻辑,这里我们简单阐述一下diff算法的实现逻辑。

首先 Vue 会执行两个前置操作以便于后续算法执行:

  1. vnode节点打上key标签将节点分为带key标签的与不带key标签的两种

  2. vnode节点树折平成一维数据,因为队列的遍历在执行效率上优于树形的遍历

而具体的算法执行则主要分为带key值与不带key值的两种:

无key值情况

此种情况下的判断最为简单,因为再次之前 Vue 已经将节点树进行树遍历,并依序排列完成,

因此此时仅需要取得新旧两个节点树的公共长度commonLength = Math.min(old.length, new.length)

然后将公共节点之外的节点进行删除或者新增即可。

有key值情况

此时情况相比之前的便要复杂得多,这里主要分为四种不同的情形:

  1. 前序相同

  2. 后序相同

  3. 中序相同

  4. 乱序排序

对于前面三种类型而言,其本质与之前的无key值排序相差无多,最为核心的是在于乱序排序这种情况,

Vue 首先会尽可能的将节点不断的重复前面三种判断,最大限度的找寻出其最大公约数,在此之后我们所剩下的便是最后的乱序节点处理了。

而对于乱序节点而言 Vue 采用了以下不同策略的方式来实现其更新:

  1. 构建key:index映射关系,比如常见的v-for指令中key,利用key值的变化判断其是否需要更新

  2. 遍历数组,更新新旧vnode数组中相同的节点,同时卸载不再使用的废弃节点

  3. 新旧节点数组中存在可重复使用的交叉节点,将重复的交叉节点进行位移操作,减少操作次数。

compile模板编译

在正式开始介绍compile编译器之前,我们先介绍一下 Vuebeforemountmounted生命周期钩子,之后我们开始阐述compile编译器概念及其基本机制。

BeforeMount与Mounted生命周期

对于模板的编译 Vue 实际上在执行beforemount生命周期钩子之前就已经完成了,

对于beforemountmounted钩子而言他们的区别仅仅在于将node节点挂载到container上面而言。

什么是compile编译器?

编译器是一个非常宽泛的概念,从广义上来讲编译器会将某种编程语言写成的源代码(原始语言)转换成另一种编程语言,其主要目的是将人类能够阅读的语法翻译成机器能够阅读的语言。

在编译原理一书中完整的编译器包括语法、词法定义以及自动机、语法制导翻译、词法解析、语法分析树等模块,而从过程角度来说则分为:

文法定义->词法分析->语法分析->语法分析树构建->语法制导翻译->代码优化->最终代码生成,这几个步骤。

无论何种编译器都离不开语法分析,中间件构建,代码生成这三个环节, Vue.js 的编译器亦是如此。

语法分析

compile的语法分析由baseParse函数完成,语法分析主要分为两步:

  1. 解析模板构建AST语法抽象树

  2. 处理style、class、diretive等辅助功能

中间件-AST语法树

AST语法分析树具体三种阶段:

  1. 初始阶段,此时的AST语法分析树由前置的语法分析构建而成,此时它仅由基本的vnode节点组成,比较粗糙。

  2. 之后AST语法分析树将会经过优化标记为带key与不带key的节点,以便于我们前面提到过的diff算法的实现

  3. 最终阶段,此时的AST语法分析树再次经过处理,生成可执行代码,例如 _c、_l 之类的。

代码生成

最后compile将会整个编译所得到的结果打包成一个render函数,以便于后续mount操作的执行。

Vue.js是如何运行的?

在经过了应用的创建与挂载之后,我们得到了完整的可运行的 Vue.js 应用,此时我们便到了本章的最后一个模块,应用是如何运行的?

如果说将Scheduler比作一个车站的调度室,那么Effect则是负责拖运货物的列车,而车上拖运的货物之一便是组件的刷新函数render

组件是如何去更新的?

我们在前面的 Reactivity 模块时便已经提过,Dep是整个 Vue.js 工厂的搬运工,它们负责将货物装载到Effect这趟列车,

然后由EffectScope这个列车长通知调度室Scheduler来发车,将货物运送到工厂由SetupRenderEffectFn来真正执行组件的更新,这个步骤共分为:

  1. Dep获取依赖的更新,通知Effect数据需要更新

  2. Effect收集依赖的更新数据情况将之汇总

  3. EffectScope这个总列车头来判断哪些Effect需要装载更新

  4. 调用Scheduler将本次组件更新的全部操作推入下一个微任务队列中

  5. 执行setupRenderEffect函数完成组件更新,在下一个微任务队列中调用updated钩子,通知组件更新完成

SetupRenderEffectFn组件更新函数

SetupRenderEffectFn函数是实际负责组件更新的地方,在这里 Vue 会将新旧节点树进行diff对比生成新的节点树,

并为此次更新创建一个新的effect,在绑定作用域之后调用effect.run开始执行更新操作,最后在下一个微任务队列中执行updated钩子。

BeforeUpdate与Updated生命周期

BeforeUpdate钩子调用于组件刷新之前,值得一提的是Updated钩子函数并非直接调用与patch更新之后,而是执行在下一个微任务队列,

这是因为patch函数执行完成后,实际DOM的更新是在下一个宏任务中执行,因此需要在下一个微任务队列中执行 Updated钩子才能确保此时组件已完全实现更新。

总结

本章,我们较为详细的介绍了一个完整的 Vue.js 应用从应用主体创建、AST虚拟DOM搭建、模板编译最后到应用运行的过程,

并介绍了各个模块中的核心部分patchdiff算法等,同时我们也提到了一些同样重要的辅助模块如:depeffectscheduler等,

以期让读者对于整个 Vue.js 的运行有着更加深刻的体会。本章比较冗长同时也是整个 Vue3 源码解析系列 中最为重要的环节,

接下来我们将开始详细介绍 Vue 的一些核心的辅助模块,之后我们会开始详细的讲解 Vue 的编译器是如何去实现的。

文献参考

Vue3 source code analysis (5): Patch algorithm

Vue3 framework principle realization (three)-patch

graphical-analysis-of-vue3-diff-algorithm.html

Compiler principle and optimization strategy in vue3

Analysis of Vue compiling principle

编译原理第二版 [Compilers:Principle,Techniques and Tools]

Last updated