babel源码分析 - traverse

5 篇文章 1 订阅
4 篇文章 0 订阅

上一篇:babel 源码分析 一 parse

接着上篇,traverse 的主要工作是将 ES6 的 AST 转化为 ES5 的 AST,babel 的各种插件也都是基于此实现的,比如 JSX,TS 的转化等。

还记得入口篇中最后 run 方法里的 transformFile 方法吗,它执行的时候最终会执行下面的方法:

// babel-traverse/src/index.js

function traverse(
  parent: t.Node,
  opts: TraverseOptions = {},
  scope?: Scope,
  state?: any,
  parentPath?: NodePath,
) {
  if (!parent) return;

  if (!t.VISITOR_KEYS[parent.type]) {
    return;
  }

  visitors.explode(opts);
	// 🌵🌵🌵 执行这里 🌵🌵🌵
  traverse.node(parent, opts, scope, state, parentPath);
}

这里的 parent 就是之前生成的 AST,opts 是根据 babel.config.js 的配置所需要依赖插件生成的数组,其中就包含了处理 let 变量的 VariableDeclaration,之后执行 node 方法。

traverse.node = function (
  node: t.Node,
  opts: TraverseOptions,
  scope?: Scope,
  state?: any,
  parentPath?: NodePath,
  skipKeys?,
) {
  const keys = t.VISITOR_KEYS[node.type];
  if (!keys) return;
	
  const context = new TraversalContext(scope, opts, state, parentPath);
  for (const key of keys) {
    if (skipKeys && skipKeys[key]) continue;
    // 🌵🌵🌵 context.visit(ast, 'program') 🌵🌵🌵
    if (context.visit(node, key)) return;
  }
};

其中的 VISITOR_KEYS 是定义在 babel-types 中,定义了每种节点对应的 key,首次进来的时候节点的类型是 file,根据下面的配置对应获取的 key 就是 program。

// babel-types/src/definitions/core.ts

defineType("File", {
  builder: ["program", "comments", "tokens"],
  visitor: ["program"],
  fields: {
    program: {
      validate: assertNodeType("Program"),
    },
    comments: {
      validate: !process.env.BABEL_TYPES_8_BREAKING
        ? Object.assign(() => {}, {
            each: { oneOfNodeTypes: ["CommentBlock", "CommentLine"] },
          })
        : assertEach(assertNodeType("CommentBlock", "CommentLine")),
      optional: true,
    },
    tokens: {
      // todo(ts): add Token type
      validate: assertEach(Object.assign(() => {}, { type: "any" })),
      optional: true,
    },
  },
});

接着将 AST 和 program 依次传入到 visit -> visitSingle -> visitQueue 中执行

visit(node, key) {
  const nodes = node[key];
  if (!nodes) return false;

  if (Array.isArray(nodes)) {
    return this.visitMultiple(nodes, node, key);
  } else {
    // 🌵🌵🌵 执行这里 🌵🌵🌵
    return this.visitSingle(node, key);
  }
}

// -------------我快乐的分割线--------------

visitSingle(node, key): boolean {
  if (this.shouldVisit(node[key])) {
    // 🌵🌵🌵 执行这里 🌵🌵🌵
    return this.visitQueue([this.create(node, node, key)]);
  } else {
    return false;
  }
}

// -------------我是快乐的分割线--------------

visitQueue(queue: Array<NodePath>) {
  this.queue = queue;
  this.priorityQueue = [];

  const visited = new WeakSet();
  let stop = false;

  for (const path of queue) {
    path.resync();

    if (
      path.contexts.length === 0 ||
      path.contexts[path.contexts.length - 1] !== this
    ) {
      path.pushContext(this);
    }

    if (path.key === null) continue;

    if (testing && queue.length >= 10_000) {
      this.trap = true;
    }

    const { node } = path;
    if (visited.has(node)) continue;
    if (node) visited.add(node);
    // 🌵🌵🌵 执行这里 🌵🌵🌵
    if (path.visit()) {
      stop = true;
      break;
    }

    if (this.priorityQueue.length) {
      stop = this.visitQueue(this.priorityQueue);
      this.priorityQueue = [];
      this.queue = queue;
      if (stop) break;
    }
  }

  for (const path of queue) {
    path.popContext();
  }

  this.queue = null;

  return stop;
}

然后就是执行 visit 方法。

