Vue3模版编译原理

模版编译流程

Vue3模版编译就是把template字符串编译成渲染函数

// template

{{LH_R}}

// 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
    }

就拿

LH_R

模版举例

  1. div开始标签入栈,context.source =

    LH_R

,ancestors = [div]
  • p开始标签入栈,context.source = LH_R

  • ,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步

    1. 创建代码生成上下文,因为该上下文对象是用于维护代码生成过程中程序的运行状态,如:

      • code:最终生成的渲染函数
      • push:拼接代码
      • indent:代码缩进
      • deindent:减少代码缩进
      • ...
    2. 生成渲染函数的前置预设部分

      • module模式下:genModulePreamble()
      • function模式下:genFunctionPreamble
      • 还有一些函数名,参数,作用域...
    3. 生成渲染函数

      • 通过调用genNode,然后在genNode内部通过switch语句来匹配不同类型的节点,并调用对应的生成器函数

    参考资料

    你可能感兴趣的:(vue.js前端)