【🚨万字警告】了不得的Vue3(上)

写在开头

你们好,我是Channing

上周五刚在部门里作完了关于Vue3的技术分享,先后大概花了整整一个星期的时间和精力去准备里面的内容,遂想在周末的时间里把这些从新再整理成文字也分享给掘友们。

或许有些部分不绝对专业,但绝对乐于接受合理的意见和建议。

本文主体脉络分为三个部分:Vue3重写的动机优化的原理,以及Vue3带来了什么 值得一看的新东西

内容方面具体的划分为以下脑图所示: javascript

Why——重写的动机?

更专业可见尤大亲笔:The process: Making Vue 3前端

重写的动机主要分为两点:vue

  1. 使用新的JS原生特性
  2. 解决设计和体系架构的缺陷

使用新的JS原生特性

随着前端标准化的发展,目前主流浏览器对不少JS新特性都广泛支持了,其中一些新特性不只解决了不少技术上的实现难题,还带来了更好的性能提高。java

在Vue3中,最重要也更为人所知的就是ES6的Proxynode

Proxy不只消除了Vue2中现有的限制(好比对象新属性的增长、数组元素的直接修改不会触发响应式机制,这也是不少新手觉得所谓的bug),并且有着更好的性能:react

咱们知道,在Vue2中对数据的侦听劫持是在组件初始化时去遍历递归一个对象,给其中的每个属性用Object.defineProperty设置它的getter和setter,而后再把处理好的这些对象挂到组件实例的this上面,因此这种方式的数据侦听是在属性层面的,这也是为何增长对象属性没法被监听的缘由,同时这种初始化的操做对于CPU来讲仍是比较昂贵的一个操做。对于javascript而言,一个对象确定越稳定越小性能就越好。git

使用Proxy以后组件的初始化就不须要这么作这么费时的操做了,由于Proxy就是真正意义给一个对象包上一层代理从而去完成数据侦听劫持的操做,因此总的来讲这个复杂度是直接少了一个数量级的。只要你对这个代理后的对象访问或修改里面的东西都能被这层代理所感知到,进而去动态地决定返回什么东西给你,而且也再也不须要把选项的这些东西再重复挂到组件实例的this上面,由于你访问的时候是有这个信息知道你访问的东西是属于props仍是data仍是其余的,vue只须要根据这个信息去对应的数据结构里面拿出来就能够了,单就这一点而言你就能够感受到组件的内存占用已经少了一半。github

因为proxy是在对象层面上的代理,因此你在对象上新增属性是能够被代理到的。数组

另外Proxy还能够代理数组,因此就算你直接修改数组里面的元素也是能够被代理到的。浏览器

可是,对于传统的浏览器——IE,就连IE11也尚未支持Proxy这个东西,又因为Proxy是原生的ES6特性,因此目前还没法经过polyfill来兼容IE(Vue3也正在作这一块的兼容).....这个东西也确实拿他没辙,不然当初React升级到1五、Vue2.X也不会抛弃IE8了。尤大在去年的VueConf上还很形象地吐槽了IE——百足之虫,死而不僵。

IE耗子尾汁吧。

解决设计和体系架构的缺陷

随着Vue2使用和维护过程,逐渐暴露出来了一些设计和体系架构上的缺陷,主要有:

  1. 框架内部模块的耦合问题
  2. TypeScript的支持很差
  3. 对于大规模应用的开发体验很差

那么在Vue3中是如何逐一解决这些问题的?

1. 解耦内部包

首先,看过Vue2源码的朋友们应该比较深有感触,单一地理解框架源码是很是痛苦的。

这个表现为各个模块内部的高度耦合和看上去彷佛不属于任何地方的浮动代码的隐式耦合,这也让源码的维护和扩展在社区中变得困难重重。

也因为内部模块的耦合,对于一些资深的高级用户(好比库做者)在构建更高级别的渲染器时不得不把整个框架的代码引入进来。咱们在看Vue2源码的时候或许会注意到里面还有Weex,这个是因为Weex是与Vue官方合做的一个多端渲染框架,而Vue2中为了支持这个能力又受限于现有架构,不得不分叉代码库而且复制大量的框架代码进去,更惨的像mpVue这种非官方合做的,就只能手动拉整个Vue的分支代码下来。

为了解决这个问题,Vue3重写时采用了monorepo的设置,把原来的各个模块拆分出来,整个框架再由这些低耦合的内部包组成,每一个包都有本身的API、类型定义、测试程序等等。一方面让开发人员能够更容易地阅读、理解甚至能够放心地大范围修改这些模块包。

另外一方面还给予了用户将其中的一些包单独拿出去用的能力,好比你能够把reactivity这个包也就是响应式系统拿出去用于须要用到响应式的场景,也能够用这个包去搭一个本身的玩具框架等等都是能够的。

