工学云
150.99MB · 2026-02-11
从范式上来看,视图层框架通常分为:
// 自然语言描述能够与代码产生一一对应的关系
// 示例:
const div = document.querySelector('#app') // 获取div
div.innerText = 'hello world'// 设置文本内容
div.addEventListener('click', () => { alert('ok') }) // 绑定点击事件
// 用户提供一个“预期的结果”,中间的过程由vue.js实现
// 示例
<div @click="() => alert('ok')">hello world</div>
因为声明式框架在更新时比命令式框架多了“找出差异”的过程,所以声明式代码的性能不会优于命令式代码的性能。而对比命令式代码,声明式代码又具有更强的可维护性,更加的直观。所以框架要做的就是:在保持可维护性的同时让性能损失最小化。
在开发过程中,原生JS操作DOM,虚拟DOM和innerHTML三者操作页面的性能都与创建页面、更新页面,页面大小、变更部分的大小有关系,选择哪种更新策略,需要结合心智负担、可维护性等因素综合考虑。
性能对比
| 更新策略 | 心智负担 | 可维护性 | 性能 | 适用场景 |
|---|---|---|---|---|
原生JS | 高 | 低 | 最高 | 简单页面 |
| 虚拟DOM | 中 | 高 | 中 | 复杂应用 |
innerHTML | 低 | 中 | 低 | 静态内容 |
当设计一个框架的时候,有三种选择
@vue/compiler-dom 实现,构建时编译通过 @vitejs/plugin-vue 实现】js代码,因为性能可能会更好,但是有损灵活性。Vue.js就是内部封装了命令式代码从而实现的面向用户的声明式框架;是运行时+编译时架构,目的在于保持灵活性的基础上尽可能的优化性能
其中组件的实现依赖于渲染器,组件中模板的编译依赖于编译器。虚拟DOM作为媒介在整个渲染过程中作为组件真实DOM的载体协助实现内容渲染和更新。
vnode】虚拟DOM 是一个用来描述真实DOM的js对象。
使用虚拟DOM的好处是可以将不同类型的标签、属性及子节点抽象成一个对象,这样描述UI可以更加灵活。
// 上文中的代码可以用以下形式表示
const vnode= {
// 标签名称
tag: 'div',
// 标签属性
props: {
onClick: () =>alert('ok')
},
// 子节点
children: 'hello world'
}
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的性能优势:
diff算法最小化DOM操作组件就是一组DOM元素的封装,它可以是一个返回虚拟DOM的函数,也可以是一个对象。组件的返回值也是虚拟DOM,它代表组件要渲染的内容。
编译器的作用是将组件模板【<template>】编译为渲染函数并添加到<script>标签块的组件对象上
// demo.vue
<template>
<div@click="handler">
hello world
</div>
</template>
<script>
exportdefault {
data() { }
methods: {
handler: () =>alert('ok')
}
}
</script>
组件编译后结果:
exportdefault {
data() {},
methods: {
handler: () =>alert('ok')
},
render() {
return _createElementVNode('div', { onClick: handler }, 'hello world', -1/* HOISTED */)
}
}
无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的。然后再将渲染函数返回的虚拟DOM作为渲染器的入参,进行真实DOM的渲染
Vue3的编译优化:
diff过程【通过在虚拟DOM中添加标记实现】tree-shaking:移除未使用代码渲染器的作用就是递归遍历虚拟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应用 // 创建新项目
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】
最终将返回
SFCTemplateCompileResults : {
code: string, // 渲染函数代码
ast?: RootNode, // 抽象语法树
preamble?: string, // 预处理代码
source: string, // 输入源
tips: string[], // 提示
errors: (string | CompilerError)[], // 错误
map?: RawSourceMap, // 源映射
}
这个阶段会将.vue文件转换为js代码,生成的是渲染函数的字符串
当浏览器加载并执行这些js代码时,就会发生真正的渲染过程
应用启动 -> createApp() -> app.mount() -> render() -> patch() -> mountElement() -> 真实DOM
到此就完成了vue中基本的渲染过程。