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 来进行初始化项目和页面渲染。