password
Created
Feb 19, 2023 03:40 PM
type
Post
status
Published
date
Oct 19, 2021
slug
summary
Vue中的AST
tags
Vue
Vue源码
AST
category
源码
icon
Vue3中
AST
流程是区分SSR
和Web
编译的, 本篇只看Web
侧的编译过程AST
的解析过程主要分为四种情况- 注释文本解析
- 普通文本节点解析
- 插值解析
- 元素节点解析
自顶向下的解析过程
Compile 编译入口
Compile是一个柯里化函数, 本质上是调用了baseCompile来处理编译过程, 而隐藏了一些默认的配置信息
function compile(template, options = {}) { return baseCompile(template, extend({}, parserOptions, options, { nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])], directiveTransforms: extend({}, DOMDirectiveTransforms, options.directiveTransforms || {}), transformHoist: null })) }
BaseCompile 调用的源compile函数
主要的作用有三个:
- 解析template生成AST
- AST转换
- 生成代码
function baseCompile(template, options = {}) { const prefixIdentifiers = false // 解析template生成AST const ast = isString(template) ? baseParse(template, options) : template const [nodeTransforms, directiveTransforms] = getBaseTransformPreset() // AST转换 transform(ast, extend({}, options, { prefixIdentifiers, nodeTransforms: [ ...nodeTransforms, ...(options.nodeTransforms || []) ], directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {}) })) // 生成代码 return generate(ast, extend({}, options, { prefixIdentifiers })) }
BaseParse 解析template生成AST
- 创建解析上下文
- 解析子节点, 并创建AST
function baseParse(content , options = {}) { // 创建解析上下文 const context = createParserContext(content, options) const start = getCursor(context) // 解析子节点, 并创建AST return createRoot(parseChildren(context, 0, []), getSelection(context, start)) }
CreateParserContext 创建解析上下文
// 默认的解析配置 const defaultParserOptions = { delimiters: [`{{`, `}}`], getNamespace: () => 0 /* HTML */, getTextMode: () => 0 /* DATA */, isVoidTag: NO, isPreTag: NO, isCustomElement: NO, decodeEntities: (rawText) => rawText.replace(decodeRE, (_, p1) => decodeMap[p1]), onError: defaultOnError } function createParserContext(context, options) { return { options: extend({}, defaultParserOptions, options), column: 1, line: 1, // 当前代码相对于原始代码的偏移量 offset: 0, // 原始代码 originalSource: content, // 当前代码 source: content, // 当前代码是否在pre标签内 inPre: false, // 当前代码是否在vpre环境下 inVPre: false } }
ParseChildren 解析创建AST节点数组
- 自顶向下分析代码, 生成nodes
- 空白字符管理, 提高编译效率
function parseChildren(context, mode, ancestors) { // 父节点 const parent = last(ancestors) const ns = parent ? parent.ns : 0 const nodes = [] while(!isEnd(context, mode, ancestors)) { const s = context.source let node = undefined if (mode === 0 || mode === 1) { if (!context.inVPre && startsWith(s, context.options.delimiters[0])) { // 处理 {{ 插值代码 node = parseInterpolation(context, mode) } else if (mode === 0 && s[0] === '<') { // 处理 < 开头的代码 if (s.length === 1) { // s 长度为1, 说明代码结尾是<, 报错 emitError(context, 5, 1)} } else if (s[1] === '!') { if (startsWith(s, '<!--')) { // 处理注释节点 node = parseComment(context) } else if (startsWith(s, '<![CDATA[')) { // 处理<![CDATA[节点 node = parseCDATA(context, ancestors) } else { node = parseBogusComment(context) } } } else if (s[1] === '/') { // 处理 </ 结束标签 // 一堆前置判断, 省略 if (/[a-z]/i.test(s[1])) { // 解析标签元素节点 parseElement(context, ancestors) } } } // node依然为undefined, 则为普通文本节点 if (!node) { node = parseText(context, mode) } if (isArray(node)) { // 如果node是数组, 则遍历添加 for (let i = 0; i < node.length; i++) { pushNode(nodes, node[i]) } } else ( // 添加单个node pushNode(nodes, node) ) } }
具体的解析流程
注释节点的解析在此处省略, 主要就是生成注释AST和通过
advanceBy
来进行解析指针移动的过程1. ParseInterpolation 插值的解析
以
{{
和 }}
开头结尾的环境下, 且不在VPre
的环境下, 则会进行调用, 对插值进行解析function parseInterpolation(context, mode) { // 从配置中获取插值开始和结束分隔符 {{ 和 }} const [open , close] = context.options.delimiters const closeIndex = context.source.indexOf(close, open.length) // 如果关闭符号不在当前的代码块中, 则返回undefined, 这部分代码省略 // ... const start = getCursor(context) // 代码前进到插值开始分隔符后 advanceBy(context, open.length) // 内部插值开始位置 const innerStart = getCursor(context) // 内部插值结束位置 const innerEnd = getCursor(context) // 插值原始内容的长度 const rawContentLength = closeIndex - open.length // 插值原始内容 const rawContent = context.source.slice(0, rawContentLength) // 获取插值的内容, 并前进代码到插值的内容后 const preTrimContent = parseTextData(context, rawContentLength, mode) const content = preTrimContent.trim() // 内容相对于插值开始分隔符的头偏移 const startOffset = preTrimContent.indexOf(content) if (startOffset > 0) { // 更新内部插值开始位置 advancePositionWithMutation(innerStart, rawContent, startOffset) } // 内容相对于插值内舒分隔符的尾偏移 const endOffset = rawContentLength - (preTrimContent.length - content.length - startOffset) // 更新内部插值结束位置 advancePositionWithMutation(innerEnd, rawContent, endOffset) // 前进代码到插值结束分隔符猴 advanceBy(context, close.length) return { type: 5, content: { type: 4, isStatic: false, isConstant: false, content, loc: getSelection(context, innerStart, innerEnd) }, loc: getSelection(context, start) } }
总得来说, 插值解析就是对
{{
和}}
中间的内容进行各种场景处理的过程, 找到原始的中间内容, 及真正有意义的中间插值逻辑, 然后转换为AST
数据2. ParseText 对文本的解析
function parseText(content, mode) { // CDATA相关内容, 省略 // ... let endIndex = context.source.length // 遍历结束符, 匹配找到结束的位置 for (let i = 0; i < endTokens.length; i++) } const index = context.source.indexOf(endTokens[i], 1) if (index !== -1 && endIndex > index) { endIndex = index } } const start = getCursor(context) // 获取文本的内容, 并前进代码到文本的内容后 const content = parseTextData(context, endIndex, mode) return { type: 2, content, loc: getSelection(context, start) } }
3. ParseElement 元素节点的解析
处理的就是核心的各种标签, 组件等节点
主要做的流程就是
- 解析开始标签
- 解析子节点
- 解析闭合标签
function parseElement(context, ancestors) { // 是否在pre标签内 const wasInPre = context.inPre // 是否在v-pre指令内 const wasInVPre = context.inVPre // 获取当前元素的父标签节点 const parent = last(ancestors) // 解析开始标签, 生成一个标签节点, 并前进代码到开始标签后 const element = parseTag(context, 0, parent) // 是否在pre标签的边界 const isPreBoundary = context.inPre && !wasInPre // 是否在v-pre指令的边界 const isVPreBoundary = context.inVPre && !wasInVPre // 自闭合标签, 直接返回标签节点 if (element.isSelfClosing || context.options.isVoidTag(element.tag)) { return element } // 接下来处理子节点 // 先处理标签节点, 添加到ancestors, 入栈 ancestors.push(element) const mode = context.options.getTextMode(element, parent) // 递归解析子节点, 传入ancestors const children = parseChildren(context, mode, ancestors) // ancestors出栈 ancestors.pop() // 添加到children属性中 element.children = children // 接下来处理闭合标签 // 结束标签 if (startsWithEndTagOpen(context.source, element.tag)) { // 解析闭合标签, 并前进代码到结束标签后 // 这个时候开始解析这个标签的相关信息内容 parseTag(context, 1, parent) } else { // 抛错, 没有结束标签 } }
ParseTag 解析并创建一个标签节点
function parseTag(context, type, parent) { // 标签打开 const start = getCursor(context) // 匹配标签文本结束的位置 const match = /^<\/?([a-z][^\t\r\n\f/>])/i.exec(context.source) const tag = match[1] const ns = context.options.getNamespace(tag, parent); // 前进代码到标签文本结束位置 advanceBy(context, match[0].length) // 前进代码到标签文本后面的空白字符后 advanceSpaces(context) // 保存当前状态乙方我们需要用`v-pre`重新解析属性 const cursor = getCursor(context) const currentSource = context.source // 解析标签中的属性变成属性数组, 并前进代码到属性后 let props = parseAttributes(context, type) // 检查是不是一个pre标签 if (context.options.isPreTag(tag)) { context.inPre = true } // 检查属性中有没有v-pre指令 if (!context.inVPre && props.some(p => p.type ===7 && p.name === 'pre')) { context.inVPre = true // 重置context extend(context, cursor) context.source = currentSource props = parseAttributes(context, type).filter(p => p.name !== 'v-pre') } // 标签闭合 let isSelfClosing = false // 内容物为空, 且判断是否自闭合标签, 是则报错, 省略 // ... let tagType = 0 const options = context.options // 判断标签类型, 是组件, 插槽, 还是模板 if (!context.inVPre && !options.isCustomElement(tag)) { // 判断是否有is属性 const hasVIs = props.some(p => p.type === 7 && p.name === 'is') if (options.isNativeTag && !hasVIs) { // 这时候是一个组件节点 if (!options.isNativeTag(tag)) tagType = 1 } else if (hasVIs || isCoreComponent(tag) || (options.isBuiltInComponent && options.isBuiltInComponent(tag)) || /![A-Z]/.test(tag) || tag === 'component') { tagType = 1 // component } if (tag === 'slot') { tagType = 2 // slot } else if (tag === 'template' && props.some(p => p.type === 7 && isSpecialTemplateDirective(p.name))) { tagType = 3 // template } } return { type: 1, // element ns, // 标签名 tag, // 标签类型 tagType, props, isSelfClosing, // children先初始化为空 children: [], // 代码的位置 loc: getSelection(context, start), codegenNode: undefined } }
使用CreateRoot来完成整个AST的链接
function createRoot(children, loc = locStub) { return { type: 0, // root children, helpers: [], components: [], directives: [], hoists: [], imports: [], cached: 0, temps: 0, codegenNode: undefined, loc } }
AST的转换
使用getBaseTransformPreset获取节点和指令转换的方法
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset() // AST转换 transform(ast, extend({}, options, { prefixIdentifiers, nodeTransforms: [ ...nodeTransforms, // 用户自定义transforms ...(options.nodeTransforms || []) ], directiveTransforms: [ ...directiveTransforms, // 用户自定义transforms ...(options.directiveTransforms || []) ], }))
transform
function transform(root, options) { // 创建transform上下文 const context = createTransformContext(root, options) // 遍历AST节点 traverseNode(root, context) // 静态提升 if (options.hoistStatic) { hoistStatic(root, context) } if (!options.ssr) { createRootCodegen(root, context) } root.helpers = [...context.helpers] root.components = [...context.components] root.directives = [...context.directives] root.imports = [...context.imports] root.hoists = context.hoists root.remps = context.temps root.cached = context.cached }
createTransformContext 创建transform上下文
主要是维护了transform过程中的一些配置, 状态数据 等等, 一个很大的对象, 代码可以看
packages/compiler-core/src/transform.ts
, 代码就不贴了traverseNode 遍历AST节点
核心的转换过程就是在此函数中实现的
主要就是在递归遍历AST节点
function traverseNode(node, context) { context.currentNode = node const { nodeTransforms } = context const exitFns = [] for (let i = 0; i < nodeTransforms.length; i++) { // 有些转换函数会设计一个退出函数, 在处理完子节点后执行 const onExit = nodeTransforms[i](node, context) if (onExit) { if (isArray(onExit)) { exitFns.push(...onExit) } else { exitFns.push(onExit) } } if (!context.currentNode) { // 节点被移除, 直接返回 return } else { // 转换过程中节点可能会被替换, 需要恢复到之前的节点 node = context.currentNode } } switch (node.type) { case 3: // COMMENT // 需要导入createComment辅助函数 if (!context.ssr) { context.helper(CREATE_COMMENT) } break case 5: // INTERPOLATION // 需要导入toString辅助函数 if (!context.ssr) { context.helper(TO_DISPLAY_STRING) } break case 9: // IF for (let i = 0; i < node.branches.length; i++) { traverseNode(node.branches[i], context) } break case 10: // IF_BRANCH case 11: // FOR case 1: // ELEMENT case 0: // ROOT traverseChildren(node, context) break } // 执行转换函数返回的退出函数 let i = exitFns.length while (i--) { exitFns[i]() } }
transformElement 节点转换函数
主要是判断, 如果不符合
node.type
是element
, 且tagType
是element
或component
, 则返回. 也就是只有它是普通节点的时候才会往下走.否则返回一个
postTransformElement
的函数, 也就是刚刚的exitFns
退出函数postTransformElement
主要是对节点的props
children
等的处理其中还有做一些优化, 如: 当有整块的无动态内容(
BlockTree
)的时候, 则跳过这里的动态追踪, 这个其实是一个不错的优化, 特别是在模板中动态内容少, 静态内容多的时候, 能够极大提升渲染效率.接下来就是处理节点的
props
, 使用buildProps
来处理返回propsBuildResult
, 通过这个返回对象进一步处理vnodeProps
(props) patchFlag
(更新标识) dynamicPropNames
(动态prop名称) directives
(指令)directives会经过
createArrayExpression(directives.map(dir => buildDirectiveArgs(dir, context)))
来进一步处理成为vnodeDirectives
接下来就是对节点的子节点的处理(插槽), 其中还会对
keep-alive
, teleport
等情况进行处理postTransformElement
则是通过createVNodeCall
来创建一个codegenNode
, 也就是代码生成节点.后续的阶段, 则会通过
createNNodeCall
这个对象来生成目标代码transformExpression 表达式节点转换函数
表达式节点并不存在子节点, 因此也不需要退出函数, 只有在nodejs环境下, 或web的非生产环境下才会调用此函数, 在web的生产环境下依然和vue2一样, 使用的是
with
来对表达式进行处理.transformText 文本节点转换函数
主要目的是合并一些相邻的文本节点, 为内部的每一个子文本节点创建一个调用函数表达式的代码生成节点.
静态文本节点和动态插值节点都会被视为一个文本节点
const transformText = (node, context) => { // ROOT || ELEMENT || FOR || IF_BRANCH if (node.type === 0 || node.type === 1 || node.type === 11 || node.type === 10) { // 在节点退出时执行转换, 保证所有表达式都已经被处理 return () => { const children = node.children let currentContainer = undefined let hasText = false // 将相邻节点合并 for (let i = 0; i < children.length; i++) { const child = children[i] if (isText(child)) { hasText = true for (let j = i + 1; j < children.length; j++) { const next = children[j] // 如果后面的是文本节点, 且currentContainer为false, 则创建一个复合的文本节点 if (isText(next)) { if (!currentContainer) { // 创建复合表达式节点 currentContainer = children[i] = { type: 8, // COMPOUND_EXPRESSION loc: child.loc, children: [child] } } currentContainer.children.push(`+`, next) children.splice(j, 1) j-- } else { currentContainer = undefined } } } } // 如果是一个带有单文本子元素的纯元素节点, 什么都不需要转换 // 这种情况在运行时可以直接设置元素的textContent来更新文本 if (!hasText || (children.length === 1 && (node.type === 0 || (node.type === 1 && node.tagType === 0)))) { return } // 为子文本节点创建一个调用函数表达式的代码生成节点 for (let i = 0; i< children.length; i++) { const child = children[i] // COMPOUND_EXPRESSION 符合表达式 if (isText(child) || child.type === 8) { const callArgs = [] // 为createTextVNode添加执行函数 if (child.type !== 2 || child.content !== '') { callArgs.push(child) } // 标记动态文本 if (!context.ssr && child.type !== 2) { callArgs.push(`${1 /* TEXT */} ${PatchFlagNames[1 /* TEXT */]}) } children[i] = { type: 12, // TEXT_CALL content: child, loc: child.loc, codegenNode: createCallExpression(context.helper(CREATE_TEXT), callArgs) } } } } } }
createCallExpression
的逻辑则很简单, 只是创建一个类型为JS_CALL_EXPRESSION
的生成节点function createCallExpression(callee, args = [], loc = locStub) { return { type: 14, loc, callee, arguments: args } }
创建的函数表达式所生成的节点, 对应的函数名是
createTextVNode
参数
callArgs
是子节点本身的child
, 如果是动态插值节点, 那么参数还会多一个TEXT的patchFlag
v-if节点转换函数
v-if的转换核心目的是为了对内容进行条件性的渲染, 所以需要对整个节点树进行转换以满足条件渲染的需要
const transformIf = createStructuralDirectiveTransform(/^(if|else|else-if)$/, (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { return () => { // 退出回调函数, 当所有子节点转换完成执行 } }) })
createStructuralDirectiveTransform
则主要是处理这几个结构化的指令, 并给其定义一个fn的退出函数function createStructuralDirectiveTransform(name, fn) { const matches = isString(name) ? (n) => n === name : (n) => name.test(n) return (node, context) => { // 只处理元素节点, 因为只有元素节点才有v-if指令 if (node.type === 1) { const { props } = node // 结构化指令转换与插槽无关, 插槽相关处理逻辑在vSlot.ts中 if (node.tagType === 3 && props.some(isVSlot)) return const exitFns = [] for (let i = 0; i< props.length; i++) { const prop = props[i] // 7: DIRECTIVE if (prop.type === 7 && matches(prop.name)) { // 删除结构指令以避免无限递归 props.splice(i, 1) i-- const onExit = fn(node, props, context) if (onExit) exitFns.push(onExit) } } return exitFns } } }
退出函数
fn(node, props, context)
则主要是使用processIf
来进行处理function processIf(node, dir, context, processCodegen) { if (dir.name === 'if') { // 创建分支节点 const branch = createIfBranch(node, dir) // 创建IF节点, 替换当前节点 const ifNode = { type: 9, // IF loc: node.loc, branches: [branch] } context.replaceNode(ifNode) if (processCodegen) { return processCodegen(ifNode, branch, true) } } else { // 处理v-if相邻节点, 如v-else-if 或 v-else const siblings = context.parent.children let i = siblings.indexOf(node) while(i-- >= -1) { const sibling = siblings[i] // IF if (sibling && sibling.type === 9) { // 把节点移动到IF节点的branches中 context.removeNode() const branch = createIfBranch(node, dir) sibling.branches.push(branch) const onExit = processCodegen && processCodegen(sibling, branch, false) // 因为分支已被删除, 所以他的子节点需要在这里遍历 traverseNode(branch, context) if (onExit) onExit() // 恢复currentNode为null, 因为它已经被删除 context.currentNode = null } else { // 报错逻辑 } break } } }
createIfBranch
用来创建分支节点, 核心是添加了type
, 和condition
参数, 来包裹原有的节点function createIfBranch(node, dir) { return { type: 10, // IF_BRANCH loc: node.loc, condition: dir.name === 'else' ? undefined : dir.exp, // 如果是template, 就直接用template的子节点, 否则就用原来的node的数组 children: node.tagType === 3 ? node.children : [node] } }
接下来我们再看
v-if
节点转换函数的退出函数return () => { if (isRoot) { // v-if节点的退出函数, 创建if节点的codegenNode ifNode.codegenNode = createCodegenNodeForBranch(branch, 0, context) } else { // v-else-if v-else节点的退出函数 // 将此分支的codegenNode附加到上一个条件节点的codegenNode的alternate中 let parentCondition = ifNode.codegenNode while(parentCondition.alternate.type === 19) { parentCondition = parentCondition.alternate } // 更新候选节点 parentCondition.alternate = createCodegenNodeForBranch(branch, ifNode, branches.length - 1, context) } }
createCodegenNodeForBranch
function createCodegenNodeForBranch(branch, index, context) { if (branch.condition) { return createConditionalExpression(branch.condition, createChildrenCodegenNode(branch, index, context), createCallExpression(context.helper(CREATE_COMMENT), [ (process.env.NODE_ENV !== 'production') ? '"v-if"' : '""', 'true' ])) } else { return createChildrenCodegenNode(branch, index, context) } }
createConditionalExpression
分支节点存在condition
的时候, 如v-if
, v-else-if
, 它通过createConditionalExpression
返回一个条件表达式节点function createConditionalExpression(test, consequent, alternate, newline = true) { return { type: 19, // JS_CONDITIONAL_EXPRESSION test, // if主branch的子节点对应的代码生成节点 consequent, // 候补branch子节点的代码生成节点 alternate, newline, loc: locStub } }
createChildrenCodegenNode
判断每一个分支子节点是不是一个vnodeCall
, 如果不是子节点, 则转换成一个blockCall
function createChildrenCodegenNode(branch, index, context) { const { helper } = context // 根据index创建key属性 const keyProperty = createObjectProperty(`key`, createSimpleExpression(index + '', false)) const { children } = branch const firstChild = children[0] const needFragmentWrapper = children.length !== 1 || firstChild.type !== ` if (needFragmentWrapper) { if (children.length === 1 && firstChild.type === 11) { const vnodeCall = firstChild.codegenNode injectProp(vnodeCall, keyProperty, context) return vnodeCall } else { return createVNodeCall(context, helper(FRAGMENT), createObjectExpression([keyProperty]), children, `${64 /* STABLE_FRAGMENT */} ${PatchFlagNames[64 /* STABLE_FRAGMENT */]}`, undefined, undefined, true, false, branch.loc) } } else { const vnodeCall = firstChild.codegenNode // 吧createVNode改变为createBlock if(vnodeCall.type === 13 && (firstChild.tagType !== 1 || vnodeCall.tag === TELEPORT)) { vnodeCall.isBlock = true // 创建block的辅助代码 helper(OPEN_BLOCK) helper(CREATE_BLOCK) } injectProp(vnodeCall, keyProperty, context) return vnodeCall } }
Hoist 静态提升
Hoist
是vue3中的一项新特性, 核心是通过静态提升的方式来避免静态无数据绑定的节点的重复渲染以提高性能例如: 我们有以下的代码
<p> hello {{ msg + test }}</p> <p>static</p> <p>static</p>
通过静态提升, 第二行代码将会被编译再渲染函数之外
const _hoisted_1 = /*#__PURE__*/_createVNode('p', null, 'static', -1) const _hoisted_2 = /*#__PURE__*/_createVNode('p', null, 'static', -1) export function render(_ctx, _cache) { return (_optnBlock(), createBlock(_Fragment, null, [ _createVNode('p', null, 'hello' + _toDisplayString(_ctx.msg | _ctx.test), 1), _hoisted_1, _hoisted_2 ], 64)) // STABLE_FRAGMENT }