阅读Vuejs设计与实现——第一章权衡艺术

219 阅读6分钟

第一章、权衡艺术

1.1 命令式和声明式

  • 视图层框架分为命名式和声明式
        $("#app').text('hello world').on('click',()=>{alert('ok')})
        
        const app = document.querySelector('#app')
        app.innerText = 'hello world'
        app.addEventListener('click',()=>{alert('ok')})
    
  • 学过jQuery和JavaScript的都可以看出,代码本身就描述了做事的过程 这就是命名式框架一大特点关注过程
  • 原生的方法操作DOM和JQuery方法操作DOM的方式都是命令式框架。而像Vue则是一个声明式框架
        <div @click="()=>alert('ok')>hello world</div>
    
  • 两者区别,命令式则是关注过程,声明式则关注结果

1.2 性能与可维护性的权衡

  • 这里我先抛出一个结论:声明式代码的性能不优于命令式代码的性能。

  • 如果我们直接修改的性能消耗定义为A,找出差异的性能消耗定义为B,那么会变成:

    • 命令式代码的更新性能消耗 = A
    • 声明式代码的更新性能消耗 = B + A
  • 按照这公式来看,最理想的情况是,当找出差异的性能消耗为0时。声明式代码和命令式代码的性能相同,无法做到超越。毕竟总的来说框架本身就是封装了命令式代码才实现了面向用户的声明式

  • 这符合我上面得出来性能的结论:声明式代码的性能不优于命令式代码的性能。

  • 两者对比,优缺点恰好相反,命名式关注过程,直接修改的性能更好,反之,声明式关注结果,看上去更直观,可维护性更强。

  • 所以,框架设计上要考虑到性能与可维护性的权衡

  • 框架设计者要做的事就是:在采用声明式提升可维护性同时让性能损失最小化。

1.3 虚拟DOM的性能到底如何

  • 对比纯JavaScript操作与DOM操作

    • 文中给出上述跑分例子,上方则是循环10000次,每次创建一个JavaScript对象并将其添加到数组中。下方则是DOM操作,每次创建一个DOM元素将其添加到页面中。因此我们可以发现,纯JavaScript层面的操作要比DOM操作快得多。
    • innerHtml创建页面的性能: HTML字符串拼接的计算量 + innerHTML的DOM计算量
  • 虚拟DOM

    • 虚拟DOM : 创建JavaScript的计算量 % 创建真实DOM的计算量
    • innerHTML: innerHTML更新页面的过程是重新构建HTML字符串,再重新设置DOM元素的innerHTML,也就是说,哪怕我们只更改了一个文字,也要重新设置innerHTML属性。而重新设置innerHTML属性就等价于 销毁所有旧的DOM元素,再全量创建新的DOM元素
    • 虚拟DOM: 它需要重新创建JavaScript对象(虚拟DOM树),然后比较新旧DOM找到变化的元素并更新他们。
  • 虚拟DOM的优势

    • 虚拟DOM在更新页面时只会更新必要的元素,但innerHTML需要全量更新。

1.4 运行时和编译时

1.4.1. 纯运行时的框架

假如我们设计了一个框架,用户构建一个树型的数据对象,根据运行Render函数来对该对象递归地将数据渲染成DOM元素。

const obj = {
	tag: "div",
  children: [
    { tag: 'span', children: "hello world"}
  ]
}

根据上述代码,每个对象有两个属性: tag代表标签名名称,children既可以是一个数组(代表子节点),也可以直接是一段文本(代表文本子节点)

1.4.2. 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 {
    // 数组, 递归调用 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 下
Reder(obj, document.body)
复制代码

不足: 有一天用户觉得手动写一个树型的对象太麻烦了,又不直观,能不能致辞类似于HTML标签的方式描述树型结构的数据对象呀?

1.4.3. 实现Compiler函数

上面它存在问题,为此,Compiler函数就为它而产生,作用就是把HTML字符串编译成树型结构的数据对象 用户只要分别调用Compiler函数和Render函数:

// 调用 Compiler 编译得到树型结构的数据对象
const html = `<div>
    <span>hello world</span>
</div>`
const obj = Compiler(html)

// 再调用 Render 进行渲染
Render(obj,document.obj)
复制代码
  • 上面这段代码能够很好地工作,我们框架就变成一个运行时 + 编译时的框架。

1.4.4. 思考: 能不能直接编译成命令式代码呢?

  • 由于纯运行时框架,由于它没有编译的过程,因此我们没有办法分析用户提供的内容。但是如果加入编译步骤,就可以在编译的时候提取这些信息,传递给Render 函数,得到信息之后,可以进一步的优化了。
  • 如果是纯编译时框架,可以分析用户提供的内容。不需要做出任何运行时,而是直接编译成可执行的JavaScript代码,性能也许会更好,但是有失灵活性,用户提供的内容必须编译后才能使用。目前前端越来越多框架涌入,出现SolidJs、Svelte等,看过SolidJs一点知识,性能直逼原生JS。

拓展:性能比较

原生 JS 是 1, Solid 是 1.05, 比 Svelte 也快,React 跑到了 1.93 。如图:

SolidJS Benchmark

  • Svelte 就是纯编译时的框架,但是它真实性能可以达不到理论的高度。
  • Vue.js3仍然保持了运行时 + 编译时的架构,在保持灵活性的基础上要尽可能去优化它。

总结

本篇讨论的主要内容是:

  1. 命名式声明式的差异、优缺点,其中命名式更加关注过程,而声明式更加关注结果。命名式性能更好,但是可维护性差;而声明式性能差,可维护性强,有效减轻用户的心智负担。所以,框架设计者必须想方法尽量使损耗最小化
  2. 虚拟 DOM的性能,并给了公式:“声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗”,存在的意义在于找出差异的性能消耗最小化
  3. 编译时和运行时的相关知识,无论是纯编译时、纯运行时以及两者都支持的框架各有什么特点,Vue.js3 仍然保持了运行时 + 编译时的架构,在保持灵活性的基础上要尽可能去优化它

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下阿绵哈哈。

宝贝们,都看到这里了,要不点个赞呗 👍

写作不易,希望可以获得你的一个「赞」。如果文章对你有用,可以选择「关注 + 收藏」。 如有文章有错误或建议,欢迎评论指正,谢谢你。❤️