失业前端在家阅读《Vue.js设计与实现》之权衡的艺术

324 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

  最近前端投简历仿佛投入了大海一样,一直处于“pedding”状态。这才叫真正的“海投”🤣🤣。 情人节向家属申请了350块学习经费。买了四本书,分别是《Vue.js设计与实现》、《Vue.js技术内幕》、《微前端设计与实现》、《React全家桶》。 为了防止以前读书像“燕过无痕”一样在大脑不留痕迹,过两天就忘光。今天开始就一边写笔记一边学习吧。就当作是督促自己。

命令式与声明式

  如果把一头猪,放进一个箱子里面,如果拍它一下,它发会出“哼唧哼唧”的声音。

作为一个小前端在没有框架的情况下会这么做:

const box = document.querySelector('#box').
box.appenChild('猪')
box.addEventListener('click',()=>alert('哼唧哼唧'))

这就是命令式代码,命令式代码关心每一步的过程。它是最直接地完成了所有工作的代码。因此它的性能是最好的。

当我手举一个'vue'框架的时候,我会这么做:

<box @click="()=>alert('哼哼唧唧')">居居</box>

这就是声明式代码,声明式代码关注结果。我只关心结果是有一只猪被放在了盒子里面,如果我打它它会“哼唧哼唧”叫,至于你怎么举起猪怎么放进去我并不关心。声明式代码利于维护。

Vue框架对开发者暴露了声明式代码的编程方式,而它的底层则是命令式。因此声明式的代码的性能是N+1(这个1指的就是命令式,无论如何优化,到最终还是需要命令式代码去完成功能)。所以如何尽量减少N,让总和无限接近命令式是Vue框架底层很重要的一个任务。

这本书的第一章的第一句就是:

框架设计里到处都体现权衡的艺术

权衡,我认为大概就是可维护性与性能之间的权衡吧。为了开发者的可维护性与易开发性,框架底层通常会帮我们完成很多繁复的操作,但是繁复的操作也会带来性能问题。牺牲一部分性能问题从而大大地提高开发者地效率。如何尽量缩小性能的牺牲呢?

虚拟DOM

在javascript的底层,我们去修改一个dom属性通常有以下两种方式:

document.createElement('div')
document.innerHTML = `<div></div>`

createElement在前面说了,是底层命令式代码了,效率较高,但是可维护性差,心智负担较大。

而innerHTML虽说也是命令式代码,但却比较特殊。 书中对于innerHTML,是这么说的:

首先要把字符串解析成DOM树,这是一个DOM层面的计算。涉及DOM的运算要远比Javascript层面的计算性差

在很久之前我学前端的时候,“innerHTML”被老师称为“暴力HTML”。它虽然很简单,配合字符串模板写代码分分钟的事情,但是它却很暴力,就算是极其微小的属性变动,它也需要销毁所有旧的DOM元素,然后全量创建新的DOM元素。

因此文中提到了虚拟DOM的概念。

虚拟DOM在变更对象属性的时候使用Diff算法去计算必要更新的DOM,再去更新其属性,从代码层面,比纯javascript运算代码更好维护,从性能上来讲,比innerHTML的全量销毁+更新更好。

运行时和编译时

上面提到虚拟DOM,假设我们将HTML的元素javascript化,使用javascript语言来表达HTML,那我们的到一个虚拟的元素js数组对象,它虽然不是真实的,但是我们却可以根据这个数组对象来生成真实的HTML元素。 示例:

const nodeTree = [
  {tag:'div', children:[{tag:'span', children:'您好1'}]},
  {tag:'div', children:[{tag:'span', children:'您好2'},{tag:'span', children:'您好3'}]}
]

现在我们来设计一个方法,当输入虚拟DOM跟目标挂载对象,我们就可以将数组对象生成真实的DOM并挂载到目标挂载对象上去。

const render = (nodeTree,root) => {

  let father = document.createElement(nodeTree.tag)

  if(typeof nodeTree.children === 'object'){

    nodeTree.children.map(child =>{

      render(child,father)

    })

  }else{

    father.textContent = nodeTree.children

  }

  root.appendChild(father)

}

功能完成后,现在开发者只要写出类似以上nodeTree这样的树状变量,再调用Render函数即可在页面上看到想要的布局

nodeTree.map(d=>render(d,document.body))

以上这个流程,就是运行时框架的概念示例。用户只要提供数据并调用渲染方法,即可渲染。

但是通常我们开发不习惯写树状js节点,我们更喜欢写HTML标签。 所以框架就需要加入一个compiler编译步骤 开发者编写HTML标签,compiler将HTML标签编译成上述树状js结构,这就是编译时的概念。用户输入的HTML需要经过编译后才能进行生成与渲染。

现在我们只要在页面上编写HTML,然后调用如下代码,我们就能在页面上看到想要的效果

// 此代码引用了书中原文示例代码
const obj = compiler(html)
render(obj,root)

这就是Vue框架是一个“运行时+编译时”框架的原因了。