聊聊 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>
在这个示例中,我们进行了以下几步:
- 导入 Vue
- 编写 HTML
- 使用 Vue 的
createApp
方法,创建了一个 Vue 实例(vm
) - 调用
vm
的mount
方法,将页面的根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 中一共有四个来源:
- 如果组件中定义了
setup
配置项且返回值是一个函数,则会将其作为该组件的渲染函数; - 如果组件中定义了
render
配置项,则将其作为渲染函数; - 如果经过以上步骤还是没有渲染函数,检查有无
template
配置项,将其作为模板; - 如果连
template
配置项也没有,则使用容器的innerHTML
作为模板。
如果通过前两种方式获取了渲染函数,可以直接使用;而通过后两种方式获得的模板,还需要将模板字符串编译为可执行的渲染函数。
编译的过程非常复杂,我就简单说一下。Vue 会先解析字符串生成抽象语法数(AST),再设置其中的动态属性/节点,然后生成函数字符串,最后通过 new Function
生成真实的渲染函数。
前文所举的例子,就是通过第四种方式生成的渲染函数;而我们平时写的单文件组件,会被 Vite/Vue CLI 提前编译成一个包含 setup
和 render
属性的对象,再传递给 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
传给子组件使用,这样在这些数据修改时,就只有子组件的更新函数会执行。
而子组件中定义的数据,父组件是没法访问的,自然就不可能使用,在改变时也只会更新子组件。
但如果是父组件用到的数据修改了,虽然子组件不需要更新,但依旧会进行一次数据的比较。
组件生命周期
我们平时还会定义组件的声明周期
- 选项式写法配置
mounted
等属性 - 组合式写法在
setup
中调用onMounted
等函数。
Vue 将这些生命周期函数采集到组件对应的 hooks
中,在执行组件更新函数中,挂载/更新组件的前后时刻调用。
beforeCreate
和 created
在配置项数据解析前后就已经调用了。
而 beforeUnmount
和 unmount
在组件卸载前后调用。
结语
本文只是带领大家简单的过一遍 Vue 的工作流程,其中有很多的细节与分支并没有介绍。比如:异步组件、diff 算法、虚拟节点/组件动画的生命周期等等。
这些东西,以后再陆续出文介绍吧。
如果喜欢或有所帮助的话,希望能点赞关注,鼓励一下作者。
如果文章有不正确或存疑的地方,欢迎评论指出。
转载自:https://juejin.cn/post/7135290464319733768