前言
最近在深入学习 Vue.js设计与实现 ,深感“理解设计理念”比“记忆API用法”更为重要。
我决定通过文章来记录和输出学习内容,这不仅是为了检验自己的理解程度,更是为了梳理Vue.js背后的核心思想和实现机制。希望这种“知其所以然”的探索,能为你带来启发。
1.1命令式和声明式
首先什么是声明式和命令式,看接下来我们要做的事
获取 id 为 app 的 div 标签
它的文本内容为 hello world
为其绑定点击事件
当点击时弹出提示:ok
命令式代码:你如何去做,自己手动操作DOM,核心概念就是 关注过程
const div = document.querySelector('#app') // 获取 div
div.innerText = 'hello world' // 设置文本内容
div.addEventListener('click', () => { alert('ok') }) // 绑定点击事件
声明式代码:你描述想要的结果,Vue自动帮你实现DOM操作,核心概念就是 关注结果
<div @click="() => alert('ok')">hello world</div>
更通俗一点的说法:
- 命令式代码是厨师,你需要洗菜、切菜、炒菜,每个步骤都需要你来
- 声明式代码是顾客,我只需要跟你说一声我要吃什么,接下来就交给你了
1.2 性能和可维护性的权衡
声明式代码好是好,但他会有更多的性能消耗,借助书中的一句结论:声明式代码的性能不优于命令式代码的性能
看上面的例子 要求:把div中的文本要求改成 hzh 真帅。
命令式代码:
div.textContent = 'hzh 真帅'
声明式代码:
<div @click="handleClick">{{ message }}</div>
const app = {
data() {
return {
message: 'hello world'
}
},
methods: {
handleClick() {
// 声明式更新:只改变数据,Vue自动更新DOM
this.message = 'hzh 真帅';
}
}
}
//当我们的this.message = 'hzh 真帅'的时候,vue自动做了这么一步
div.textContent = 'hzh 真帅'
书中结论:
如果我们把直接修改的性能消耗定义为 A,把找出差异的性能消耗定义为 B,那么有:
● 命令式代码的更新性能消耗 = A
● 声明式代码的更新性能消耗 = B + A
那既然声明式代码会消耗更多的性能,那我们是不是要更多的使用命令式代码?答案是不一定,直接上截图
其实对于大多数业务应用,声明式的可维护性优势远大于其性能开销。而且随着框架优化技术的进步,这个性能差距正在不断缩小。这也是为什么vue,react这些声明式框架会成为主流
1.3 虚拟DOM的性能到底如何
首先,虚拟DOM是什么?先看看真实DOM是什么
真实DOM
// 真实DOM - 浏览器中的实际节点
<div class="title" id="app">Hello World</div>
虚拟DOM
// 对应的虚拟DOM
const vnode = {
type: 'div',
props: {
class: 'title',
id: 'app'
},
children: 'Hello World'
}
可以看出,虚拟DOM就是一个js对象,为什么要有虚拟DOM这个东西呢,回想之前的声明式代码,把找出差异的性能消耗定义为 B,虚拟DOM就是为了优化这个'找出差异(B)'的性能消耗(书中原话:虚拟DOM就是为了最小化找到差异这一步性能消耗而出现的)
DOM的更新方式以下有几种
- 原生的js操作
- innerHTML
- 虚拟DOM
innerHTML更新需要触发需要删除以前的元素,在重新更新新的元素
innerHTML创建页面的性能:HTML 字符串拼接的计算量 + innerHTML 的 DOM 计算量
<div id="list">
<li>项目1</li>
<li>项目2</li>
</div>
// 更新列表 - 重新渲染整个列表
const list = document.getElementById('list');
list.innerHTML = `
<li>项目1</li>
<li>项目2</li>
<li>新项目3</li> <!-- 只增加了这一项 -->
`;
虚拟DOM创建js对象,这个对象可以理解成真实DOM,并使用递归地遍历虚拟 DOM 树并创建真实 DOM
虚拟DOM创建页面的性能:创建 JavaScript 对象的计算量 + 创建真实 DOM 的计算量
// 旧的虚拟DOM
const oldVNodes = [
{ type: 'li', children: '项目1' },
{ type: 'li', children: '项目2' }
];
// 新的虚拟DOM
const newVNodes = [
{ type: 'li', children: '项目1' },
{ type: 'li', children: '项目2' },
{ type: 'li', children: '新项目3' }
];
那么他两的区别在哪呢?
innerHTML的更新过程:innerHTML的更新过程会销毁所有现有DOM元素,然后重新创建新的DOM元素
大概意思是:
原本:list.innerHTML = `
<li>项目1</li>
<li>项目2</li>
`;
->
销毁之后:list.innerHTML = ``
->
重新更新:list.innerHTML = `
<li>项目1</li>
<li>项目2</li>
<li>新项目3</li>
`;
虚拟DOM的更新过程:javaScript对象+diff,只会更新更改的元素
// 虚拟DOM如何工作
const oldVNode = {
type: 'ul',
children: [
{ type: 'li', children: 'Item 1' },
{ type: 'li', children: 'Item 2' }
]
};
const newVNode = {
type: 'ul',
children: [
{ type: 'li', children: 'Item 1' },
{ type: 'li', children: 'Updated Item 2' }, // 只有这里变了
{ type: 'li', children: 'Item 3' } // 新增
]
};
// Diff算法发现:
// - 第一个li没变,复用
// - 第二个li文本变了,只更新文本
// - 新增第三个li,创建新元素
虚拟DOM的优势在于:无论页面有多大,我只更新变化的地方,而innerHTML则是全部销毁更新,这对性能消耗很大
书中原话:innerHTML、虚拟 DOM 以及原生 JavaScript 在更新页面时的性能我们分了几个维度:心智负担、可维护性和性能
1.4编译时和运行时
当设计一个框架的时候,我们有三种选择:纯运行时的、运行时 +编译时的或纯编译时的
运行时 假设一个框架,提供一个render函数,我们可以提供一个树型结构的对象
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
}
tag代表标签名称,children即可以是一个数组(代表子元素),也可以是一段文本内容,接下来实现一下render函数
function Render(obj, root) {
const el = document.createElement(obj.tag)
if (typeof obj.children === 'string') {
const text = document.createTextNode(obj.children)
el.appendChild(text)
} else if (obj.children) {
// 数组,递归调用 Render,使用 el 作为 root 参数
obj.children.forEach((child) => Render(child, el))
}
// 将元素添加到 root
root.appendChild(el)
}
有了这个函数之后,我们可以这样使用来渲染到body下
Render(obj, document.body)
编译时
有时我们觉得,直接写一个树型结构的对象好麻烦啊,我能不能获取一下html标签把他变成树型结构的对象呢?,为此写了一个Compiler函数,可以实现上述效果
const html = `
<div>
<span>hello world</span>
</div>
`
// 调用 Compiler 编译得到树型结构的数据对象
const obj = Compiler(html)
Compiler函数做的事:将html编译成命令式代码的过程
那么这三种选择分别有哪些优缺点呢?
运行时:
- ✅ 无构建步骤:直接引入即可使用
- ✅ 灵活性高:可以在运行时动态创建任何结构
- ✅ 调试简单:没有编译过程,代码就是最终代码
- ❌ 性能较差:无法进行编译时优化
- ❌ 代码冗长:需要手动创建DOM和绑定事件
- ❌ 无语法糖:没有模板、JSX等便捷语法
编译时:
- ✅ 性能极佳:编译时完成所有优化,运行时几乎零开销
- ✅ 包体积小:不需要包含运行时框架代码
- ✅ 无虚拟DOM:直接操作DOM,减少内存占用
- ❌ 灵活性差:难以在运行时动态创建复杂结构
- ❌ 调试困难:编译后代码与源代码差异大
- ❌ 构建依赖:必须使用构建工具,无法直接运行
编译时 + 运行时:
- ✅ 开发体验好:提供模板、JSX等友好语法
- ✅ 性能优化:编译时可以进行静态分析优化
- ✅ 灵活性:支持运行时动态创建
- ✅ 渐进式:可以选择使用编译特性
- ⚠️ 需要构建工具:需要配置webpack、vite等
- ⚠️ 包体积较大:包含编译器和运行时
- ⚠️ 复杂度高:需要处理编译和运行时的协调
总结
通过对比命令式与声明式、分析虚拟DOM原理、了解编译时与运行时的选择,我们看到了Vue.js在性能与可维护性之间的智慧平衡。这种"知其所以然"的理解,能让我们更好地使用和欣赏这个优秀的框架。
希望这篇文章对你有所启发!