第1章-权衡的艺术

79 阅读5分钟

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在创建页面时的性能

虚拟DOMinnerHTML
纯JavaScript运算- 新建JavaScript对象(VNode)- 渲染HTML字符串
DOM运算- 新建所有DOM元素- 新建所有DOM元素

结论: 可以看到,无论JavaScript层面的计算还是DOM层面的计算,其实两者差距不大。

1.3.2、虚拟DOM和innerHTML在更新页面时的性能

虚拟DOMinnerHTML
纯JavaScript运算- 创建新的JavaScript对象+Diff- 渲染HTML字符串
DOM运算- 必要的DOM更新- 销毁所有旧DOM
  • 新建所有新DOM |

可以发现,在更新页面时,虚拟DOM在JavaScript层面的运算要比创建页面时多出一个Diff的性能消耗,然而它毕竟也是JavaScript层面的运算,所以不会产生数量级的差异。再观察DOM层面的运算,可以发现虚拟DOM在更新页面时只会更新必要的元素,但innerHTML需要全量更新。这时虚拟DOM的优势就体现出来了。

当更新页面是,影响虚拟DOM的性能因素与影响innerHTML的性能因素不同。对于虚拟DOM来说,无论页面多大,都只会更新变化的内容,而innerHTML来说,页面越大,就意味着更新时的性能消耗越大。

1.3.3、性能因素

虚拟DOMinnerHTML
纯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)