相关概念:
命令式 VS 声明式
从范式上来看,视图层框架通常分为:
-
命令式框架
- 更加关注过程,代码本身描述的是"做事的过程",符合逻辑直觉
dart// 自然语言描述能够与代码产生一一对应的关系 // 示例: const div = document.querySelector('#app') // 获取div div.innerText = 'hello world'// 设置文本内容 div.addEventListener('click', () => { alert('ok') }) // 绑定点击事件 -
声明式框架
- 更加关注结果,主要是提升代码的可维护性
less// 用户提供一个"预期的结果",中间的过程由vue.js实现 // 示例 <div @click="() => alert('ok')">hello world</div>- 更新时性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
因为声明式框架在更新时比命令式框架多了"找出差异 "的过程,所以声明式代码的性能不会优于命令式代码的性能。而对比命令式代码,声明式代码又具有更强的可维护性,更加的直观。所以框架要做的就是:在保持可维护性的同时让性能损失最小化。
在开发过程中,原生JS操作DOM,虚拟DOM和innerHTML三者操作页面的性能都与创建页面、更新页面,页面大小、变更部分的大小有关系,选择哪种更新策略,需要结合心智负担、可维护性等因素综合考虑。
性能对比
| 更新策略 | 心智负担 | 可维护性 | 性能 | 适用场景 |
|---|---|---|---|---|
原生JS |
高 | 低 | 最高 | 简单页面 |
| 虚拟DOM | 中 | 高 | 中 | 复杂应用 |
innerHTML |
低 | 中 | 低 | 静态内容 |
运行时 VS 编译时
以上文中声明式框架示例代码为例,简单描述
vue.js的渲染过程:1、通过编译器【compile】 解析模版字符串识别到需要创建一个DOM元素,设置内容为
hello world,并为其绑定一个点击事件,完成后输出一个虚拟DOM【即一个描述真实DOM的js对象】2、通过渲染函数【render】 将虚拟DOM渲染成真实的DOM树挂载到指定元素上,完成渲染
当设计一个框架的时候,有三种选择
- 纯运行时
- 上面提到的如果只用渲染函数 ,由用户直接提供虚拟DOM作为入参,就是所谓的纯运行时框架
- 没有编译过程,也就无法添加相关的优化手段,比如tree-shaking
- 运行时 + 编译时
- 代码运行时由编译器将语义化代码编译成目标数据并作为渲染函数的入参,这种操作就是 运行时编译框架。它既支持运行时【即用户直接提供数据对象】,又支持编译时【即将用户语义化代码编译为目标数据】
- 由于代码运行时才开始编译会产生一定的性能开销,因此可以在构建时就执行编译操作,以提升性能。【在 Vue 3.5.22 中,运行时编译通过
@vue/compiler-dom实现,构建时编译通过@vitejs/plugin-vue实现】
- 纯编译时
- 如果省略上面的渲染函数,直接将用户代码通过编译器完成真实DOM的渲染,就是一个纯编译时框架。即不支持任何运行时内容。
- 由于不需要任何运行时,而是直接将代码编译成可执行的
js代码,因为性能可能会更好,但是有损灵活性。
Vue.js就是内部封装了命令式代码从而实现的面向用户的声明式框架;是运行时+编译时架构,目的在于保持灵活性的基础上尽可能的优化性能
其中组件的实现依赖于渲染器 ,组件中模板的编译依赖于编译器 。虚拟DOM作为媒介在整个渲染过程中作为组件真实DOM的载体协助实现内容渲染和更新。
虚拟DOM【vnode】
虚拟DOM 是一个用来描述真实DOM的js对象。
使用虚拟DOM的好处是可以将不同类型的标签、属性及子节点抽象成一个对象,这样描述UI可以更加灵活。
javascript
// 上文中的代码可以用以下形式表示
const vnode= {
// 标签名称
tag: 'div',
// 标签属性
props: {
onClick: () =>alert('ok')
},
// 子节点
children: 'hello world'
}
vue中的h函数就是一个辅助创建虚拟DOM的工具函数
typescript
import { h } from 'vue'
export default {
render() {
return h('div', { onClick: () => alert('ok') }, 'hello world')
}
}
// 等价于
export default {
render() {
return {
tag: 'div',
props: {
onClick: () => alert('ok')
},
children: 'hello world'
}
}
}
// 等价于
<div @click="() => alert('ok')">hello world</div>
虚拟DOM的性能优势:
- 批量更新:可以将多次DOM操作合并为一次
- 跨平台:同一套代码可以渲染到不同平台
- 优化策略:通过
diff算法最小化DOM操作
组件
组件就是一组DOM元素的封装,它可以是一个返回虚拟DOM的函数,也可以是一个对象。组件的返回值也是虚拟DOM,它代表组件要渲染的内容。
编译器【compile】
编译器的作用是将组件模板【<template>】编译为渲染函数并添加到<script>标签块的组件对象上
xml
// demo.vue
<template>
<div@click="handler">
hello world
</div>
</template>
<script>
exportdefault {
data() { }
methods: {
handler: () =>alert('ok')
}
}
</script>
组件编译后结果:
javascript
exportdefault {
data() {},
methods: {
handler: () =>alert('ok')
},
render() {
return _createElementVNode('div', { onClick: handler }, 'hello world', -1/* HOISTED */)
}
}
无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的。然后再将渲染函数返回的虚拟DOM作为渲染器的入参,进行真实DOM的渲染
Vue3的编译优化:
- 静态提升:将静态内容提升到渲染函数外部
- 补丁标记:为动态内容添加标记,优化
diff过程【通过在虚拟DOM中添加标记实现】 tree-shaking:移除未使用代码
渲染器【renderer】
渲染器的作用就是递归遍历虚拟DOM对象,并调用原生DOM API来完成真实DOM的创建。
渲染器的精髓在于后续的更新,它会通过Diff算法寻找并且只更新变化内容。
大致实现思路如下:
- 如果不是内容变更:
-
- 根据
vnode.tag创建对应DOM元素 - 遍历
vnode.props对象,如果key以on字符开头,说明它是一个事件,调用addEventListener绑定事件处理函数;否则作为属性添加到DOM元素上 - 处理
children,如果是字符串,就创建文本节点;如果是数组就递归调用render继续渲染,最后把创建的元素挂载到新创建的元素内
- 根据
- 否则先找出
vnode对象的变更点,并且只更新变更的内容
组件渲染过程详解:
vite、@vitejs/plugin-vue和vue-core的关系
-
vite中使用了@vitejs/plugin-vue来处理vue组件 -
@vitejs/plugin-vue中集成了vue-core中的compiler-sfc用于解析编译Vue组件 -
compiler-sfc中调用了compiler-core中的基础逻辑进行组件的编译和渲染
当我们新建并启动vue项目后,内容是如何渲染的,又是如何实时更新的?
创建并启动一个Vue应用
arduino
// 创建新项目
npm create vue@latest
// 进入项目后安装依赖
npm install
// 启动,实际执行的是vite命令
npm run dev
当项目运行npm run dev命令时执行内容如下:
编译阶段:
启动一个vite开发服务器,浏览器会通过这个服务器来访问此项目的网页和代码
vite是一个通用的构建工具,vite本身并不直接处理.vue文件,而是通过插件系统来处理各种类型文件,其中@vitejs/plugin-vue就是用来处理vue单文件组件的

