Vue3模版编译原理

模版编译流程

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步分析

  1. parse:将模版字符串转换成模版AST
  2. transform:将模版AST转换为用于描述渲染函数的AST
  3. 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>流程举例
  1. div开始标签入栈,context.source = <p>LH_R</p></div>,ancestors = [div]
  2. p开始标签入栈,context.source = LH_R</p></div>,ancestors = [div, p]
  3. 解析文本LH_R
  4. 解析p结束标签,p标签出栈
  5. 解析div结束标签,div标签出栈
  6. 栈空,模版解析完毕

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步

  1. 创建代码生成上下文,因为该上下文对象是用于维护代码生成过程中程序的运行状态,如:
  • code:最终生成的渲染函数
  • push:拼接代码
  • indent:代码缩进
  • deindent:减少代码缩进
  1. 生成渲染函数的前置预设部分
    • module模式下:genModulePreamble()
    • function模式下:genFunctionPreamble
    • 还有一些函数名,参数,作用域…
  2. 生成渲染函数
    • 通过调用genNode,然后在genNode内部通过switch语句来匹配不同类型的节点,并调用对应的生成器函数

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值