模版编译流程
Vue3 模版编译就是把 template 字符串编译成渲染函数
// template
<div><p>{{LH_R}}</p></div>
// render
import {toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock} from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createElementBlock("div", null, [_createElementVNode("p", null, _toDisplayString(_ctx.LH_R), 1 /* TEXT */)
]))
}
我会依照编译流程分 3 步剖析
- parse:将模版字符串转换成模版 AST
- transform:将模版 AST 转换为用于形容渲染函数的 AST
-
generate:依据 AST 生成渲染函数
export function baseCompile( template: string | RootNode, options: CompilerOptions = {}): CodegenResult { // ... const ast = isString(template) ? baseParse(template, options) : template const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(prefixIdentifiers) transform( ast, extend({}, options, { prefixIdentifiers, nodeTransforms: [ ...nodeTransforms, ...(options.nodeTransforms || []) // user transforms ], directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // user transforms) }) ) return generate( ast, extend({}, options, {prefixIdentifiers}) ) }
parse
- parse 对模版字符串进行遍历,而后循环判断开始标签和完结标签把字符串宰割成一个个token,存在一个 token 列表,而后扫描 token 列表并保护一个开始标签栈,每当扫描一个开始标签节点,就将其压入栈顶,栈顶的节点始终作为下一个扫描的节点的父节点。这样,当所有 Token 扫描实现后,即可构建成一颗树形 AST
-
以下是简化版 parseChildren 源码,是 parse 的主入口
function parseChildren( context: ParserContext, mode: TextModes, ancestors: ElementNode[] // 节点栈构造,用于保护节点嵌套关系): TemplateChildNode[] { // 获取父节点 const parent = last(ancestors) const ns = parent ? parent.ns : Namespaces.HTML const nodes: TemplateChildNode[] = [] // 存储解析进去的 AST 子节点 // 遇到闭合标签完结解析 while (!isEnd(context, mode, ancestors)) { // 切割解决的模版字符串 const s = context.source let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined if (mode === TextModes.DATA || mode === TextModes.RCDATA) {if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {// 解析插值表达式{{}} node = parseInterpolation(context, mode) } else if (mode === TextModes.DATA && s[0] === '<') {if (s[1] === '!') {// 解析正文节点和文档申明...} else if (s[1] === '/') {if (s[2] === '>') { // 针对自闭合标签,后退三个字符 advanceBy(context, 3) continue } else if (/[a-z]/i.test(s[2])) { // 解析完结标签 parseTag(context, TagType.End, parent) continue } else { // 如果不合乎上述情况,就作为伪正文解析 node = parseBogusComment(context) } } else if (/[a-z]/i.test(s[1])) { // 解析 html 开始标签,取得解析到的 AST 节点 node = parseElement(context, ancestors) } } } if (!node) { // 一般文本节点 node = parseText(context, mode) } // 如果节点是数组,就遍历增加到 nodes 中 if (isArray(node)) {for (let i = 0; i < node.length; i++) {pushNode(nodes, node[i]) } } else {pushNode(nodes, node) } } return nodes }
就拿 <div><p>LH_R</p></div>
模版举例
- div 开始标签入栈,context.source =
<p>LH_R</p></div>
,ancestors =[div]
- p 开始标签入栈,context.source =
LH_R</p></div>
,ancestors =[div, p]
- 解析文本
LH_R
- 解析 p 完结标签,p 标签出栈
- 解析 div 完结标签,div 标签出栈
- 栈空,模版解析结束
transform
- transform 采纳深度优先的形式对 AST 进行遍历,在遍历过程中,对节点的操作与转换采纳插件化架构,都封装为独立的函数,而后转换函数通过
context.nodeTransforms
来注册 - 转换过程是优先转换子节点,因为有的父节点的转换依赖子节点
-
以下是 AST 遍历
traverseNode
外围源码/* 遍历 AST 节点树,通过 node 转换器对以后节点进行 node 转换 子节点全副遍历实现后执行对应指令的 onExit 回调退出转换 */ export function traverseNode( node: RootNode | TemplateChildNode, context: TransformContext ) { // 记录以后正在遍历的节点 context.currentNode = node /* nodeTransforms:transformElement、transformExpression、transformText... transformElement:负责整个节点层面的转换 transformExpression:负责节点中表达式的转化 transformText:负责节点中文本的转换 */ const {nodeTransforms} = context const exitFns = [] // 顺次调用转换工具 for (let i = 0; i < nodeTransforms.length; i++) { /* 转换器只负责生成 onExit 回调,onExit 函数才是执行转换主逻辑的中央,为什么要推到栈中先不执行呢?因为要等到子节点都转换实现挂载 gencodeNode 后,也就是深度遍历实现后 再执行以后节点栈中的 onExit,这样保障了子节点的表达式全副生成结束 */ const onExit = nodeTransforms[i](node, context) if (onExit) {if (isArray(onExit)) { // v-if、v-for 为结构化指令,其 onExit 是数组模式 exitFns.push(...onExit) } else {exitFns.push(onExit) } } if (!context.currentNode) { // node was removed 节点被移除 return } else { // node may have been replaced // 因为在转换的过程中节点可能被替换,复原到之前的节点 node = context.currentNode } } switch (node.type) { case NodeTypes.COMMENT: if (!context.ssr) { // inject import for the Comment symbol, which is needed for creating // comment nodes with `createVNode` // 须要导入 createComment 辅助函数 context.helper(CREATE_COMMENT) } break case NodeTypes.INTERPOLATION: // no need to traverse, but we need to inject toString helper if (!context.ssr) {context.helper(TO_DISPLAY_STRING) } break // for container types, further traverse downwards case NodeTypes.IF: // 对 v -if 生成的节点束进行遍历 for (let i = 0; i < node.branches.length; i++) {traverseNode(node.branches[i], context) } break case NodeTypes.IF_BRANCH: case NodeTypes.FOR: case NodeTypes.ELEMENT: case NodeTypes.ROOT: // 遍历子节点 traverseChildren(node, context) break } // 以后节点树遍历实现,顺次执行栈中的指令退出回调 onExit context.currentNode = node let i = exitFns.length while (i--) {exitFns[i]()} }
generate
generate
生成代码大抵分为 3 步
-
创立代码生成上下文,因为该上下文对象是用于保护代码生成过程中程序的运行状态,如:
code
:最终生成的渲染函数push
:拼接代码indent
:代码缩进deindent
:缩小代码缩进- …
-
生成渲染函数的前置预设局部
- module 模式下:
genModulePreamble()
- function 模式下:
genFunctionPreamble
- 还有一些函数名,参数,作用域 …
- module 模式下:
-
生成渲染函数
- 通过调用
genNode
,而后在genNode
外部通过 switch 语句来匹配不同类型的节点,并调用对应的生成器函数
- 通过调用
参考资料
- Vue3 模板编译原理
- 《Vue.js 设计与实现》