构建时阶段
Vite接收到组件请求,会执行插件【@vitejs/plugin-vue】的load钩子函数,再执行Transform钩子函数

在上图钩子函数执行过程中触发了compiler-sfc相关方法的执行


监听组件变化
@Vitejs/plugin-vue插件的核心入口文件【packages/plugin-vue/src/index.ts】中定义了Vite插件的所有钩子函数,其中handleHotUpdate钩子是Vite提供的热更新处理函数,当Vue文件发生变化时,Vite会自动调用这个钩子,此时插件会检查变化的文件是否为Vue组件,如果是则调用专门的handleHotUpdate函数【packages/plugin-vue/src/handleHotUpdate.ts】

最终将返回
c
SFCTemplateCompileResults : {
code: string, // 渲染函数代码
ast?: RootNode, // 抽象语法树
preamble?: string, // 预处理代码
source: string, // 输入源
tips: string[], // 提示
errors: (string | CompilerError)[], // 错误
map?: RawSourceMap, // 源映射
}
这个阶段会将.vue文件转换为js代码,生成的是渲染函数的字符串
运行时阶段
当浏览器加载并执行这些js代码时,就会发生真正的渲染过程
scss
应用启动 -> createApp() -> app.mount() -> render() -> patch() -> mountElement() -> 真实DOM

到此就完成了vue中基本的渲染过程。