关于vue.js:Vue3模版编译原理

66次阅读

共计 4574 个字符,预计需要花费 12 分钟才能阅读完成。

模版编译流程

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:缩小代码缩进
  2. 生成渲染函数的前置预设局部

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

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

参考资料

  • Vue3 模板编译原理
  • 《Vue.js 设计与实现》

正文完
 0