1.1、 命令式和声明式
从范式上来看,视图层框架通常分为命令式和声明式,命令式框架的一大特点是关注过程,例如Jquery。
1 获取id为app的div标签
2 它的文本内容为hello world
3 为其绑定点击事件
4 为点击时提示:ok
对应的代码:
$('#app') // 获取div
.text('hello world') // 设备文本内容
.on('click', () => alert('ok')) // 绑定点击事件
若是用原生JavaScript来实现同样的功能:
const div = document.querySelector("#app") // 获取div
div.innerText = 'hello world' // 设置文本内容
div.addEventListener('click', () => alert('ok'))
可以看到,自然语言描述能够与代码产生一一对应的关系,代码本身描述的是‘做事的过程’,这符合我们的逻辑直觉。
那么什么是声明式框架呢?与命令式框架跟家关注过程不同,声明式框架更加关注结果。 结合Vue.js,我们来来看看如何实现上面自然语言描述的功能:
<div @click="() => alert('ok')">hello world</div>
这段类HTML的模板就是Vue.js实现如上功能的方式。
我们能够猜到Vue.js的内部一定是命令式的,而暴露给用户的却声明式的。
1.2、性能和可维护行的权衡
结论:声明式代码的性能不优于命令式代码的性能。
例如: 我们要将div标签的文本内容修改为hello vue3。
命令式:
div.textContent = 'hello vue3' //直接修改
声明式:
// 之前
<div @click="() => alert('ok')">hello world</div>
// 之后
<div @click="() => alert('ok')">hello world</div>
对于框架来说,为了实现最优的更新性能,他需要找到前后的差异并只更新变化的地方,但最终完成这次更新的代码依然是:
div.textContent = 'hello vue3' //
如果我们把直接修改消耗的性能定义为A,把找出差异的性能消耗定义为B,那么有:
◻️🔴命令式代码的更新性能消耗 = A
◻️🔴 声明式代码的更新性能消耗 = B + A
框架设计者要做的就是:在保持可维护性的同时让性能损失最小化。
1.3、虚拟DOM的性能到底如何
声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
因此,如果我们能够最小化找出差异的性能消耗, 就可以让声明式的代码性能无限接近命令式代码的性能,而所谓的虚拟DOM,就是为了最小化找出差异这一步的性能消耗而出现的。
理论上讲采用虚拟DOM的更新技术的性能不可能比原生JavaScript操作DOM更高
但是: 我们很难写出绝对优化的命令式代码
- HTML字符串的拼接计算:
const app = []
for(let i=0; i<10000; i++) {
const div = { tag: 'div'}
app.push(div)
}
- innerHTML的DOM计算量
const app = document.querySelector('#app')
for(let i=0;i<10000;i++) {
const div = document.createElement('div')
app.appendChild(div)
}
从上述可以发现,纯JavaScript层面的操作比DOM操作快的多,它们不在一个数量级上。
innerHTML创建页面的性能 = HTML字符串拼接的计算量 + innerHTML的DOM计算量。
1.3.1、innerHTML和虚拟DOM在创建页面时的性能
| 虚拟DOM | innerHTML | |
|---|---|---|
| 纯JavaScript运算 | - 新建JavaScript对象(VNode) | - 渲染HTML字符串 |
| DOM运算 | - 新建所有DOM元素 | - 新建所有DOM元素 |
结论: 可以看到,无论JavaScript层面的计算还是DOM层面的计算,其实两者差距不大。
1.3.2、虚拟DOM和innerHTML在更新页面时的性能
| 虚拟DOM | innerHTML | |
|---|---|---|
| 纯JavaScript运算 | - 创建新的JavaScript对象+Diff | - 渲染HTML字符串 |
| DOM运算 | - 必要的DOM更新 | - 销毁所有旧DOM |
- 新建所有新DOM |
可以发现,在更新页面时,虚拟DOM在JavaScript层面的运算要比创建页面时多出一个Diff的性能消耗,然而它毕竟也是JavaScript层面的运算,所以不会产生数量级的差异。再观察DOM层面的运算,可以发现虚拟DOM在更新页面时只会更新必要的元素,但innerHTML需要全量更新。这时虚拟DOM的优势就体现出来了。
当更新页面是,影响虚拟DOM的性能因素与影响innerHTML的性能因素不同。对于虚拟DOM来说,无论页面多大,都只会更新变化的内容,而innerHTML来说,页面越大,就意味着更新时的性能消耗越大。
1.3.3、性能因素
| 虚拟DOM | innerHTML | |
|---|---|---|
| 纯JavaScript运算 | - 创建新的JavaScript对象+Diff | - 渲染HTML字符串 |
| DOM运算 | - 必要的DOM更新 | - 销毁所有旧DOM |
- 新建所有新DOM | | 性能因素 | - 与数据变化量相关 | - 与模板大小有关 |
1.3.4、innerHTML、虚拟DOM以及原生JavaScript在更新页面时的性能
上述原生JavaScript是指(createElement等方法)
1.4、运行时和编译
1.4.1、运行时
假设我们设计了一个框架,它提供一个Render函数,用户可以为该函数提供一个树形结构的数据对象,然后Render函数会根据该对象递归地将数据渲染成DOM元素。我们规定树形结构的数据对象如下:
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
}
<script>
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) {
// array,递归调用 Render,使用 el 作为 root 参数
obj.children.forEach((child) => Render(child, el))
}
// 将元素添加到 root
root.appendChild(el)
}
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
}
// 渲染到 body 下
Render(obj, document.body)
</script>
在浏览器中运行上面的这段代码,就可以看到我们预期的内容。
上述代码过程就是一个纯运行时的代码。
1.4.2、运行时+编译时
<div>
<span>hello world</span>
</div>
↓ ****编译
const obj = {
tag: 'div',
children: [
{ tag: 'span', children: 'hello world' }
]
}
把HTML标签编译成树型结构的数据对象
为此,你编写了一个叫作Compiler的程序,它的作用是把HTML字符串编译成树型结构的数据对象。
const html = `
<div>
<span>hello world</span>
</div>
`
// 调用Compiler 编译得到树型结构的数据对象
const obj = Compiler(html)
// 再调用Render进行渲染
Render(obj,document.body)
1.4.3、纯编译
<div>
<span>hello world</span>
</div>
//编译
const div = document.createElement('div')
const span = document.createElement('span')
span.innnerText = 'hello world'
div.appendChild(span)
document.body.appendChild(div)