第一章:权衡的艺术

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函数进一步分析优化。如果时纯编译时的框架,也可以分析用户提供的内容,但是必须在编译完之后才能运行,灵活性比较差。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值