上一篇: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 代码了。