vue3新特点
# 简介
Vue.js 从 1.x 到 2.0 版本,最大的升级就是引入了虚拟 DOM 的概念,它为后续做服务端渲染以及跨端框架 Weex 提供了基础;
# vue2.x的痛点
- 源码自身的维护性;
- 数据量大后带来的渲染和更新的性能问题;
- 一些想舍弃但为了兼容一直保留的鸡肋 API 等;
- TypeScript 支持;
# Vue2与Vue3的对比
- 对TypeScript支持不友好(所有属性都放在了this对象上,难以推倒组件的数据类型)
- 大量的API挂载在Vue对象的原型上,难以实现TreeShaking。
- 架构层面对跨平台dom渲染开发支持不友好
- CompositionAPI。受ReactHook启发
- 对虚拟DOM进行了重写、对模板的编译进行了优化操作...
- 从vue2中使用的Flow换成了Typescript;
# monorepo
# 简介
monorepo是一种将多个package放在一个repo中的代码管理模式
Vue3中 使用 yarn workspace + lerna
来管理项目
"workspaces": [
"packages/*"
]
2
3
通过
workspaces
来指定需要管理的模块
# 管理源码
monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。
# 源码目录
vue-next
├─scripts
| ├─build.js
| ├─dev.js
| ├─utils.js
├─packages
| ├─global.d.ts
| ├─vue
| ├─template-explorer
| ├─size-check
| ├─shared
| ├─server-renderer
| ├─runtime-dom
| ├─runtime-core
| ├─reactivity
| ├─compiler-ssr
| ├─compiler-sfc
| ├─compiler-dom
| ├─compiler-core
├─test-dts
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- reactivity:响应式系统
- runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
- runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
- runtime-test:用于测试
- server-renderer:用于服务器端渲染
- compiler-core:与平台无关的编译器核心
- compiler-dom: 针对浏览器的编译模块
- compiler-ssr: 针对服务端渲染的编译模块
- template-explorer:用于调试编译器输出的开发工具shared:多个包之间共享的内容vue:完整版本,包括运行时和编译器
- shared:多个包之间共享的内容
- vue:完整版本,包括运行时和编译器
# 个包关系
+---------------------+
| |
| @vue/compiler-sfc |
| |
+-----+--------+------+
| |
v v
+---------------------+ +----------------------+
| | | |
+------------>| @vue/compiler-dom +--->| @vue/compiler-core |
| | | | |
+----+----+ +---------------------+ +----------------------+
| |
| vue |
| |
+----+----+ +---------------------+ +----------------------+ +-------------------+
| | | | | | |
+------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |
| | | | | |
+---------------------+ +----------------------+ +-------------------+
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# lerna
# 简介
lerna是在js项目中用来管理多个package的工具
- 全局安装
npm install lerna -g
lerna init
2
- 常用命令
lerna bootstrap # 安装依赖生成软链
lerna ls # 查看所有包
lerna publish # 发布包
2
3
# 实践
# 性能优化
# 源码体积优化
- 首先,移除一些冷门的 feature(比如 filter、inline-template 等);
- 其次,引入
tree-shaking
的技术,减少打包体积。
tree-shaking
依赖 ES2015 模块语法的静态结构(即 import 和 export),通过编译阶段的静态分析,找到没有引入的模块并打上标记。然后压缩阶段会利用例如 uglify-js、terser 等压缩工具真正地删除这些没有用到的代码。
例如,如果你在项目中没有引入 Transition、KeepAlive 等组件,那么它们对应的代码就不会打包,这样也就间接达到了减少项目引入的 Vue.js 包体积的目的。
# 数据劫持优化Proxy
Object.defineProperty
切换成 es6的Proxy
实现
Proxy 劫持了我们对 observed 对象的一些操作,比如:
- 访问对象属性会触发 get 函数;
- 设置对象属性会触发 set 函数;
- 删除对象属性会触发 deleteProperty 函数;
- in 操作符会触发 has 函数;
- 通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。
Proxy 劫持的是对象本身,并不能劫持子对象的变化,这点和 Object.defineProperty API 一致。但是 Object.defineProperty 是在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升。
# 编译优化
- Vue.js 2.x 的数据更新并触发重新渲染的粒度是组件级的,虽然 Vue 能保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vnode 树。
- vue3做到动静分离,执行diff时仅对比动态节点。
- 除此之外,Vue.js 3.0 在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法。
# diff算法优化
深度递归遍历vnode树,节点的标签和key相同认为是同一个节点则更新,不同则删除,然后处理子节点。
子节点分这几种情况处理:
纯文本、vnode 数组和空
- 空往往意味着添加或删除;
- 纯文本相同直接更新innerText,不同则删除;
- 新旧子节点都是vnode数组则diff算法来处理;
vue3.0 diff算法思想
- 编译模版时进行
静态分析
,标记动态节点
,diff对比差异时仅对比动态节点(性能提升明显); - diff算法先
去头去尾
,借此缩短遍历对比数组长度(对数组插入和删除操作性能优化明显); - 通过对更新前后子节点数组
建立映射表
的方式,将O(n^2)复杂度的遍历降低到O(n); - 通过
最长递增子序列
方法了来diff前后的子节点数组,减少移动操作的次数;
最长递增子序列算法实现:
/*
* 寻找最长递增子序列
* 使用动态规划思想,a -> c = a -> b + b -> c
* 其中p数组存储的是从p[p[i]] 到 p[i] 的最长递增子序列索引,也就是前一个b的索引;
* r数组存储最后一个元素也就是c的索引
*/
function getSequenceOfLIS(arr) {
const p = [0];
const result = [0];
for (let i = 0; i < arr.length; i ++) {
const val = arr[i];
const top = result[result.length - 1];
if (arr[top] < val) {
p[i] = top;
result.push(i);
continue;
}
// 二分法搜索
let l = 0, r = result.length - 1;
while(l < r) {
const c = (l + r) >> 1;
if (arr[result[c]] < val) {
l = c + 1;
} else {
r = c;
}
}
if (val < arr[result[l]]) {
if (l > 0) {
p[i] = result[l - 1]
}
result[l] = i;
}
}
// 回朔p数组,找出最长递增子序列
let preIndex = result[result.length - 1];
for (let i = result.length - 1; i > 0; i --) {
result[i] = preIndex;
preIndex = p[preIndex]
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 语法 API 优化
# Composition API
# 优化逻辑组织
编写组件本质就是在编写一个“包含了描述组件选项的对象”,我们把它称为 Options API,符合直觉思维。
Options API
的设计是按照 methods、computed、data、props 这些不同的选项分类,当组件小的时候,这种分类方式一目了然;但是在大型组件中,一个组件可能有多个逻辑关注点,当使用 Options API 的时候,每一个关注点都有自己的 Options,如果需要修改一个逻辑点关注点,就需要在单个文件中不断上下切换和寻找。
Composition API
将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去。
# 优化逻辑复用
- vue2 有mixin 变量命名容易冲突的问题
- Composition API 显示的将变量引入当前组件,解决明明冲突问题
- 简单组件使用Options API,复杂组件使用 Composition API
# Vue2/3 Diff算法区别
我们知道在数据变更触发页面重新渲染,会生成虚拟 DOM 并进行 patch 过程,这一过程在 Vue3 中的优化有如下
编译阶段的优化:
- 事件缓存:将事件缓存(如: @click),可以理解为变成静态的了
- 静态提升:第一次创建静态节点时保存,后续直接复用
- 添加静态标记:给节点添加静态标记,以优化 Diff 过程
由于编译阶段的优化,除了能更快的生成虚拟 DOM 以外,还使得 Diff 时可以跳过"永远不会变化的节点",Diff 优化如下
- Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff
- 使用最长递增子序列优化了对比流程
根据尤大公布的数据就是 Vue3 update
性能提升了 1.3~2 倍