• 【Vue.js 3.0源码】AST 转换之节点内部转换


    自我介绍:大家好,我是吉帅振的网络日志;微信公众号:吉帅振的网络日志;前端开发工程师,工作4年,去过上海、北京,经历创业公司,进过大厂,现在郑州敲代码。

    一、前言

    template 的解析过程,最终拿到了一个 AST 节点对象。这个对象是对模板的完整描述,但是它还不能直接拿来生成代码,因为它的语义化还不够,没有包含和编译优化的相关属性,所以还需要进一步转换。举个例子:

    >hello {{ msg + test }}

    static

    static

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    示例中,我们有普通的 DOM 节点,有组件节点,有 v-bind 指令,有 v-if 指令,有文本节点,也有表达式节点。

    // 获取节点和指令转换的方法
    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
    // AST 转换
    transform(ast, extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // 用户自定义  transforms
      ],
      directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // 用户自定义 transforms
      )
    }))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对于这个模板,我们通过 parse 生成一个 AST 对象,先通过 getBaseTransformPreset 方法获取节点和指令转换的方法,然后调用 transform 方法做 AST 转换,并且把这些节点和指令的转换方法作为配置的属性参数传入。

    function getBaseTransformPreset(prefixIdentifiers) {
      return [
        [
          transformOnce,
          transformIf,
          transformFor,
          transformExpression,
          transformSlotOutlet,
          transformElement,
          trackSlotScopes,
          transformText
        ],
        {
          on: transformOn,
          bind: transformBind,
          model: transformModel
        }
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    getBaseTransformPreset 返回节点和指令的转换函数,这些转换函数会在后续执行 transform 的时候调用。

    function transform(root, options) {
      const context = createTransformContext(root, options)
      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.temps = context.temps
      root.cached = context.cached
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    transform 的核心流程主要有四步:创建 transform 上下文、遍历 AST 节点、静态提升以及创建根代码生成节点。

    二、创建 transform 上下文

    function createTransformContext(root, { prefixIdentifiers = false, hoistStatic = false, cacheHandlers = false, nodeTransforms = [], directiveTransforms = {}, transformHoist = null, isBuiltInComponent = NOOP, expressionPlugins = [], scopeId = null, ssr = false, onError = defaultOnError }) {
      const context = {
        // 配置
        prefixIdentifiers,
        hoistStatic,
        cacheHandlers,
        nodeTransforms,
        directiveTransforms,
        transformHoist,
        isBuiltInComponent,
        expressionPlugins,
        scopeId,
        ssr,
        onError,
        // 状态数据
        root,
        helpers: new Set(),
        components: new Set(),
        directives: new Set(),
        hoists: [],
        imports: new Set(),
        temps: 0,
        cached: 0,
        identifiers: {},
        scopes: {
          vFor: 0,
          vSlot: 0,
          vPre: 0,
          vOnce: 0
        },
        parent: null,
        currentNode: root,
        childIndex: 0,
        // methods
        helper(name) {
          context.helpers.add(name)
          return name
        },
        helperString(name) {
          return `_${helperNameMap[context.helper(name)]}`
        },
        replaceNode(node) {
          context.parent.children[context.childIndex] = context.currentNode = node
        },
        removeNode(node) {
          const list = context.parent.children
          const removalIndex = node
            ? list.indexOf(node)
            : context.currentNode
              ? context.childIndex
              : -1
          if (!node || node === context.currentNode) {
            // 移除当前节点
            context.currentNode = null
            context.onNodeRemoved()
          }
          else {
            // 移除兄弟节点
            if (context.childIndex > removalIndex) {
              context.childIndex--
              context.onNodeRemoved()
            }
          }
          // 移除节点
          context.parent.children.splice(removalIndex, 1)
        },
        onNodeRemoved: () => { },
        addIdentifiers(exp) {
        },
        removeIdentifiers(exp) {
        },
        hoist(exp) {
          context.hoists.push(exp)
          const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`, false, exp.loc, true)
          identifier.hoisted = exp
          return identifier
        },
        cache(exp, isVNode = false) {
          return createCacheExpression(++context.cached, exp, isVNode)
        }
      }
      return context
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    创建 transform 上下文的过程和 parse 过程一样,在 transform 阶段会创建一个上下文对象,它的实现过程是这样的:上下文对象 context 维护了 transform 过程的一些配置,比如前面提到的节点和指令的转换函数等;还维护了 transform 过程的一些状态数据,比如当前处理的 AST 节点,当前 AST 节点在子节点中的索引,以及当前 AST 节点的父节点等。此外,context 还包含了在转换过程中可能会调用的一些辅助函数,和一些修改 context 对象的方法。

    三、遍历 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 */:
          if (!context.ssr) {
            // 需要导入 createComment 辅助函数
            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]()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    这里,traverseNode 函数的基本思路就是递归遍历 AST 节点,针对每个节点执行一系列的转换函数,有些转换函数还会设计一个退出函数,当你执行转换函数后,它会返回一个新函数,然后在你处理完子节点后再执行这些退出函数,这是因为有些逻辑的处理需要依赖子节点的处理结果才能继续执行。

    四、静态提升

    节点转换完毕后,接下来会判断编译配置中是否配置了 hoistStatic,如果是就会执行 hoistStatic 做静态提升:

    if (options.hoistStatic) {
      hoistStatic(root, context)
    }
    
    • 1
    • 2
    • 3

    静态提升也是 Vue.js 3.0 在编译阶段设计了一个优化策略,举个例子:

    >hello {{ msg + test }}

    static

    static

    • 1
    • 2
    • 3

    如果为它配置 hoistStatic,经过编译后,它的代码就变成了这样:

    import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"
    const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
    const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
    export function render(_ctx, _cache) {
      return (_openBlock(), _createBlock(_Fragment, null, [
        _createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 /* TEXT */),
        _hoisted_1,
        _hoisted_2
      ], 64 /* STABLE_FRAGMENT */))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这里,我们先忽略 openBlock、Fragment ,我会在代码生成章节详细说明,重点看一下 _hoisted_1 和 _hoisted_2 这两个变量,它们分别对应模板中两个静态 p 标签生成的 vnode,可以发现它的创建是在 render 函数外部执行的。这样做的好处是,不用每次在 render 阶段都执行一次 createVNode 创建 vnode 对象,直接用之前在内存中创建好的 vnode 即可。那么为什么叫静态提升呢?因为这些静态节点不依赖动态数据,一旦创建了就不会改变,所以只有静态节点才能被提升到外部创建。

    五、创建根代码生成节点

    function createRootCodegen(root, context) {
      const { helper } = context;
      const { children } = root;
      const child = children[0];
      if (children.length === 1) {
        // 如果子节点是单个元素节点,则将其转换成一个 block
        if (isSingleElementRoot(root, child) && child.codegenNode) {
          const codegenNode = child.codegenNode;
          if (codegenNode.type === 13 /* VNODE_CALL */) {
            codegenNode.isBlock = true;
            helper(OPEN_BLOCK);
            helper(CREATE_BLOCK);
          }
          root.codegenNode = codegenNode;
        }
        else {
          root.codegenNode = child;
        }
      }
      else if (children.length > 1) {
        // 如果子节点是多个节点,则返回一个 fragement 的代码生成节点
        root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, `${64 /* STABLE_FRAGMENT */} /* ${PatchFlagNames[64 /* STABLE_FRAGMENT */]} */`, undefined, undefined, true);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    createRootCodegen 做的事情很简单,就是为 root 这个虚拟的 AST 根节点创建一个代码生成节点,如果 root 的子节点 children 是单个元素节点,则将其转换成一个 Block,把这个 child 的 codegenNode 赋值给 root 的 codegenNode。如果 root 的子节点 children 是多个节点,则返回一个 fragement 的代码生成节点,并赋值给 root 的 codegenNode。

    六、总结

    如果说 parse 阶段是一个词法分析过程,构造基础的 AST 节点对象,那么 transform 节点就是语法分析阶段,把 AST 节点做一层转换,构造出语义化更强,信息更加丰富的 codegenCode,它在后续的代码生成阶段起着非常重要的作用。

  • 相关阅读:
    .NET开源全面方便的第三方登录组件集合 - MrHuo.OAuth
    实战 | 服务端开发与计算机网络结合的完美案例
    别人做跨境电商都月入过万了,真这么好做吗?
    NestJS学习:控制器
    【机器学习基础】多元线性回归(适合初学者的保姆级文章)
    linux安装Ftp
    海信电视U8发布,一场针对画质的“定向跨越”
    汽车EDI:波森Boysen EDI项目案例
    大数据培训技术自定义Sink案例测试
    E-Prime心理学实验设计软件丨产品简介
  • 原文地址:https://blog.csdn.net/qq_42451979/article/details/125909671