Vue中的AST
2021-10-19
| 2023-2-19
0  |  0 分钟
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流程是区分SSRWeb编译的, 本篇只看Web侧的编译过程
AST的解析过程主要分为四种情况
  1. 注释文本解析
  1. 普通文本节点解析
  1. 插值解析
  1. 元素节点解析

自顶向下的解析过程

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函数

主要的作用有三个:
  1. 解析template生成AST
  1. AST转换
  1. 生成代码
 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

  1. 创建解析上下文
  1. 解析子节点, 并创建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节点数组

  1. 自顶向下分析代码, 生成nodes
  1. 空白字符管理, 提高编译效率
 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 元素节点的解析

处理的就是核心的各种标签, 组件等节点
主要做的流程就是
  1. 解析开始标签
  1. 解析子节点
  1. 解析闭合标签
 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.typeelement, 且tagTypeelementcomponent, 则返回. 也就是只有它是普通节点的时候才会往下走.
否则返回一个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 }
源码
  • Vue
  • Vue源码
  • AST
  • Vue中组件的生命周期实现机制Vue中AST如何生成可运行代码
    目录