likes
comments
collection
share

Vue3源码学习-1 | 权衡的艺术

作者站长头像
站长
· 阅读数 17

Vue3源码学习-1 | 权衡的艺术

Vue3源码学习 | 权衡的艺术

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

(提前说明:这里都是关于Vue3的代码学习)

作为学习者,我们在学习框架的时候,也应该从全局的角度对框架的设计拥有清晰的认知,否则很容易被细节困住,看不清全貌。在阅读之前先抛出几个问题:在阅读之前先抛出几个问题:

  • 从范式的角度来看,框架应该设计成命名式还是声明式?
  • 这两种范式有何缺点和优点?
  • 框架要设计成纯运行时还是纯编译时的?,或者说是运行时+编译时?它们之间的差异,优缺点又是什么?
  • Vue框架的设计无时无刻都体现了“权衡”的艺术“ 权 衡 ”的艺术

1.1 命令式和声明式

想必有些读者对于命令式和声明式还不是很了解,那接着往下看吧,宝子们!

命令式 - 这是一个很关注 过程过程 的男人

  • 在很久很久以前,年间流行的jQuery就是典型的命令式框架

例如将下面例子翻译成对应代码

// - 获取 id 为 app 的div标签
// - 它的文本内容为 hello world
// - 为其绑定点击事件
// - 当点击时弹出提示:ok

命令式的代码:

const div = document.querySelecter("#app")
div.innerText = 'hello world'
div.addEventListener('click',() => {alert('ok)})

可以看到,自然语言描述能够与代码产生一一对应的关系,代码本身描述的是“做事的过程”,嫁给这样的男人肯定很幸福吧。

声明式 - 这是一个很关注 结果结果 的男人

废话不多说!我们来看看实现上面自然语言的功能的声明式代码

<div @click="() => alert('ok')">hello world</div>

上面的代码就是很直接的给我们提供了一个 “结果” ,就像我们在告诉Vue.js:"嘿!,Vue.js,看到没,我要的就是一个div,文本内容是hello world,它有个事件绑定,你帮我搞定吧。"至于实现该 “结果” 的 过程过程,则是由Vue.js帮我们完成的。

  • 换句话说,Vue.js帮我们封装了 过程过程。因此,我们能够猜到Vue.js内部实现一定是 命令式命令式 的,而暴露给用户的却更加 声明式声明式

1.2性能与可维护性的权衡

命令式和声名式各有优缺点,在框架设计方面,则体现在性能与可维护性之间的权衡。这里先抛出一个结论:

声明式代码的性能不优于命令式代码的性能声明式代码的性能不优于命令式代码的性能
  • 这就引出一个问题:为什么Vue.js使用的是声明式代码?

实例说明

假设现在要将上述 div 标签的文本内容修改成hello vue3

  • 用命令式代码实现
div.textContent = 'hello vue3' // 直接修改

思考一下,还有没有其他办法比上面这句代码的性能更好?答案是 “没有”。理论上命名式代码可以做到极致的性能优化,但是声明式代码不一定能做到这一点,因为它描述的是结果。

  • 用声明式代码实现
// 之前
<div @click="() => alert('ok')">hello world</div>
// 之后
<div @click="() => alert('ok')">hello vue3</div>

对于框架来说,为了实现最优的更新性能,它需要找到前后的差异(也就是使用diff算法【这个以后会说】)并只更新变化的地方,但是因为Vue.js是内部对于命令式代码的封装,所以最终完成这次更新的代码仍然是:

div.textContent = 'hello vue3' // 直接修改

从性能上考虑

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

  • 命令式代码的更新性能消耗 = A
  • 声明式代码的更新性能消耗 = B + A

可以看出在性能层次上看命令式代码会比声明式代码好,尽管是最理想的情况(即找出差异的性能消耗为0时)两种代码的性能相同,并不能做到超越。

  • 毕竟框架本身就是封装了命令式代码才实现了面向用户的声明式

问题来了!为什么Vue.js要选择声明式的设计方案呢?

因为在日常开发中,我们面对的是整个项目,需要完成整个项目的开发。如果采用命令式代码开发的话,我们需要维护实现目标的整个过程,包括要手动完成DOM元素的创建、更新、删除等工作。而声明式代码展示的就是我们要的 结果结果,看上去更加直观,不用去关注做事儿的过程,只看页面展示的结果是否是我们想要的即可!

这就体现了我们在框架上要做出的关于可维护性和性能之间的权衡。在从用声明式提升可维护性的同时,性能就会有一定的损失,而框架设计者要做的就是:

  • 在保持可维护性的同时让性能损失最小化

1.3 虚拟 DOM 的性能到底如何

特殊说明:这里尽管可能你不知道虚拟DOM(以后会说)是个什么玩意,但是这并不影响你继续阅读

前文说到,声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗

因此,如果能够最小化 找到差异的性能消耗,就可以让声明式代码的性能无限接近命令式代码的性能。

而!所谓的虚拟 DOM ,就是为了最小化找出差异这一步的性能消耗而出现的!!!

用大白话来讲,采用虚拟DOM的更新技术的性能理论上不可能比原生Js操作DOM更高,但是它可以让我们很舒服的,不用付出太多努力的(写声明式代码),还能保证应用程序性能下线,让应用程序的性能不至于太差,甚至无限逼近命令式代码的性能!总之就是一个字!好东西!。这!就是虚拟DOM要解决的问题!

特殊说明:不要看到上文把虚拟DOM吹的云里雾里,就觉得DOM运算会比原生Js的性能好!大错特错!从根本上!源头上!盘古开天辟地!上来讲Js始终是爹!

  • 记住!涉及DOM的运算要远比JavaScript层次的计算性能差!

虚拟DOM 的性能体现在哪里?

虚拟DOM创建页面的过程分为两步:

  • 第一步:创建JavaScript对象(这个对象可以理解为真实DOM的描述)
  • 第二步:是递归地遍历虚拟DOM树并创建真实DOM(可以先看一下这个以后会说,不妨碍继续阅读)

实例说明

假设有一个场景,页面的元素发生变化,也就是代码会出现一定的改变

  • 原生js(也就是命令式)它会将全部旧的DOM节点全部销毁,再新建所有新的DOM节点,就比如你稍稍微改变一个东西都会这么干(我真怀疑它有洁癖)
  • 虚拟DOM,会进行必要的更新采用Diff算法,讲新旧节点对比更新

Diff算法简易图 Vue3源码学习-1 | 权衡的艺术

乍一看!是不是觉得虚拟DOM更加的和蔼可亲了,它是声明式的,因此心智负担小,可维护性强!

1.4 运行时和编译时

当设计一个框架的使用,我们有三种选择:纯运行时的、运行时+编译时的或纯编译时的。接下来的问题就是:

  • 什么是运行时?什么是编译时?
  • 运行时和编译时各有什么特征?
  • Vue.js又选择了哪一种?

纯运行时

大概就是提供一个 Render 函数,为这个函数提供一个树形对象,即:

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

然后 Render 函数会根据该对象递归地将数据渲染成DOM元素,Render函数:

function Render(obj,root) {
    const el = document.createElement(obj.tag)
    if(typeof obj.children === 'string'){
        const text = document.createTextNode(obj.children)
        el.appendChildren(text)
    }else if(obj.children) {
        // 数组,递归调用 Render ,使用el作为参数
        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)

上面编写的就是一个纯运行纯运行的框架,总的来说就是纯纯用代码来实现页面的渲染,然后我们又来到了一个场景:有一天你的用户抱怨说“老子不不干了!手写树形结构的数据对象太麻烦了,而且不直观,能不能支持用类似于HTML标配的方式描述树形结构的数据对象呢?”。那就来到了引入了编译时编译时了!

编译时

用户的需求是:

<div>
   <span> hello world </span>
 </div>

编译成--->

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

假设你编写了一个叫作Compiler的程序满足了上述用户的需求,这就是一个 编译时编译时 的框架,再加上Render函数,我们由来到了!

运行时 + 编译时

当结合了上述的运行时 + 编译时,我们可以得到Vue.js保持的架构:运行时 + 编译时

它在保持灵活性的基础上,还能够通过编译手段分析用户提供的内容,从而进一步提升更新性能

1.5 总结

  • 讨论了命名式和声明式这两种范式的差异,如何去权衡性能可维护性
  • 讨论的原生Js和虚拟DOM,权衡选择了虚拟DOM
  • 了解了运行时,编译时,为实现用户需求,权衡选择了运行时+编译时