2.使用TypeScript重写以及设计类型友好的新API

讲到TypeScript,这应该也是咱们比较关注的一个问题。

首先Vue2最初是使用纯JS写成的,但后来意识到一个类型系统将对这样大型规模的项目是很是必要的,尤为体如今重构或者扩展,类型检查将很大程度地减小这个过程当中引入意外错误的机会,也让更多代码贡献者能够更大胆放心地进行大范围的更改。

因此Vue2当时引入了Facebook的Flow进行静态类型检查,一方面是由于它能够渐进地添加到现有的纯JS项目中,但惋惜的是Flow虽然有必定的帮助可是并无指望中那么香,最离谱的是谁能想到连Flow本身也都烂尾了,能够上Flow的官网看看,这玩意到如今仍是0.X的版本,相比TypeScript的飞速发展以及TS与开发工具的深度集成尤为是VSCode,Flow真的是一言难尽好吧。不能否认,尤大本身也说本身当初是压错宝了。

也因为TS的蓬勃发展,愈来愈多的用户也在Vue中使用TS,而对Vue来讲,同时支持两个类型系统是一件比较麻烦的事情,而且在类型推导上变得很是困难。若是源代码切换到TS也就没那么多屁事了。

其次,之因此Vue2对TS的支持一塌糊涂,也是由于Options API与类型系统之间是存在断层的。

Vue的API设计开始并无针对语言自己的机制和类型系统去设计,部分缘由也是Vue开始写的时候js中甚至尚未类型系统这个玩意。

vue组件实例本质上就是个包含了一个个描述组件选项属性的对象,这种设计的好处就是更符合人类的直觉,因此这也是为何它对于新手来讲更好理解和容易上手。

可是这种设计的缺陷就是跟TypeScript这样的类型系统存在一个“断层”,这个断层怎么说呢,对于不用类型系统只关注业务逻辑的用户来讲是感知不到的。

Vue2中的optionsAPI是一个看似面向对象可是实际上却有必定误差,这就致使了它不够类型友好,尤为是对于选项来讲,类型推导是比较困难的。

但这个断层其实也是双向的:你能够说是optionsAPI的设计不够类型友好,也能够说TS还不够强大不能给Vue提供足够好的类型支持。

举个栗子,正如JSX一开始也是没有类型支持的,彻底是由于TypeScript强行给加了一整套针对JSX的类型推导机制才给了TSX如今的开发体验。因此能够这么理解,若是TypeScript当时由于JSX不属于真正的JS规范而不给它提供支持,是否是也能够说React的设计跟类型系统存在着断层呢?

那么如何在Vue中去抹平这个断层呢?一个很直接的方法就是从新设计一个类型友好的API。这个方法提及来很简单,可是对Vue来讲改一个API是须要考虑不少东西的:

  1. 与原有API的兼容性:可否同时支持新旧API?旧的用户又如何升级?像Angular2当时那样直接改的面目全非固然比较简单,但说直接点就是无论旧版本用户的死活,下场你们也清楚。如今主流的框架大版本升级都开始在版本兼容上足够重视或者下了大功夫,好比十月份正式发布的React17,这个版本没有任何新的用户层面的API,但其中一个有意思的新特性就是让一个React应用能够同时加载多个React版本,使得旧版本能够逐步升级。

  2. 如何设计出既能提供良好的类型推导,又不让类型推导而作的设计影响到非TS用户的开发体验?如何在TS和非TS的使用体验中作到一个最好的平衡一致性?像Angular那样无论非TS的用户固然也是比较简单的,可是Vue不会这么作。

咱们回顾一下Vue2里面是怎么去使用TypeScript的:

在Vue2中使用过TypeScript的话咱们基本对这两个社区方案比较熟悉了——vue-class-componentvue-property-decorator

这两个方案都是基于Class实现的,那么Vue3要作到类型友好,既然有了这么成熟的两个社区方案,在Vue3中继续沿用这个方向,基于Class设计出一个更好用的API不就简单完事了吗?

确实,在Vue3的原型阶段甚至已经实现了新的Class API,可是后面又把这个API给删了。由于class的水真的是太深了。

首先,Class API依赖于fields、decorators等提案,尤为是decorators的提案真的是太多坑了,咱们能够看看github上TC39关于decorators提案的讨论和进度:github.com/tc39/propos…

这玩意目前仍有41个在讨论中的issue,在已关闭的167个issue中比较有意思的是以前V8团队出于性能考虑直接否决掉了decorators其中的一个提案,有个老哥在底下评论说球球不要由于这个提案也推翻以前的提案,由于社区中已经有不少人在使用了,好比EmberAngularVue等等。

而decorators自己经历了这么长时间的争论,已经大改了好几回,但也仍然停留在stage2的阶段:

stage2是什么概念?能够在TC39在About中贴出来的文档中看到:

stage2意味着这个提案的东西随时可能会发生翻天覆地的变化,至少得进入stage3阶段才不会出现破坏性的改动。

那么如今TS里面的decorators还能用是由于TS实现的是decorators比较早期的一个版本,已经跟最新的decorators提案脱节了,期间decorators还通过了几回的大改。

另外,VueLoader里面用的Babel对decorators的实现和TS对decorators的实现又有不一样,这在一些比较极端的用例里面可能就会踩坑了。

因此出于Class的复杂性不肯定性,这玩意在Vue3仍是暂时不考虑了,而且Class API除了稍微好一点的类型支持之外也并无带来其余的实用性。可是为了版本兼容,Vue3中也仍然会支持刚刚提到的两个社区方案。那么抛弃了Class API,要怎么去拥抱TypeScript呢?

事实上Class的本质就是一个函数,因此一个基于function的API一样能够作到类型友好,而且能够作得更好,尤为是函数中的参数和返回值都是对类型系统很是友好的,所以这个基于函数的API就应运而生了,也就是如今Vue3中的Composition API

3. 解决开发大规模应用的需求

随着Vue被愈来愈普遍地采用,开发大型项目的需求也愈来愈多,对于这种类型的项目,首先须要的是一个像TypeScript这样的类型系统,还须要能够干净地组织可重用代码的能力

巧妙的是,基于函数的Composition API,也叫作组合API,把这些需求全都给解决了,好家伙!对于Composition API我会在第三部分中再去进一步谈谈。


How——如何优化?

关于优化,主要从两个方面谈谈:如何更快如何更小

如何更快?

  1. Object.defineProperty => Proxy
  2. 突破Virtual DOM瓶颈
  3. 更多编译时优化
  • Slot 默认编译为函数

  • 模板静态分析生成VNode优化标记——patchFlag

Object.defineProperty => Proxy

这部分咱们刚刚已经讲过了,它不只让内存占用变得更小,还让组件的初始化变得更快,那么有多快呢?

我搬运了Vue3原型阶段Vue2.5的一个初始化性能测试对比图,测试的benchmark是渲染3000个带状态的组件实例

能够看到,内存占用仅仅为Vue2的一半,初始化的速度快了将近一倍

可是,还不够!

这只是初始化,咱们看看组件更新时的优化。

突破Virtual DOM瓶颈

首先,咱们看看传统的Virtual DOM 树是如何更新的:

当数据发生改变的时候,两棵vdom的树会进行diff比较,找到须要更新的节点再patch为实际的DOM更新到浏览器上。这个过程在Vue2中已经优化到了组件的粒度,经过渲染Watcher去准确找到须要更新的组件,将整个组件内的vdom tree进行diff。这个组件粒度的优化React也作到了,只不过这个优化的操做是交给了用户,好比利用pureComponengshouldComponentUpdate等等。

但组件的粒度仍是相对比较粗的,因而Vue3重写了Virtual Dom,以利用模板的静态分析优点去将更新的粒度进一步缩小到动态元素甚至是动态的属性

咱们先看一个最简单的状况:

在传统的Virtual DOM下的diff过程:

咱们能够看到,在这个模板下,整个组件节点的结构是固定不变的,而里面有夹杂不少彻底静态的节点,只有一个节点的文本内容是动态的。而在传统的vdom下,仍然去遍历diff了这些彻底不会发生变化的节点。虽然Vue2已经对这些彻底静态的节点进行了优化标记以一种fastPath的方式去跳过这些静态节点的diff,但仍然存在一个遍历递归的过程。

那么在Vue3新的Virtual DOM下,会如何进行diff呢?

经过compiler对模板的静态分析,在优化模式下将静态的内容进行hosting,也就是把静态节点提高到外面去,实际生成vnode的就只有动态的元素<p class="text">{{ msg }}</p>,再分析这个元素内可能发生变化的东西,对这个元素打上patchFlag,表示这个元素可能发生变化的类型是文本内容textContent仍是属性类class等等。

咱们看看模板编译为render函数后的结果:

能够看到,彻底静态的元素已经被提高到render函数上面去了,实际会建立vnode的就只有一个含有动态文本内容的p元素。

因此在新的Virtual DOM下,这个组件的diff过程就变成了:

肉眼可见的,这是一个数量级的优化。

那么刚刚说了,这是一个组件节点结构彻底固定的状况,那么也就有另外一种状况:动态节点

而在Vue的模板中,出现动态节点的状况就只有两种

  1. v-if
  2. v-for

先看v-if

咱们能够看到,在v-if内部,节点结构又是彻底固定的,而且只有{{ msg }}是动态节点。因此若是把v-if划分为一个区块Block的话,又变成了咱们上一个看的那种状况。所以,只要先将整个模板看做一个Block,而后以动态指令进行划分一个个嵌套的Block,每一个Block就都变成最简单的那种状况了:

而且每一个Block里面的动态元素只须要以一个简单的打平的数组去记录跟踪便可。因此diff的过程就只是遍历递归去找那些存在动态节点的Block,根据这些动态Block中的一个数组就能够完成diff的过程。

因此刚刚这个v-if的例子的新diff过程就是:

v-for也是相同的原理,将v-for划分为一个Block:

只有 v-for 是动态节点 ,每一个 v-for 循环内部:只有 {{ item.message }} 是动态节点。它的diff过程:

总结:

  • 将模版基于动态节点指令切割为嵌套的区块
  • 每一个区块内部的节点结构是固定
  • 每一个区块只须要以一个平面数组追踪自身包含的动态节点

因此Virtual DOM的更新性能从与模板总体大小相关,提高到了只与动态内容的数量相关:

更多编译时优化

  • Slot默认编译为函数

    这个让使用插槽的父子组件之间的更新关系再也不强耦合

  • 利用模板静态分析对vnode生成的类型标记——patchFlag

    这一点咱们刚刚也讲到了,对于pacthFlag的定义,咱们能够去源码中看看(为了方便截图,我删了部分的注释,以及标注了前几个的类型的二进制值出来):

<< 就是左移操做符,咱们能够看到一共有十个动态的类型,每一个类型的数值都是在1的基础上移动不一样位数获得的,因此一个十一位的二进制数就描述了vnode的动态类型。而且尤大很是友好地告诉咱们了这个怎么用:

vnode的patchFlag经过 | 操做符去组合起来,vnode的patchFlag和某个特定类型所表明的patchFlag就用 & 操做符计算一下,若是获得的结果为0,则说明这个vnode的这个类型的属性是不会变的,不为0则相反。还引导了你去renderer.ts下看看怎么使用的,不过他的路径彷佛有点问题....我看的是packages/runtime-core/src/renderer.ts。但更深刻的内容就不在这里展开了,感兴趣的话之后能够写一篇专门讲讲这个吧。

看到尤大这个操做的时候真的是惊了,写代码还能这么玩的啊?

而后灵光一闪,我寻思写用户鉴权好像也能够这么玩吧。

So,try it now:

还蛮有意思的~

言归正传,通过了这么层层优化,Vue3究竟有多快

我去vue3.0 release时给出的数据docs.google.com/spreadsheet… 中搬运了过来:

能够看到,与Vue2相比,Vue 3在bundle包大小减小41%、初始渲染快了55%、更新快了133% 和内存使用 减小54%


如何更小?

最主要的就是充分利用了Tree-shaking的特性,那么什么是Tree-shaking呢? 中文翻译过来就是抖树,咱们来看看它的工做原理

小玩笑...

MDN上对Tree shaking的描述:

什么意思呢?为了更好地体会到它的做用,咱们先看看两种export的写法:

第一种:

const msgA = 'hhhh'

const msgB = 777

const funcA = () => {
    console.log('AAA')
}

const funcB = () => {
    console.log('BBB')
}

export default{
    msgA,
    msgB,
    funcA,
    funcB
};
复制代码

第二种:

export const msgA = 'hhhh'

export const msgB = 777

export const funcA = () => {
    console.log('AAA')
}

export const funcB = () => {
    console.log('BBB')
}
复制代码

而后我在main.ts中分别引入并使用这两个模块:

第一种:

import TreeShaking1 from "@/benchmarks/TreeShaking1"

console.log(TreeShaking1.msgA)
TreeShaking1.funcA()
复制代码

第二种:

import {funcA,msgA} from "@/benchmarks/TreeShaking2"

console.log(msgA)
// funcA()
复制代码

build之后生成的app.js bundle

第一种:

第二种:

咱们能够看到,tree shaking之后,进入bundle的只有被引入而且真正会被使用的代码块。在Vue3中许多渐进式的特性都使用了第二种的写法来进行重写,并且模板自己又是Tree shaking友好的。

但不是全部东西均可以被抖掉,有部分代码是对任何类型的应用程序都不可或缺的,咱们把这些不可或缺的部分称之为基线大小,因此Vue3尽管增长了不少的新特性,可是被压缩后的基线大小只有10KB左右,甚至不到Vue2的一半

我把刚刚的两个demo所在的项目build之后:

能够看到这个app.js的bundle只有9.68kb,这仍是包括了router在内的,而以往Vue2构建出来的广泛都在20+kb以上。


因为篇幅缘由,剩下的内容将在下一篇为你们分享。

【🚨万字警告】了不得的Vue3(下)

下一篇中,让咱们一块儿去看看Vue3给咱们带来了什么值得一看的新东西(内含精彩Demo):

  • Composition API
  • Fragment
  • Suspense
  • TelePort
  • createRenderer API
  • Vite
相关文章
相关标签/搜索