Vue 3.0 目前已经公开源代码了,可以去https://github.com/vuejs/vue-next查看。关于Vue 3.0 相信大家都好奇很久了,之前的 RFC 讨论也是相当激烈。而这一版本在设计上的一些理念,这里会给大家介绍一下,大家可以在前面内容的基础上来进行渐进式学习,这与 Vue 框架的“渐进式框架”定位也比较相符。
1. Vue 3.0 兼容性
对于大多数开发者来说,尤其是经历过 Angular 断崖式升级的小伙伴们,对框架升级最担心的是兼容性。新能力大家当然想用,但对于项目和业务来说,稳定性是首位的,所以很多人会担心框架升级是否部分功能不可用。Vue 3.0 有不少新的想法和改动,但基本上兼容性大部分是没问题的。总体来看,除渲染函数 API 和scoped-slot
语法之外,其余均保持不变或者将通过另外构建一个兼容包来兼容 2.x。
根据作者介绍的 Vue 3.0 计划中,以下是计划中的公共 API 更改:
- 模板语法将保持 99%不变。
scoped-slot
语法可能会有一些小的调整,但是除此之外,没有计划更改模板的其他任何内容 - API 在设计时考虑了 TypeScript 的类型推断,3.x 代码库本身将使用 TypeScript 编写,并提供增强的 TypeScript 支持。也就是说,在应用程序中使用 TypeScript 仍然是完全可选的
- 顶层 API 可能会进行较大的调整,以避免在安装插件时会修改 Vue 的运行时。相反,插件的作用域会局限在组件树中
- 函数性的组件将支持纯函数,但是将需要通过辅助函数来显式创建异步组件
- 变化最大的部分是渲染功函数(render)能中使用的虚拟 DOM 的格式
也有人会好奇,是否 Vue 3.0 出来之后,之前对 Vue 的积累和理解都需要重新学习呢?其实不是这样的,Vue 3.0 是基于原有的框架设计上,引入了一些新的理念。代码是重构了,但是思想是演进的。可以理解为我们的项目跑了两三年之后,发现除了不少好用的新技术和能力,我们就要考虑是否要进行优化或是重构了。
2. 设计目标
总的来说,Vue 3.0 的设计目标主要有以下几点:
- 更小
- 更快
- 加强 API 设计一致性
- 加强 Typescript 支持
- 提高自身可维护性
- 开放更多底层功能
我们一个个来看一下,Vue 3.0 的一些设计理念和实现方式调整,以及对开发者来说要怎样理解。以下内容基本上有了解 Vue 3.0 的开发者都已经从作者的分享、网上的 PPT 资源或是介绍说明中接触到,这里依然会先介绍一下。
2.1 更小
Tree-shaking
什么是 Tree-shaking 呢?我们在进行项目开发的时候,经常会需要引入一些开源库。但是大多数时候我们都只会使用到这个开源库中的一小部分内容,Tree-shaking 用于描述移除 JavaScript 上下文中的未引用代码。也就是说,我们在打包代码的时候,会自动移除未用到的一些特性和功能代码,以此来减少生成的代码文件大小。
在 Vue 3.0 中,全局 API 和内置组件、功能都将支持 Tree-shaking。
~10kb gzipped
新的运行时,代码尺寸将控制在 10kb gzipped 上下。
2.2 更快
Virtual DOM
传统 Virtual DOM 有以下性能瓶颈:
(1) 虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 Virtual DOM 树。
(2) 在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费。
(3) 传统 Virtual DOM 的性能跟模版大小正相关,跟动态节点的数量无关。
在 Vue 3.0 中,Virtual DOM 会完全重写,通过动静结合的模式来进行突破,更新速度和内存占用均有质的提升,整体比 Vue2 性能提升 2-5 倍。具体实现方式包括:
(1) 通过模版静态分析生成更优化的 Virtual DOM 渲染函数,将模版切分为 block(if
, for
, slot
)。
(2) 每个 block 内部动态节点位置是固定的,同时每个 block 的根节点会记录自己所包含的动态节点(包含子 block 的根节点)。
(3) 更新时只需要直接遍历动态节点,Virtual DOM 的更新性能与模版大小解耦,变为与动态节点的数量相关。
这里可以简单理解为,Virtual DOM 的更新从以前的整体作用域调整为树状作用域,树状的结构会带来算法的简化以及性能的提升。
观察者机制
Proxy 对象是什么呢,它用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),通过new Proxy(target, handler)
来创建,参数说明如下:
表 16-1 new Proxy()
参数说明
参数 | 说明 |
---|---|
target |
用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理) |
handler |
一个对象,其属性是当执行一个操作时定义代理的行为的函数 |
同时handler
参数中,常用的方法包括:
-
handler.get()
: 用于拦截对象的读取属性操作 -
handler.set()
: 用于拦截设置属性值的操作 -
handler.getPrototypeOf()
: 当读取代理对象的原型时,该方法就会被调用 -
handler.has()
: 针对in
操作符的代理方法
我们能看到,当我们给某个对象添加了代理之后,就可以改变一些原有的行为,或是通过钩子的方式添加一些处理,我们用来触发界面更新、其他数据更新等也是可以的。
相比于之前的 getter/setter 监控数据变更,Vue 3.0 将会是基于 Proxy 的变动侦测,通过代理的方式来监控变动,整体性能会得到优化。同时从长远来看,JS 引擎会持续优化 Proxy(getter/setter 基本不会变化)。
编译时优化
编译器架构重构,更多编译时(compile-time)会得到优化,以减少 runtime 开销。
2.3 加强 API 设计一致性
Vue 3.0 整体使用 Function-based API,对比 Class API,拥有以下优势:
- 更灵活的逻辑复用能力
- 更好的 TypeScript 类型推导支持
- 更好的性能
- Tree-shaking 友好
- 代码更容易被压缩
2.4 加强 Typescript 支持
Vue 3.0 本身用 TypeScript 重写,内置 typing,支持 TSX,同时不会影响不使用 TS 的用户。
2.5 开放更多底层功能
底层 API 会更多地开放给开发者使用,例如自定义渲染能力:
import { createRenderer } from "@vue/runtime-core"; const { render } = createRenderer({ nodeOps, patchData });
以上内容,在之前的一些 Vue 分享会中也有相关说明。如今相关的源代码也已经出来了,虽然还是 Pre Alpha 阶段,但我们可以通过这部分源码来结合理解一下 Vue 3.0 是进行了怎样的进化。
3. Vue 3.0 Pre Alpha
2019 年 10 月 5 日,Vue 3.0 Pre Alpha 阶段的源代码(https://github.com/vuejs/vue-next)已经公开了。我们来看一下Vue 3.0 的设计是否都实现了,大概又是怎样实现的。
3.1 代码 README 说明
我们去看源代码,通常会不知道如何下手。即使是代码注释比较充分,但是数量的确是很多,要怎么从中理清楚主要的逻辑和思路呢?一般来说我们可以从 README 开始,例如我们来看看 Vue 3.0 Pre Alpha 的说明,这里主要讲了两块的内容:编译器 Compiler 和运行时 Runtime。
其实前面宣传和分享的时候,给大家介绍的更多是目标,而源代码能更好地表达如何实现。显然,对于更快、更小、对开发者更友好等种种目标,Vue 3.0 是从编译器和运行时两大块来着手处理实现的。
编译器 Compiler
关于编译器,代码说明是包括以下调整:
- 模块化架构
- "Block tree"优化
- 更激进的 static tree hoisting 功能
- Source Map 支持
- 内置标识符前缀(又名“stripWith”,它会删除生成的渲染函数中的
with
用法,使它们兼容严格模式) - 内置整齐打印(pretty-print)功能
- 移除 Source Map 和标识符前缀后,精简至 10kb 左右大小的 brotli 压缩的浏览器版本
运行时 Runtime
关于运行时,包括以下的优化内容:
- 速度显著提升
- 同时支持 Composition API 和 Options API,并支持 typings
- 基于 Proxy 的变更检测
- 支持 Fragments、Portals、Suspense w/ async setup()
2.x 功能迁移
在该 Pre Alpha 阶段代码里,还有一些 2.x 的功能尚未完成:
- 服务器端渲染
<keep-alive>
<transition>
- 编译器对 DOM 特定修饰符的转换,包括
v-on
、v-model
、v-text
、v-pre
、v-once
、v-html
、v-show
等,部分已完成
此外,该版本的实现还需要运行时环境中的本机 ES2015+,且尚未支持 IE11。
3.2 源码欣赏
我们下载了源代码之后,可以看到有以下的 packages:
表 16-2 Vue 3.0 Pre Alpha 源码 packages
packages | 说明 |
---|---|
compiler-core | 编译器核心,包括将模板转成 AST、生成 JS 逻辑等功能 |
compiler-dom | 编译器 DOM 补充部分,与浏览器相关的模板编译和逻辑处理等功能 |
reactivity | 数据相关的系统,包括数据监控(Proxy 代理)、计算和响应等基础能力 |
runtime-core | 运行时核心,包括 Vue 组件系统、Vue 的各种 API 实现、虚拟 DOM 相关,同时暴露了更多的底层能力如自定义渲染器 |
runtime-dom | 运行时 DOM 相关补充,包括处理原生 DOM API、DOM 事件和 DOM 属性等 |
runtime-test | 用于测试的运行时,该运行时基于虚拟 DOM 的 JS 对象,可以用在所有 JS 环境里 |
server-renderer | SSR 服务端渲染 |
shared | 包含一些通用的配置和方法 |
template-explorer | 模板编译输出,方便调试 |
vue | 用于构建完整版本的 Vue,使用了编译器和运行时 |
我们看到完整版本的 Vue 里引用了 compiler-dom 和 runtime-dom,但这两者其实还依赖了 compiler-core、runtime-core、reactivity,也就是最终完整的的 Vue 3.0 Pre Alpha 版本了。
@vue/complier-core
翻开源码,我们其实能看到和 Vue2.x 相比一个很大的改变:整体的功能代码进行了解耦,每个功能模块都清晰独立地抽离出来了。还可以开放更多的底层能力给到开发者。
Vue 3.0 整体使用 Function-based API,同时使用 Typescript 进行开发。除了可读性、维护性上的提升,之前提到的一些优势也都体现出来了:模块化解耦获得了更灵活的逻辑复用能力、Tree-shaking、Source Map 支持更加友好,开发者也获得更好的 TypeScript 类型推导支持。
以 complier-core 中 index.ts 中的代码为例:
// @vue/compiler-core/src/index.ts // 函数式 API 可便捷地对逻辑进行解耦和复用 // 能看到 complier-core 中功能主要包括模板解析、转成AST、生成JS逻辑等一些功能 import { parse, ParserOptions } from "./parse"; import { transform, TransformOptions } from "./transform"; import { generate, CodegenOptions, CodegenResult } from "./codegen"; import { RootNode } from "./ast"; import { isString } from "@vue/shared"; import { transformIf } from "./transforms/vIf"; import { transformFor } from "./transforms/vFor"; import { transformExpression } from "./transforms/transformExpression"; import { transformSlotOutlet } from "./transforms/transformSlotOutlet"; import { transformElement } from "./transforms/transformElement"; import { transformOn } from "./transforms/vOn"; import { transformBind } from "./transforms/vBind"; import { defaultOnError, createCompilerError, ErrorCodes } from "./errors"; import { trackSlotScopes, trackVForSlotScopes } from "./transforms/vSlot"; import { optimizeText } from "./transforms/optimizeText"; import { transformOnce } from "./transforms/vOnce"; import { transformModel } from "./transforms/vModel"; export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions; // 我们将其命名为“baseCompile”,以便更高级别的编译器(例如 @vue/compiler-dom) // 可以在重新导出其他所有内容的同时导出“compile” export function baseCompile( template: string | RootNode, options: CompilerOptions = {} ): CodegenResult { // 省略一部分 // 这里先解析模板,然后生成AST对象 const ast = isString(template) ? parse(template, options) : template; const prefixIdentifiers = !__BROWSER__ && (options.prefixIdentifiers === true || options.mode === "module"); // 对每个AST对象节点进行处理 transform(ast, { ...options, prefixIdentifiers, nodeTransforms: [ transformIf, transformFor, ...(prefixIdentifiers ? [ // 此处顺序很重要 trackVForSlotScopes, transformExpression ] : []), trackSlotScopes, transformSlotOutlet, transformElement, optimizeText, // 开放给使用者添加的一些处理逻辑 // 例如 compiler-dom 中新增了对 DOM 的一些处理 ...(options.nodeTransforms || []) ], directiveTransforms: { on: transformOn, bind: transformBind, once: transformOnce, model: transformModel, // 开放给使用者添加的一些处理逻辑 ...(options.directiveTransforms || {}) } }); // 将AST对象生成VNode、JS逻辑等 return generate(ast, { ...options, // 开放给使用者添加的一些转换逻辑 prefixIdentifiers }); }
从上面的代码,我们能看到,模板解析生成 AST 之后,还会根据 AST 对象来进行转换。而 Vue 3.0 中提到的 Virtual DOM 优化的内容也在这里进行,通过模版静态分析生成更优化的 Virtual DOM 渲染函数,将模版切分为 block(if, for, slot),来提升 Virtual DOM 的性能。例如对某些语法进行“block”分块,然后整个应用以树状将每个“block”串起来,这样在更新、数据对比监控的时候,都可以从上而下地进行,也可以将变更控制在更小的范围,从而提升性能。
其他的功能设计也跟这里比较相似,通过函数式 API 来解耦基础能力,同时留空一些可补充的能力。功能拆解后,不管是组装的灵活性,还是可测试、易拓展、Tree-shaking 等,都有质的提升。
@vue/complier-dom
同样地,要了解这个模块主要是用来做什么,我们也需要首先打开 index.ts 文件:
// @vue/compiler-dom/src/index.ts // 依赖了@vue/compiler-core import { baseCompile, CompilerOptions, CodegenResult } from "@vue/compiler-core"; // 与 DOM 相关的一些解析和转换 import { parserOptionsMinimal } from "./parserOptionsMinimal"; import { parserOptionsStandard } from "./parserOptionsStandard"; import { transformStyle } from "./transforms/transformStyle"; import { transformCloak } from "./transforms/vCloak"; import { transformVHtml } from "./transforms/vHtml"; import { transformVText } from "./transforms/vText"; import { transformModel } from "./transforms/vModel"; // 导出补充了DOM相关的“compile”模块 export function compile( template: string, options: CompilerOptions = {} ): CodegenResult { // 基于 @vue/compiler-core 中的 baseCompile 进行补充拓展 // 从而导出一个完整的 Vue 3.0 版本的编译器 return baseCompile(template, { ...options, ...(__BROWSER__ ? parserOptionsMinimal : parserOptionsStandard), nodeTransforms: [transformStyle, ...(options.nodeTransforms || [])], directiveTransforms: { cloak: transformCloak, html: transformVHtml, text: transformVText, model: transformModel, // 此处重写了 compiler-core 的 vModel 转换 ...(options.directiveTransforms || {}) } }); } export * from "@vue/compiler-core";
显然,compiler-dom 模块是基于 compiler-core 模块补齐 DOM 相关能力,我们可以理解为运行在浏览器中需要的一些能力补齐。这里我们能看到,complier-dom 中重写了 compiler-core 的 vModel 转换,因为在 Vue 中v-model
只能用在表单或是自定义表单的组件,我们来看一下实现:
// @vue/compiler-dom/src/transforms/vModel.ts export const transformModel: DirectiveTransform = (dir, node, context) => { const res = baseTransform(dir, node, context); const { tag, tagType } = node; if (tagType === ElementTypes.ELEMENT) { // 其他省略 // 显然,这里对原生的表单组件进行处理 if (tag === "input" || tag === "textarea" || tag === "select") { let directiveToUse = V_MODEL_TEXT; if (tag === "input") { const type = findProp(node, `type`); if (type) { if (type.type === NodeTypes.DIRECTIVE) { // :type="foo" directiveToUse = V_MODEL_DYNAMIC; } else if (type.value) { switch (type.value.content) { case "radio": directiveToUse = V_MODEL_RADIO; break; case "checkbox": directiveToUse = V_MODEL_CHECKBOX; break; } } } } else if (tag === "select") { directiveToUse = V_MODEL_SELECT; } // 通过 needRuntime 返回辅助符号 // 导入将替换 resovleDirective 调用 // 注入运行时指令 res.needRuntime = context.helper(directiveToUse); } else { // 报错,此处省略 } } return res; };
其实在 Vue 中,除了原生表单组件 input、textarea、select 这些,还有一些自定义组件也需要进行处理的,这里也需要补齐对自定义组件的处理,这部分我们会在 runtime-dom 运行时 DOM 补充的部分能看到。
@vue/reactivity
该模块实现了一个数据管理模块,它可以单独使用,也可以与框架配合使用。我们能看到 Vue 3.0 的数据监控的优化亮点都包含在这个模块了,这个模块主要包括了:
- baseHandlers/collectionHandlers: 处理器,用作 Proxy 的处理器
- computed: 计算属性部分
- effect: 一些辅助能力,例如跟踪依赖项、触发一些检测、响应等等
- reactive: 响应式数据,基于 Proxy 代理实现
- ref: 数据容器,在 Vue 中是指向组件实例的引用
我们可以看看创建响应式数据的部分:
// @vue/reactivity/src/reactive.ts // 创建响应式数据 function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // 目标已经有相应的代理 let observed = toProxy.get(target); if (observed !== void 0) { return observed; } // 目标已经是一个代理 if (toRaw.has(target)) { return target; } // 只有在白名单里的值可被观察 if (!canObserve(target)) { return target; } // 获取集合或是非集合类型的处理器 const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers; // 创建代理,并记录源对象和代理对象的关系 observed = new Proxy(target, handlers); toProxy.set(target, observed); toRaw.set(observed, target); // 存储{target-> key-> dep}关系 if (!targetMap.has(target)) { targetMap.set(target, new Map()); } return observed; }
我们已经可以看到,通过 Proxy 的方式创建一个响应式数据,并将数据之间的依赖关系、源数据与代理对象之间的关系都记录下来,当我们更新数据的时候,就可以根据依赖进行范围内的检查更新:
// @vue/reactivity/src/baseHandlers.ts // 这是响应式数据 set 的时候代理处理函数 function set( target: any, key: string | symbol, value: any, receiver: any ): boolean { // 获取代理对象数据 value = toRaw(value); // 获取原对象数据 const hadKey = hasOwn(target, key); const oldValue = target[key]; if (isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } // 采用 Reflect.set 方法将值赋值给对象的属性 // 确保完成原有的行为,然后再部署额外的功能 const result = Reflect.set(target, key, value, receiver); // 如果目标是原始原型链中的某个对象,请勿触发 // 可防止循环触发 if (target === toRaw(receiver)) { // 计算数据变更方式(更新或新增),并进行变更触发 // trigger 会根据依赖关系,对 computed 和需要连带更新的数据进行更新 if (__DEV__) { const extraInfo = { oldValue, newValue: value }; if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo); } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key, extraInfo); } } else { if (!hadKey) { trigger(target, OperationTypes.ADD, key); } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key); } } } return result; }
我们能看到,使用了 Proxy 代理+依赖关系链的方式来进行数据检测和变更更新,同时结合运行时 DOM 相关补充,就可以完成对页面的局部刷新。
@vue/runtime-core
运行时核心模块主要包括了组件系统和 Vue 的 API 实现、虚拟 DOM 相关的功能,我们同样地可以从 index.ts 文件中看到:
// @vue/runtime-core/src/index.ts // 公共API部分------------ export { createComponent } from "./apiCreateComponent"; export { nextTick } from "./scheduler"; export * from "./apiReactivity"; export * from "./apiWatch"; export * from "./apiLifecycle"; export * from "./apiInject"; // 高级API部分-------------- // 需要进行原始渲染功能的用户可以使用 export { h } from "./h"; // VNode(虚拟DOM)相关能力 export { createVNode, cloneVNode, mergeProps, openBlock, createBlock } from "./vnode"; // VNode类型的symbols export { Text, Comment, Fragment, Portal, Suspense } from "./vnode"; // VNode的标志 export { PublicShapeFlags as ShapeFlags } from "./shapeFlags"; export { PublicPatchFlags as PatchFlags } from "@vue/shared"; // 可用于高级插件 export { getCurrentInstance } from "./component"; // 可用于自定义渲染 export { createRenderer } from "./createRenderer"; export { warn } from "./warning"; export { handleError, callWithErrorHandling, callWithAsyncErrorHandling } from "./errorHandling"; // 内部API,用于编译器生成的代码 // 应该与'@vue/compiler-core/src/runtimeConstants.ts'同步 export { applyDirectives } from "./directives"; export { resolveComponent, resolveDirective } from "./helpers/resolveAssets"; export { renderList } from "./helpers/renderList"; export { toString } from "./helpers/toString"; export { toHandlers } from "./helpers/toHandlers"; export { renderSlot } from "./helpers/renderSlot"; export { createSlots } from "./helpers/createSlots"; export { capitalize, camelize } from "@vue/shared"; // 内部API,用于与运行时编译器集成 export { registerRuntimeCompiler } from "./component";
我们能看到,runtime-core 暴露了一些底层 API 能力,例如自定义渲染等,开发者可以基于这些基础能力之上,补充平台相关能力,打造一个完整的运行时。像@vue/runtime-dom 就时补充了 DOM 相关的运行时能力,从而构成完整的 Vue 运行时功能模块。
@vue/runtime-dom
runtime-dom 模块是基于 runtime-core 开发的浏览器上的运行时,主要补齐了浏览器环境下 DOM 节点和节点属性的一些渲染和更新能力。
怎么补齐呢?我们看一个例子:
// @vue/runtime-dom/src/nodeOps.ts // 对一些 VNode 操作,增加更新到页面的 DOM 节点操作 // 获取 document 对象 const doc = document; export const nodeOps = { insert: (child: Node, parent: Node, anchor?: Node) => { if (anchor != null) { parent.insertBefore(child, anchor); } else { parent.appendChild(child); } }, remove: (child: Node) => { const parent = child.parentNode; if (parent != null) { parent.removeChild(child); } }, createElement: (tag: string, isSVG?: boolean): Element => isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag), querySelector: (selector: string): Element | null => doc.querySelector(selector) // 篇幅原因,省略一部分内容 };
我们可以通过runtime-core
提供的自定义渲染能力,把 DOM 节点增删改查的能力添加进去:
// @vue/runtime-dom/src/index.ts import { createRenderer } from '@vue/runtime-core' import { nodeOps } from './nodeOps' import { patchProp } from './patchProp' const { render, createApp } = createRenderer<Node, Element>({ patchProp, ...nodeOps }) export { render, createApp }
所以在 Vue 3.0 版本中,可以通过 render 和 createApp 这两个 API 来进行初始化项目和页面渲染。
其他模块
对于其他模块(runtime-test、server-renderer、shared、template-explorer、vue),前面也有简单介绍,篇幅关系就不再一个个描述了。
回顾一下 Vue 3.0 的目标(更小、更快、加强 API 设计一致性、加强 Typescript 支持、提高自身可维护性、开放更多底层功能),其实基本上都达成了。
重构是一件很花精力、考验团队能力的事情,尤其是在 Vue 本身就很受欢迎的情况下,依然决定要进行重构。虽然偶尔会有些“学不动”的捣乱声音,但作为技术人,该有的追求还是要有的,如今互联网世界日新月异,止步不前只会面临被淘汰。而 Vue 3.0 的这一番举动,个人觉得真的是很棒的一个决定。