第一章:权衡的艺术
1.1 命令式和声明式
命令式:关注过程:代码描述的“做事的过程”。
声明式:关注结果:代码描述的是“做事的结果”。
例如:实现以下内容
获取id为app的div
文本内容设置为hello world
绑定点击事件
点击弹出提示ok
命令式:
//jquery
$('#app').text('hello world').on('click', () => { alert('ok')})
// 原生
const div = document.queryselect('#app');
div.innerText = 'hello world';
div.addEventListener('click', () => { alert('ok')});
声明式:
<div id="app" @click="() => alert('ok')">hello word</div>
我们只需要提供代码结果,具体过程由vue框架实现,也就是说vue框架内一定是命令式的,暴露给框架使用者偏向声明式。
1.2 性能与可维护的权衡
结论:声明式代码的性能不优于命令式。
举例说明:将上边div盒子内容修改为hello vue
命令式写法可以清楚的看到需要修改的部分,使用相关api即可,这是性能最好的方法了。命令式写法理论上可以做到极致的性能优化
命令式:
div.textContent = 'hello vue'
声明式需要找到前后的差异,只更新变化的地方,但最终要实现效果仍然需要调用命令式写法的api。
声明式:
<div id="app" @click="() => alert('ok')">hello word</div>
<div id="app" @click="() => alert('ok')">hello vue</div>
所以:
命令行的性能消耗:修改的性能
声明式的性能消耗:修改的性能 + 找差异的性能
即使将找差异的性能消耗减少为0,声明式性能也无法超越命令式。
vue选择声明式方案的原因:代码的维护性更强。这样vue就要做到在保持可维护性的同时让性能损失最小化
1.3 虚拟DOM的性能到底如何
虚拟DOM的技术是为了解决使用声明式方案带来的性能问题。
采用原生innerHTML属性创建页面。
通过innerHtml创建页面,需要写html字符串。将html字符串赋值给innerHTML属性。在渲染页面过程中,需要先把字符串解析成dom树,这属于dom层面的计算,其性能远比JavaScript的计算差。
const html = '<div>...</div>'
div.innerHtml = html
innerHTML创建页面的性能消耗:HTML字符拼接的计算量 + innerHTML的DOM计算量。
采用虚拟DOM方式创建页面
通过虚拟DOM方式创建页面
第一步需要创建JavaScript对象,这个对象用来描述真实DOM
第二步需要将虚拟DOM树递归遍历并创建真实DOM。
通过虚拟DOM方式创建页面的性能消耗:创建JavaScript对象的计算量 + 创建真实DOM的计算量。
对比JavaScript层面与DOM层面计算两个差距不大。
innerHTML与虚拟DOM方式更新页面对比
通过innerHTML属性更新页面时,即使只更改了一个字母,也需要重新设置inner HTML的值。这意味着,需要销毁所有DOM元素再重新渲染html字符串的所有内容。
而通过虚拟DOM方式更新页面时,需要重新创建JavaScript对象(虚拟DOM),再通过diff算法比较更新前后的差异。只更新产生变化的DOM元素。
虽然更新页面时虚拟DOM方式需要增加diff算法的性能消耗,但是这也属于JavaScript层面的计算,因此不会产生数量级上的差异。而innerHTML则需要全部更新。
无论页面多复杂,虚拟DOM都只会更新需要更新的部分,而对innerHTML来说,页面越复杂消耗的性能越大。
因此,虚拟DOM、innerHTML和原生JavaScript更新DOM元素的性能消耗比较如下
innerHTML(模板) | 虚拟DOM | 原生JavaScript | ||
---|---|---|---|---|
心智负担中等 | < | 心智负担小 | < | 心智负担大 |
性能差 | 性能不错 | 性能高 |
-
原生DOM消耗的性能最少,但是需要手动创建、删除、修改大量DOM元素。可维护性也很差。
-
innerHTML通过拼接HTML方式修改dom元素,其可维护性较高,但是事件绑定仍然需要原生JavaScript处理。在模板很大并且更新很少时,这种方式消耗性能最多。
-
虚拟DOM方式采用声明式写法,维护起来最方便,性能虽然比不上原生JavaScript方式,但是在可维护性和心智负担小的前提下比innerHTML方式要强很多。
1.4 运行时和编译时
设计框架的三种选择 :纯运行时、运行时 + 编译时和编译时。
纯运行时:只有在运行程序时才有效。
框架提供一个Render函数,规定好树形结构的数据对象。在浏览器运行以下代码即可看到效果。
const obj = {
tag: 'div',
children: [
{tag: 'span', children: 'hello world' }
]
}
//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.appendChild(el)
}
// 渲染到body下
Render(obj, document.body)
这种方式只能手写树形结构的数据对象来渲染页面,不直观。不支持类似HTML标签方式描述形结构的数据对象。
运行时+编译时:运行程序时才开始编译代码。
为了满足上述需求,需要引入编译手段将HTML标签编译成树形结构的数据对象。因此添加Compiler函数用来编译HTML标签。
const html = '<div><span>Hello world</span></div>'
// 调用Compiler函数 将HTML编译成树形结构的对象
const obj = Compiler(html)
// 调用Render渲染
Render(obj)
因为编译时产生性能开销所以在构建时就可以执行Compiler函数提前将用户需要的内容编译好,这样运行过程中就不需要编译了,对性能是很友好的。
编译时:编译完代码才能运行程序。
通过Compiler函数直接将HTML标签编译成命令式代码,连Render函数也不需要了。这样就不支持任何运行时内容,代码只有在编译完才能运行。
<div>
<span>Hello World</span>
</dib>
通过Compiler函数编译为👇
const div = document.createElement("div")
const span document.createElement("span")
span.innerText = 'hello world'
div.appendChild(span)
document.body.appendChild(div)
选择分析
纯运行时由于没有编译的过程,所以无法对用户提供的内容进行分析,但是如果加入编译,就可以分析用户提供的内容,查看可能改变或者永远不变的内容。这样在编译时提取相关信息传递给Render函数,由Render函数进一步分析优化。如果时纯编译时的框架,也可以分析用户提供的内容,但是必须在编译完之后才能运行,灵活性比较差。