Vue3 的编译模块蕴含 4 个目录:
compiler-core // 编译外围
Compiler-DOM // 浏览器相干
Compiler-sfc // 单文件组件
Compiler-SSR // 服务端渲染
其中,compiler-core 模块是 Vue 编译的外围模块,与平台无关。其余三个基于 compiler-core,实用于不同的平台。
Vue 的编译分为三个阶段,即 解析 (Parse)、转换(Transform) 和代码生成(Codegen)。
Parse 阶段将模板字符串转换为语法形象树 AST。Transform 阶段对 AST 做一些转换解决。Codegen 阶段依据 AST 生成相应的渲染函数字符串。
Parse 阶段
剖析模板字符串时,Vue 可分为两种状况:以< 结尾的字符串,和不是以 < 结尾的字符串。
不是以 < 结尾的字符串有两种状况:文本节点或者插入表达式 {{exp}}。
应用 < 将字符串的结尾分为以下几种状况:
- 元素开始标签 <div>
- 元素完结标签 </div>
- 正文节点 <!– 123 –>
- 文件申明 <!DOCTYPE html>
用伪代码示意,近似过程如下:
while (s.length) {if (startsWith(s, '{{')) { // 如果开始为 '{{'
node = parseInterpolation(context, mode)
} else if (s[0] === '<') { // 元素开始标签
if (s[1] === '!') {if (startsWith(s, '<!--')) { // 正文节点
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) { // 文档语句
node = parseBogusComment(context)
}
} else if (s[1] === '/') { // 完结标签
parseTag(context, TagType.End, parent)
} else if (/[a-z]/i.test(s[1])) { // 开始标签名
node = parseElement(context, ancestors)
}
} else { // 一般文本节点
node = parseText(context, mode)
}
}
原始代码点这里 vue-next parse.ts
绝对应的几个函数如下:
parseChildren()
, 入口函数parseInterpolation()
,剖析双花插值表达式parseComment()
,解析正文parseBogusComment()
,剖析文件申明parseTag()
,剖析标签parseElement()
,剖析元素节点,它将在外部执行parseTag()
parseText()
,剖析一般文本parseAttribute()
,分析属性
当标签、文本、正文等每个节点生成相应的 AST 节点时,Vue 将截断解析的字符串。
字符串被截断是应用 AdvanceBy(context,numberOfCharacters)函数,context 是字符串的上下文对象,numberOfCharacters是要截断的字符数。
应用一个简略的示例来模仿截断操作:
<div name="test">
<p></p>
</div>
首先剖析 <div,而后执行 advanceBy(context,4) 截断操作(外部执行 s=s.slice(4)),变成:
name="test">
<p></p>
</div>
而后分析属性,并将其截断为:
<p></p>
</div>
相似地,以下内容的截断为:
></p>
</div>
</div>
<!-- 所有字符串都已解析 -->
所有 AST 节点定义都在 Compiler-core/astts 文件中,上面是元素节点的定义:
export interface BaseElementNode extends Node {
TYPE: NODETYPES.EEMENT / / Type 类型
NS: namespace // 名称空间默认为 html, ie 0
Tag: String // 标签名称
tagType: ElementTypes // 元素类型
IsselfClosing: boolean // 是否为自闭标记, 例如 <hr />
Props: Array <Attribute | DirectiveNode> // 属性, 蕴含 Html 属性和指令
Children: TemplateChildNode [] // 子级模板指向}
用一个比较复杂的例子来解释解析过程。
<div name="test">
<!-- This is a comment-->
<p>{{test}}</p>
A text node
<div>good job!</div>
</div>
下面的模板字符串假设为 S,第一个字符 S[0] 在开始时为 <,这意味着它只能是方才提到的四种状况之一。
再看看 S[1] 第二个字符的规定:
- 遇到 ! 时,调用字符串原始办法 startsWith(),剖析是 <!–的结尾,还是 <!DOCTYPE 的结尾,它们对应的处理函数不同,例子中代码最终将解析到正文节点。
- 如果是 /,按完结标签。
- 如果不是 /,按开始标签解决。
在咱们的示例中,这是一个 <div> 开始标签。
这里要提到的一点,Vue 将应用栈来保留已解析的元素标签。当遇到开始标记时,标签被推入栈中。当遇到完结标记时,将弹出栈。它的作用是保留已解析但尚未解析完的元素标签。在这个栈中还有另一个角色,通过 stack[stack.length-1],能够失去它的父元素。
从咱们的例子来看,在解析过程中,栈中存储如下:
1. [div] // div 入栈
2. [div, P] // p 入栈
3. [div] // P 弹出
4. [div, div] // div 入栈
5. [div] // div 弹出
6. [] // 最初一个 div 弹出后,模板字符串已解析,栈为空。
依照下面的例子,接下来将截断 <div 字符串,并解析其属性。
属性有两种状况:
- HTML 的一般属性
- Vue 的指令
生成的类型节点值,HTML 一般属性节点类型为 6,Vue 指令节点类型为 7。
所有节点类型值详情如下:
Root, // 根节点为 0
Element, // 元素节点为 1
Text, // 文本节点为 2
Comment, // 正文节点为 3
Simple_expression, // 简略表达式为 4
Interpolation, // 双花插值 {{}} 为 5
Attribute, // 属性为 6
Directive, // 指令为 7
属性剖析后,div 开始标签被剖析结束,<div name=”test”> 此行字符串被截断。其余字符串当初如下所示:
<!-- This is a comment -->
<p>{{test}}</p>
A text node
<div>good job!</div>
</div>
正文文本和一般文本节点解析规定比较简单简略,间接截断,生成节点。正文节点调用 parseComment() 函数解决,Text 节点调用 parseText() 解决。
双花插值 {{test}} 的字符串解决逻辑略微简单一些:
- 首先提取出双括号内的内容,即 test,调用 trim 函数去掉两边空格。
- 而后生成两个节点,一个节点为 INTERPOLATION 类型值为 5,示意它是一个双花插值。
- 第二个节点是其内容 test,将生成节点为 Simple_expression,类型值为 4。
return {
TYPE: NODETYPES.ITERPOLATION, // 双花括号类型
content: {
type: NodeTypes.SIMPLE_EXPRESSION, // 简略表达式类型
Isstatic: false, // 不是动态节点
isConstant: false,
content,
loc: getSelection(context, innerStart, innerEnd)
},
loc: getSelection(context, start)
}
字符串解析逻辑的其余部分与上述内容相似,因而未对其进行解释。示例解析 AST 如下所示:
从 AST 中,还能够看到一些节点上的其余属性:
- NS,命名空间,通常为 HTML,值为 0
- LOC,它是一条地位音讯,批示此节点位于源 HTML 字符串的地位,蕴含行、列、偏移量等信息。
- {{test}} 解析后的节点将具备 isStatic属性,该值为 false,示意这是一个动静节点。如果是动态节点,则只生成一次,并且会复用雷同的节点,不须要进行差别比拟。
还有一个标签类型值,它有 4 个值:
export const enum ElementTypes {
ELEMENT, // 0 元素节点
Component, // 1 正文节点
Slot, // 2 插槽节点
Template // 3 模板
}
次要用于辨别以上四种类型的节点。
Transform 阶段
在转换阶段,Vue 将对 AST 执行一些转换操作,次要是依据 CodeGen 阶段 应用的不同 AST 节点增加不同的选项参数。以下是一些重要的选项:
cacheHandlers 缓存处理程序
如果 CacheHandlers 的值为 true,则启用函数缓存。例如 @click=”foo” 默认状况下编译为 {onClick:foo},如果关上此选项,则编译为:
{onClick: _cache[0] || (_cache[0] = e => _ctx.foo(e)) } // 具备缓存性能
hoistStatic 动态晋升
hoistStatic 是一个标识符,示意是否应启用动态节点晋升。如果值为 true,动态节点将被晋升在 render() 函数内部,生成名为 _hoisted_x 的变量。
例如,文本 A text node 生成的代码为 const hoisted_2 = / # pure / createtextVNode (“a text node”)。
在上面两张图片中,前一张为 hoistStatic=false,后一张为 hoistStatic=true,都能够本人尝试一下 地址。
prefixIdentifiers 前缀标识
此参数的角色用于代码生成。例如,{{foo}} 模块 (module) 模式下生成的代码是 _ctx.foo,函数 (function) 模式下生成的代码是 width(this){…}。因为在模块 (module) 模式下,默认为严格模式,不能应用 with 语句。
PatchFlags 补丁标识
转换为 AST 节点时,应用 PatchFlag 参数,该参数次要用于差别比拟 diff 过程。当 DOM 节点具备此标记且大于 0 时,它将被更新,并且不会跳过。
来看看 PatchFlag 的值:
export const enum PatchFlags {
// 动静文本节点
TEXT = 1,
// 动静类
CLASS = 1 << 1, // 2
// 动静 Style
STYLE = 1 << 2, // 4
// 动静属性,但不包含 calss 和 style
// 如果是组件,则能够蕴含 calss 和 style。PROPS = 1 << 3, // 8
// 具备动静键属性,当键更改时,须要进行残缺的 DIFF 差别比拟
FULL_PROPS = 1 << 4, // 16
// 具备侦听事件的节点
HYDRATE_EVENTS = 1 << 5, // 32
// 不扭转子序列的片段
STABLE_FRAGMENT = 1 << 6, // 64
// 具备 key 属性的片段或局部子字节具备 key
KEYED_FRAGMENT = 1 << 7, // 128
// 子节点没有密钥的 key
UNKEYED_FRAGMENT = 1 << 8, // 256
// 节点将仅执行 non-PROPS 比拟
NEED_PATCH = 1 << 9, // 512
// 动静插槽
DYNAMIC_SLOTS = 1 << 10, // 1024
// 动态节点
HOISTED = -1,
// 退出 DIFF 差别比拟优化模式
BAIL = -2
}
从下面的代码能够看出,PatchFlag 应用 bit-map 来示意不同的值,每个值都有不同的含意。Vue 会在 diff 过程中依据不同的修补标记应用不同的修补办法。
下图为变换后的 AST:
能够看到 CodegenNode、Helpers 和 Hoists 已填充了相应的值。CodegenNode 是生成要应用的代码的数据。Hoists 存储动态节点。Helpers 存储创立 vNode 的函数名(实际上是 Symbol)。
在正式开始转换之前,须要创立一个 transformContext,即转换上下文。与这三个属性相干的数据和办法如下:
helpers: new Set(),
hoists: [],
// methods
helper(name) {context.helpers.add(name)
return name
},
helperString(name) {return `_${helperNameMap[context.helper(name)]}`
},
hoist(exp) {context.hoists.push(exp)
const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`,
false,
exp.loc,
true
)
identifier.hoisted = exp
return identifier
},
让咱们来看看具体的转换过程是如何应用的。用 <p>{{test}}</p> 举例说明。
此节点对应 TransformElement() 转换函数,因为 p 没有绑定动静属性,没有绑定指令,所以焦点不在它下面。而 {{test}} 是一个双花插值表达式,所以将其 patchflag 设置为 1(动静文本节点),相应的执行代码 patchFlag |=1。而后执行 createVNodeCall() 函数,其返回值为该节点的 codegennode 值。
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* disableTracking */,
node.loc
)
createVNodeCall() 会相应的在 createVNode() 中增加一个符号,它搁置在 helpers 中。事实上,helpers 性能将在代码生成阶段引入。
// createVNodeCall () 外部执行过程,多余代码已删除
context.helper(CREATE_VNODE)
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
loc
}
hoists 晋升
是否将节点晋升,次要看它是否是动态节点。
<div name = "test"> // 动态属性节点
<! - This is a comment->
<p>{{test}}</p>
A text node // 动态节点
<div> good job! </div> // 动态节点
</div>
能够看到,下面有三个动态节点,因而 hoists 数组有 3 个值。正文为什么不算动态节点,临时还没有找到起因。。。
TYPE changes 类型扭转
从上图中能够看出,最外层 div 的类型为 1,由 Transform 生成的 CodeGen node 中的类型为 13。
这 13 是 VNODE_CALL 对应的类型值,其余还有:
// codegen
VNODE_CALL, // 13
JS_CALL_EXPRESSION, // 14
JS_OBJECT_EXPRESSION, // 15
JS_PROPERTY, // 16
JS_ARRAY_EXPRESSION, // 17
JS_FUNCTION_EXPRESSION, // 18
JS_CONDITIONAL_EXPRESSION, // 19
JS_CACHE_EXPRESSION, // 20
方才提到的例子 {{test}}, 其 codegen node是 createVnodeCall 函数生成。
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
loc
}
从下面的代码能够看出,type 设置为 nodetypes.VNODE_CALL,即 13。
每个不同的节点由不同的变换函数解决。能够本人再深刻的理解。
Codegen 阶段
代码生成阶段最初生成了一个字符串,去掉了字符串的双引号,具体内容是什么:
const _Vue = Vue
const {createVNode: _createVNode, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode} = _Vue
const _hoisted_1 = {name: "test"}
const _hoisted_2 = / * # __ pure __ * / _ createtextVNode ("a text node")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
return function render(_ctx, _cache) {with (_ctx) {const { createCommentVNode: _createCommentVNode, toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createBlock: _createBlock} = _Vue
return (_openBlock(), _createBlock("div", _hoisted_1, [_CreateCommentVNode ("This is a comment"),
_createVNode("p", null, _toDisplayString(test), 1 /* TEXT */),
_hoisted_2,
_hoisted_3
]))
}
}
代码生成模式
能够看到下面的代码最终返回了 render() 函数,生成相应的 VNODE。
实际上,代码生成有两种模式:模块和函数。选取哪种模式由前缀标识符决定。
函数模式性能:应用 const {helpers…}=Vue 获取帮忙函数的办法,即 createVode()、createCommentVNode() 这些函数,最初返回 render() 函数。
模块模式为:应用 ES6 模块导入导出性能,即 import 和 export。
Static node 动态节点
此外,还有三个变量。以 hoisted 命名,前面跟数字,示意这是动态变量。
看看解析阶段的 HTML 模板字符串:
<div name="test">
<! - This is a comment->
<p>{{test}}</p>
A text node
<div>good job!</div>
</div>
这个示例只有一个动静节点,即 {{test},其余的都是动态节点。从生成的代码中还能够看出,生成的节点和模板中的代码对应于一个或多个节点。动态节点的作用是只生成一次,当前间接重用。
仔细的你可能会发现 Highed_2 和 Highed_3 变量有一个 /#\_PURE_/ 的正文。
此正文的 作用是表明此性能是纯性能,无副作用,次要用于 Tree-shaking 。压缩工具将间接从打包时未应用的代码中删除。
来看下一代动静节点,{{test}} 生成代码对应为 _createVNode(“p”, null, _toDisplayString(test), 1 / TEXT /)。
其中,_toDisplayString(test) 的外部实现是:
return val == null
? ''
: isObject(val)
? JSON.stringify(val, replacer, 2)
: String(val)
该代码非常简单,它是一个字符串转换输入。
_createVNode(“p”, null, _toDisplayString(test), 1 / TEXT /) 的最初一个参数减少转换时的 Patchflag 值。
Help function 辅助函数
在 Transform 和 Codegen 阶段,都看到了 helpers 辅助函数的影子,它是什么呢?
Name mapping for runtime helpers that need to be imported from 'vue' in
generated code. Make sure these are correctly exported in the runtime!
Using `any` here because TS doesn't allow symbols as index type.
// 须要从生成代码中的“vue”导入的运行时帮忙程序的名称映射。// 确保这些文件在运行时正确导出!// 此处应用 'any',因为 TS 不容许将符号作为索引类型。export const helperNameMap: any = {[FRAGMENT]: `Fragment`,
[TELEPORT]: `Teleport`,
[SUSPENSE]: `Suspense`,
[KEEP_ALIVE]: `KeepAlive`,
[BASE_TRANSITION]: `BaseTransition`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
[CREATE_VNODE]: `createVNode`,
[CREATE_COMMENT]: `createCommentVNode`,
[CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[WITH_DIRECTIVES]: `withDirectives`,
[RENDER_LIST]: `renderList`,
[RENDER_SLOT]: `renderSlot`,
[CREATE_SLOTS]: `createSlots`,
[TO_DISPLAY_STRING]: `toDisplayString`,
[MERGE_PROPS]: `mergeProps`,
[TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize`,
[CAPITALIZE]: `capitalize`,
[SET_BLOCK_TRACKING]: `setBlockTracking`,
[PUSH_SCOPE_ID]: `pushScopeId`,
[POP_SCOPE_ID]: `popScopeId`,
[WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`
}
export function registerRuntimeHelpers(helpers: any) {Object.getOwnPropertySymbols(helpers).forEach(s => {helperNameMap[s] = helpers[s]
})
}
事实上,帮忙函数是 Vue 在代码生成时引入的一些函数,因而程序能够失常执行,从下面生成的代码能够看出。helperNameMap 是默认的映射表名,它是要从 Vue 引入的函数名。
此外,咱们还能够看到一个注册函数。registerRuntimeHelpers(helpers: any() 是做什么用的呢?
咱们晓得编译模块的编译器外围是一个独立于平台的,而 编译 Dom是一个与浏览器相干的编译模块。要在浏览器中运行 Vue 程序,请导入与浏览器相干的 Vue 数据和性能。
registerRuntimeHelpers(helpers: any() 用于执行此操作,能够从 Compiler-dom 的 runtimehelpers.ts 文件中看到:
registerRuntimeHelpers({[V_MODEL_RADIO]: `vModelRadio`,
[V_MODEL_CHECKBOX]: `vModelCheckbox`,
[V_MODEL_TEXT]: `vModelText`,
[V_MODEL_SELECT]: `vModelSelect`,
[V_MODEL_DYNAMIC]: `vModelDynamic`,
[V_ON_WITH_MODIFIERS]: `withModifiers`,
[V_ON_WITH_KEYS]: `withKeys`,
[V_SHOW]: `vShow`,
[TRANSITION]: `Transition`,
[TRANSITION_GROUP]: `TransitionGroup`
})
运行 registerRuntimeHelpers(helpers: any() 映射表被注入与浏览器相干的函数。
如何应用这些辅助函数?
在解析阶段,解析不同节点时会生成相应的类型。
在转换阶段,生成一个辅助对象,它是一个汇合数据结构。每当转换 AST 时,都会依据 AST 节点的类型增加不同的帮忙器函数。
例如,假如当初正在转换正文节点,它将执行 context.helper(CREATE_COMMENT),外部通过 helpers.add(‘createCommentVNode’) 增加。
而后在 Codegen 阶段,遍历 helpers,从 Vue 导入所需的函数,代码实现如下:
// 这是模块模式
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(',')} } from ${JSON.stringify(runtimeModuleName)}\n`
如何生成代码?
从 Codegen.ts 文件中,能够看到许多代码生成函数:
Generate () // 入口文件
GenfunctionExpression () // 生成函数表达式
Gennode () // 生成 vNode 节点
...
生成代码是基于不同的 AST 节点调用不同的代码生成函数,最初将代码字符串拼合在一起,输入残缺的代码字符串。
老规矩,还是看一个例子:
const _hoisted_1 = {name: "test"}
const _hoisted_2 = / * # __ pure __ * / _ createtextVNode ("a text node")
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "good job!", -1 /* HOISTED */)
看看这段代码是如何生成的,外部会执行 genHoists(ast.hoists, context),晋升的动态节点作为第一个参数,genHoists() 外部简化实现:
hoists.forEach((exp, i) => {if (exp) {push(`const _hoisted_${i + 1} = `);
genNode(exp, context);
newline();}
})
从下面的代码能够看出,遍历 hoists 数组,调用 genNode(exp, context) 函数。genNode() 依据不同的类型执行不同的性能。
const _hoisted_1 = {name: "test"}
这一行的 const _hoisted_1 = 通过 genHoists() 函数生成,{name: “test”} 是通过 genObjectExpression() 函数生成。