聊聊 Vue3 是如何工作的

今天,我们从理论的层面聊一聊,Vue 是如何解析我们的代码与模板,将数据与视图绑定的。

之前一直将逻辑与实现混在一起,对于 Vue 初学者可能并不友好。所以接下来的教程,会将逻辑与分开,掘友们根据自己的需要理解阅读。

基础

示例

我们先以 Vue 官网的示例来讲解。

<script src="https://unpkg.com/vue@3"></script>

<div id="app">{{ message }}</div>

<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>

在这个示例中,我们进行了以下几步:

  1. 导入 Vue
  2. 编写 HTML
  3. 使用 Vue 的 createApp 方法,创建了一个 Vue 实例(vm)
  4. 调用 vmmount 方法,将页面的根DOM元素传过去

前两步就是正常的 HTML 代码,没什么好说的,接下来我们具体分析第三步和第四步

创建 vm

我们在使用 createApp 方法创建 vm 的时候,会传入一个配置对象,在示例中我们传入了一个函数属性 data,其返回一个包含 message 属性的对象。

在这一步中,Vue 会先创建一个渲染器,在这个渲染器中定义了将来操作节点/组件的所有方法(挂载 mount / 更新 patch / 移除 remove)

然后这个渲染器保存我们的配置项,返回一个 vm 示例。

绑定元素

我们获取 vm 后,调用 mount 方法,将我们准备的容器(<div id="app">)传递过去

这里传递一个字符串选择器,Vue内部会调用 document.querySelector 获取 DOM,我们也可以直接传递 DOM 元素

Vue 在获取到我们的根容器之后,就开始工作了。Vue 会创建一个根组件(对象),并将容器元素与之前保存的配置项整合到根组件身上。

然后进行第一次内容更新,在这一步,会挂载我们的根组件(mountComponent),并根据我们的配置项,生成响应式数据(props)与渲染函数(render),并创建一个组件更新函数(componentUpdateFn),此时页面中并没有内容渲染。

然后执行这个组件更新函数,其中会进行第二次内容更新,这一次会根据渲染函数,生成虚拟节点树(subTree),然后根据这颗虚拟节点树,生成真实DOM,将内容渲染到页面上。

至此初始化流程结束,在页面中能够看到 Hello Vue!

进阶

在上一节,我们只是粗略的讲解了 Vue 初始化的大致过程。

在这一节,我们将展开说明各个部分是如何运作的。

渲染器

Vue 创建 vm 时,会根据当前运行环境创建对应的渲染器,渲染器中的方法会因当前运行环境而发生改变。

一般我们在浏览器操作节点用的都是 document.XXX,而在其他环境中,如:SSR、移动应用、wx小程序等。这些环境中并没有 document 对象,而是使用其他方式操作元素节点。

因此,Vue 提供了接口,使用这些环境中特有的元素节点 API。

而渲染器中的这些方法,就是根据虚拟节点树的属性变化映射为真实的 DOM 元素操作,实现了节点/组件的挂载 mount / 更新 patch / 移除 remove

渲染函数

渲染函数指的就是创建虚拟节点的函数,在 Vue 中一共有四个来源:

  1. 如果组件中定义了 setup 配置项且返回值是一个函数,则会将其作为该组件的渲染函数;
  2. 如果组件中定义了 render 配置项,则将其作为渲染函数;
  3. 如果经过以上步骤还是没有渲染函数,检查有无 template 配置项,将其作为模板;
  4. 如果连 template 配置项也没有,则使用容器的 innerHTML 作为模板。

如果通过前两种方式获取了渲染函数,可以直接使用;而通过后两种方式获得的模板,还需要将模板字符串编译为可执行的渲染函数。

编译的过程非常复杂,我就简单说一下。Vue 会先解析字符串生成抽象语法数(AST),再设置其中的动态属性/节点,然后生成函数字符串,最后通过 new Function 生成真实的渲染函数。

前文所举的例子,就是通过第四种方式生成的渲染函数;而我们平时写的单文件组件,会被 Vite/Vue CLI 提前编译成一个包含 setuprender 属性的对象,再传递给 Vue 使用。

数据响应式

先简单说一下什么是响应式?

比如我们有数据 A 和函数 B,我们希望每次 A 变化后 B 能够自动执行,这就是响应式。其中 A 称为响应式数据,B 称为回调函数。

而在 Vue 中,响应式数据就是我们通过特殊语法声明并且模板中使用到的数据,而回调函数就是页面视图的更新(即将要讲的组件更新函数)。

我们传给 Vue 的响应式数据,一般有两种形式:

  • 组合式写法,我们在 setup 中使用 reactive ref 等 api 定义响应式数据并返回,交给 Vue 直接使用;
  • 选项式写法,我们传入 data 配置项,Vue 内部调用 reactive 将其变为响应式并绑定到 vm(this) 身上。

还有一种响应式数据是通过父组件传给子组件的 props,Vue 会将其变为浅层只读响应式(shallowReadonly)传递给 setup 函数,也绑定到 this 身上。

关于响应式原理的实现我之前已经总结过——万字细说 Vue3 响应式原理

组件更新函数

每次页面的更新,都是以组件为单位进行的

Vue 在挂载组件时,会定义该组件的更新函数,首次执行是挂载,之后再执行为更新,那你可以简单地将其理解为整颗树推到重建。实际 Vue 内部做了很多优化去尽可能地复用 DOM 节点,这里不展开介绍。

Vue 的组件更新很智能,只有在该组件渲染函数/模板中用到的响应式数据改变时,这个组件更新函数才会去执行。

比如一些响应式数据定义在根组件中,但根组件模板中并没有使用,而是作为 props 传给子组件使用,这样在这些数据修改时,就只有子组件的更新函数会执行。

而子组件中定义的数据,父组件是没法访问的,自然就不可能使用,在改变时也只会更新子组件。

但如果是父组件用到的数据修改了,虽然子组件不需要更新,但依旧会进行一次数据的比较。

聊聊 Vue3 是如何工作的

组件生命周期

我们平时还会定义组件的声明周期

  • 选项式写法配置 mounted 等属性
  • 组合式写法在 setup 中调用 onMounted 等函数。

Vue 将这些生命周期函数采集到组件对应的 hooks 中,在执行组件更新函数中,挂载/更新组件的前后时刻调用。

beforeCreatecreated 在配置项数据解析前后就已经调用了。

beforeUnmountunmount 在组件卸载前后调用。

结语

本文只是带领大家简单的过一遍 Vue 的工作流程,其中有很多的细节与分支并没有介绍。比如:异步组件、diff 算法、虚拟节点/组件动画的生命周期等等。

这些东西,以后再陆续出文介绍吧。

如果喜欢或有所帮助的话,希望能点赞关注,鼓励一下作者。

如果文章有不正确或存疑的地方,欢迎评论指出。