export function visit(this: NodePath): boolean {
  if (!this.node) {
    return false;
  }

  if (this.isDenylisted()) {
    return false;
  }

  if (this.opts.shouldSkip && this.opts.shouldSkip(this)) {
    return false;
  }

  // Note: We need to check "this.shouldSkip" twice because
  // the visitor can set it to true. Usually .shouldSkip is false
  // before calling the enter visitor, but it can be true in case of
  // a requeued node (e.g. by .replaceWith()) that is then marked
  // with .skip().
  // 🌵🌵🌵 执行这里 🌵🌵🌵
  if (this.shouldSkip || this.call("enter") || this.shouldSkip) {
    this.debug("Skip...");
    return this.shouldStop;
  }

  this.debug("Recursing into...");
  traverse.node(
    this.node,
    this.opts,
    this.scope,
    this.state,
    this,
    this.skipKeys,
  );
	// 🌵🌵🌵 执行这里 🌵🌵🌵
  this.call("exit");

  return this.shouldStop;
}

这个方法会执行插件中挂载的 enter 和 exit 方法方法,而 enter 和 exit 方法的收集是在 transformFile 中,也是在转化逻辑之前之前收集的。

function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
  for (const pluginPairs of pluginPasses) {
    const passPairs = [];
    const passes = [];
    const visitors = [];

    for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
      const pass = new PluginPass(file, plugin.key, plugin.options);

      passPairs.push([plugin, pass]);
      passes.push(pass);
      visitors.push(plugin.visitor);
    }

    // 🌵🌵🌵 merge all plugin visitors into a single visitor 🌵🌵🌵
    const visitor = traverse.visitors.merge(
      visitors,
      passes,
      file.opts.wrapPluginVisitorMethod,
    );
    // 🌵🌵🌵 转化入口 🌵🌵🌵
    traverse(file.ast, visitor, file.scope);
  }
}

然后就进入到对应的插件中执行,我们这需要将 let 转化为 var,所以就会进入到 plugin-transform-block-scoping 中执行具体执行过程就不贴都是流水账,大致顺序如下:

"BlockStatement|SwitchStatement|Program"(path, state)
-> blockScoping.run();
-> this.getLetReferences();
-> addDeclarationsFromChild(declarPaths[i]);
-> convertBlockScopedToVar(path, node, block, this.scope);

其中将 let 转化为 var 的操作就在最后一个函数中

function convertBlockScopedToVar(
  path,
  node,
  parent,
  scope,
  moveBindingsToParent = false,
) {
  if (!node) {
    node = path.node;
  }

  // https://github.com/babel/babel/issues/255
  if (isInLoop(path) && !t.isFor(parent)) {
    for (let i = 0; i < node.declarations.length; i++) {
      const declar = node.declarations[i];
      declar.init = declar.init || scope.buildUndefinedNode();
    }
  }
	// 🌵🌵🌵 执行这里 🌵🌵🌵
  node[t.BLOCK_SCOPED_SYMBOL] = true;
  node.kind = "var";

  // Move bindings from current block scope to function scope.
  if (moveBindingsToParent) {
    const parentScope = scope.getFunctionParent() || scope.getProgramParent();
    for (const name of Object.keys(path.getBindingIdentifiers())) {
      const binding = scope.getOwnBinding(name);
      if (binding) binding.kind = "var";
      scope.moveBindingTo(name, parentScope);
    }
  }
}

这方法会将 AST 上的变量节点的 kind 属性从 let 变成 var。

至此,traverse 逻辑就分析完毕,接下就是根据转化好的 AST 生成 ES5 代码了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
babel-import-plugin是一个用于实现组件库/工具库的按需加载的插件。它类似于babel-plugin-transform-imports和babel-plugin-import。它可以自动按需引入需要的组件,从而减少打包文件的大小。不过,实际生产中,建议使用babel-plugin-transform-imports和babel-plugin-import这两个插件中的一个。 使用babel-plugin-import可以实现自动按需引入,具体配置如下: 1. 首先安装babel-plugin-import:npm i babel-plugin-import -D 2. 在babel配置文件中添加以下配置: ```javascript module.exports = { plugins: [ ['import', { libraryName, libraryDirectory: 'es', style: true }, libraryName] ] }; ``` 这里的libraryName是你需要按需引入的组件库的名称。 通过以上配置,你就可以使用import语句来按需引入需要的组件了,例如: ```javascript import { Button } from libraryName; ``` 这样就可以只引入需要的Button组件,而不是整个组件库。 同时,现在的babel也提供了一些API来简化babel-plugin-import的代码或逻辑,例如path.replaceWithMultiple。不过,源码中的一些看似多余的逻辑可能是为了满足特定的场景而保留的。所以,在使用这些API时需要根据实际情况进行判断和选择。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [babel-plugin-my-import:像babel-plugin-transform-imports这样的babel插件](https://download.csdn.net/download/weixin_42166918/18495983)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [babel-import-plugin:](https://blog.csdn.net/weixin_48123820/article/details/130476061)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [你不容错过的babel-plugin-import史上最全源码详解!](https://blog.csdn.net/a958014226/article/details/115356326)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值