深入浅出 Babel 上篇:架构和原理 + 实战

console.log(` exit p a t h . t y p e ( {path.type}( path.type({path.key})`)

}

})

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

查看代码执行结果

enter Program(program)

enter FunctionDeclaration(0)

enter Identifier(id)

exit Identifier(id)

enter Identifier(0)

exit Identifier(0)

enter BlockStatement(body)

enter ExpressionStatement(0)

enter CallExpression(expression)

enter MemberExpression(callee)

enter Identifier(object)

exit Identifier(object)

enter Identifier(property)

exit Identifier(property)

exit MemberExpression(callee)

enter BinaryExpression(0)

enter BinaryExpression(left)

enter StringLiteral(left)

exit StringLiteral(left)

enter Identifier(right)

exit Identifier(right)

exit BinaryExpression(left)

enter StringLiteral(right)

exit StringLiteral(right)

exit BinaryExpression(0)

exit CallExpression(expression)

exit ExpressionStatement(0)

exit BlockStatement(body)

exit FunctionDeclaration(0)

exit Program(program)

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

当访问者进入一个节点时就会调用 enter(进入) 方法,反之离开该节点时会调用 exit(离开) 方法。 一般情况下,插件不会直接使用enter方法,只会关注少数几个节点类型,所以具体访问者也可以这样声明访问方法:

traverse(ast, {

// 访问标识符

Idenfifier(path) {

console.log(`enter Identifier`)

},

// 访问调用表达式

CallExpression(path) {

console.log(`enter CallExpression`)

},

// 上面是enter的简写,如果要处理exit,也可以这样

// 二元操作符

BinaryExpression: {

enter(path) {},

exit(path) {},

},

// 更高级的, 使用同一个方法访问多种类型的节点

“ExportNamedDeclaration|Flow”(path) {}

})

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

那么 Babel 插件是怎么被应用的呢?

Babel 会按照插件定义的顺序来应用访问方法,比如你注册了多个插件,babel-core 最后传递给访问器的数据结构大概长这样:

{

Identifier: {

enter: [plugin-xx, plugin-yy,] // 数组形式

}

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

当进入一个节点时,这些插件会按照注册的顺序被执行。大部分插件是不需要开发者关心定义的顺序的,有少数的情况需要稍微注意以下,例如plugin-proposal-decorators:

{

“plugins”: [

“@babel/plugin-proposal-decorators”, // 必须在plugin-proposal-class-properties之前

“@babel/plugin-proposal-class-properties”

]

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

所有插件定义的顺序,按照惯例,应该是新的或者说实验性的插件在前面,老的插件定义在后面。因为可能需要新的插件将 AST 转换后,老的插件才能识别语法(向后兼容)。下面是官方配置例子, 为了确保先后兼容,stage-*阶段的插件先执行:

{

“presets”: [“es2015”, “react”, “stage-2”]

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

注意Preset的执行顺序相反,详见官方文档

节点的上下文

访问者在访问一个节点时, 会无差别地调用 enter 方法,我们怎么知道这个节点在什么位置以及和其他节点的关联关系呢?

通过上面的代码,读者应该可以猜出几分,每个visit方法都接收一个 Path 对象, 你可以将它当做一个‘上下文’对象,类似于JQuery的 JQuery(const $el = $(‘.el’)) 对象,这里面包含了很多信息:

  • 当前节点信息

  • 节点的关联信息。父节点、子节点、兄弟节点等等

  • 作用域信息

  • 上下文信息

  • 节点操作方法。节点增删查改

  • 断言方法。isXXX, assertXXX

下面是它的主要结构:

export class NodePath<T = Node> {

constructor(hub: Hub, parent: Node);

parent: Node;

hub: Hub;

contexts: TraversalContext[];

data: object;

shouldSkip: boolean;

shouldStop: boolean;

removed: boolean;

state: any;

opts: object;

skipKeys: object;

parentPath: NodePath;

context: TraversalContext;

container: object | object[];

listKey: string; // 如果节点在一个数组中,这个就是节点数组的键

inList: boolean;

parentKey: string;

key: string | number; // 节点所在的键或索引

node: T; // 当前节点

scope: Scope; // 当前节点所在的作用域

type: T extends undefined | null ? string | null : string; // 节点类型

typeAnnotation: object;

// … 还有很多方法,实现增删查改

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

你可以通过这个手册来学习怎么通过 Path 来转换 AST. 后面也会有代码示例,这里就不展开细节了

副作用的处理

实际上访问者的工作比我们想象的要复杂的多,上面示范的是静态 AST 的遍历过程。而 AST 转换本身是有副作用的,比如插件将旧的节点替换了,那么访问者就没有必要再向下访问旧节点了,而是继续访问新的节点, 代码如下。

traverse(ast, {

ExpressionStatement(path) {

// 将 `console.log(‘hello’ + v + ‘!’)` 替换为 `return ‘hello’ + v`

const rtn = t.returnStatement(t.binaryExpression(‘+’, t.stringLiteral(‘hello’), t.identifier(‘v’)))

path.replaceWith(rtn)

},

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

上面的代码, 将console.log(‘hello’ + v + ‘!’)语句替换为return “hello” + v;, 下图是遍历的过程:

深入浅出 Babel 上篇:架构和原理 + 实战

深入浅出 Babel 上篇:架构和原理 + 实战

我们可以对 AST 进行任意的操作,比如删除父节点的兄弟节点、删除第一个子节点、新增兄弟节点… 当这些操作’污染’了 AST 树后,访问者需要记录这些状态,响应式(Reactive)更新 Path 对象的关联关系, 保证正确的遍历顺序,从而获得正确的转译结果

作用域的处理

访问者可以确保正确地遍历和修改节点,但是对于转换器来说,另一个比较棘手的是对作用域的处理,这个责任落在了插件开发者的头上。插件开发者必须非常谨慎地处理作用域,不能破坏现有代码的执行逻辑。

const a = 1, b = 2

function add(foo, bar) {

console.log(a, b)

return foo + bar

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

比如你要将 add 函数的第一个参数 foo 标识符修改为a, 你就需要递归遍历子树,查出foo标识符的所有引用, 然后替换它:

traverse(ast, {

// 将第一个参数名转换为a

FunctionDeclaration(path) {

const firstParams = path.get(‘params.0’)

if (firstParams == null) {

return

}

const name = firstParams.node.name

// 递归遍历,这是插件常用的模式。这样可以避免影响到外部作用域

path.traverse({

Identifier(path) {

if (path.node.name === name) {

path.replaceWith(t.identifier(‘a’))

}

}

})

},

})

console.log(generate(ast).code)

// function add(a, bar) {

// console.log(a, b);

// return a + bar;

// }

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

慢着,好像没那么简单,替换成 a 之后, console.log(a, b) 的行为就被破坏了。所以这里不能用 a,得换个标识符, 譬如c.

这就是转换器需要考虑的作用域问题,AST 转换的前提是保证程序的正确性。 我们在添加和修改引用时,需要确保与现有的所有引用不冲突。Babel本身不能检测这类异常,只能依靠插件开发者谨慎处理。

Javascript采用的是词法作用域, 也就是根据源代码的词法结构来确定作用域:

深入浅出 Babel 上篇:架构和原理 + 实战

深入浅出 Babel 上篇:架构和原理 + 实战

词法区块(block)中,由于新建变量、函数、类、函数参数等创建的标识符,都属于这个区块作用域. 这些标识符也称为绑定(Binding),而对这些绑定的使用称为引用(Reference)

在Babel中,使用Scope对象来表示作用域。 我们可以通过Path对象的scope字段来获取当前节点的Scope对象。它的结构如下:

{

path: NodePath;

block: Node; // 所属的词法区块节点, 例如函数节点、条件语句节点

parentBlock: Node; // 所属的父级词法区块节点

parent: Scope; // ⚛️指向父作用域

bindings: { [name: string]: Binding; }; // ⚛️ 该作用域下面的所有绑定(即该作用域创建的标识符)

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

Scope 对象和 Path 对象差不多,它包含了作用域之间的关联关系(通过parent指向父作用域),收集了作用域下面的所有绑定(bindings), 另外还提供了丰富的方法来对作用域仅限操作

我们可以通过bindings属性获取当前作用域下的所有绑定(即标识符),每个绑定由Binding类来表示:

export class Binding {

identifier: t.Identifier;

scope: Scope;

path: NodePath;

kind: “var” | “let” | “const” | “module”;

referenced: boolean;

references: number; // 被引用的数量

referencePaths: NodePath[]; // ⚛️获取所有应用该标识符的节点路径

constant: boolean; // 是否是常量

constantViolations: NodePath[];

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

通过Binding对象我们可以确定标识符被引用的情况

Ok,有了 Scope 和 Binding, 现在有能力实现安全的变量重命名转换了。 为了更好地展示作用域交互,在上面代码的基础上,我们再增加一下难度:

const a = 1, b = 2

function add(foo, bar) {

console.log(a, b)

return () => {

const a = ‘1’ // 新增了一个变量声明

return a + (foo + bar)

}

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

现在你要重命名函数参数 foo, 不仅要考虑外部的作用域, 也要考虑下级作用域的绑定情况,确保这两者都不冲突。

上面的代码作用域和标识符引用情况如下图所示:

深入浅出 Babel 上篇:架构和原理 + 实战

深入浅出 Babel 上篇:架构和原理 + 实战

来吧,接受挑战,试着将函数的第一个参数重新命名为更短的标识符:

// 用于获取唯一的标识符

const getUid = () => {

let uid = 0

return () => `_${(uid++) || ‘’}`

}

const ast = babel.parseSync(code)

traverse(ast, {

FunctionDeclaration(path) {

// 获取第一个参数

const firstParam = path.get(‘params.0’)

if (firstParam == null) {

return

}

const currentName = firstParam.node.name

const currentBinding = path.scope.getBinding(currentName)

const gid = getUid()

let sname

// 循环找出没有被占用的变量名

while(true) {

sname = gid()

// 1️⃣首先看一下父作用域是否已定义了该变量

if (path.scope.parentHasBinding(sname)) {

continue

}

// 2️⃣ 检查当前作用域是否定义了变量

if (path.scope.hasOwnBinding(sname)) {

// 已占用

continue

}

// 再检查第一个参数的当前的引用情况,

// 如果它所在的作用域定义了同名的变量,我们也得放弃

if (currentBinding.references > 0) {

let findIt = false

for (const refNode of currentBinding.referencePaths) {

if (refNode.scope !== path.scope && refNode.scope.hasBinding(sname)) {

findIt = true

break

}

}

if (findIt) {

continue

}

}

break

}

// 开始替换掉

const i = t.identifier(sname)

currentBinding.referencePaths.forEach(p => p.replaceWith(i))

firstParam.replaceWith(i)

},

})

console.log(generate(ast).code)

// const a = 1,

// b = 2;

// function add(_, bar) {

// console.log(a, b);

// return () => {

// const a = ‘1’; // 新增了一个变量声明

// return a + (_ + bar);

// };

// }

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

上面的例子虽然没有什么实用性,而且还有Bug(没考虑label),但是正好可以揭示了作用域处理的复杂性。

Babel的 Scope 对象其实提供了一个generateUid方法来生成唯一的、不冲突的标识符。我们利用这个方法再简化一下我们的代码:

traverse(ast, {

FunctionDeclaration(path) {

const firstParam = path.get(‘params.0’)

if (firstParam == null) {

return

}

let i = path.scope.generateUidIdentifier(‘_’) // 也可以使用generateUid

const currentBinding = path.scope.getBinding(firstParam.node.name)

currentBinding.referencePaths.forEach(p => p.replaceWith(i))

firstParam.replaceWith(i)

},

})

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

能不能再短点!

traverse(ast, {

FunctionDeclaration(path) {

const firstParam = path.get(‘params.0’)

if (firstParam == null) {

return

}

let i = path.scope.generateUid(‘_’) // 也可以使用generateUid

path.scope.rename(firstParam.node.name, i)

},

})

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

查看generateUid的实现代码

generateUid(name: string = “temp”) {

name = t

.toIdentifier(name)

.replace(/^_+/, “”)

.replace(/[0-9]+$/g, “”);

let uid;

let i = 0;

do {

uid = this._generateUid(name, i);

i++;

} while (

this.hasLabel(uid) ||

this.hasBinding(uid) ||

this.hasGlobal(uid) ||

this.hasReference(uid)

);

const program = this.getProgramParent();

program.references[uid] = true;

program.uids[uid] = true;

return uid;

}

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

非常简洁哈?作用域操作最典型的场景是代码压缩,代码压缩会对变量名、函数名等进行压缩… 然而实际上很少的插件场景需要跟作用域进行复杂的交互,所以关于作用域这一块就先讲到这里。

搞一个插件呗

等等别走,还没完呢,这才到2/3。学了上面得了知识,总得写一个玩具插件试试水吧?

现在打算模仿babel-plugin-import, 写一个极简版插件,来实现模块的按需导入. 在这个插件中,我们会将类似这样的导入语句:

import {A, B, C as D} from ‘foo’

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

转换为:

import A from ‘foo/A’

import ‘foo/A/style.css’

import B from ‘foo/B’

import ‘foo/B/style.css’

import D from ‘foo/C’

import ‘foo/C/style.css’

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

首先通过 AST Explorer 看一下导入语句的 AST 节点结构:

深入浅出 Babel 上篇:架构和原理 + 实战

深入浅出 Babel 上篇:架构和原理 + 实战

通过上面展示的结果,我们需要处理 ImportDeclaration 节点类型,将它的specifiers拿出来遍历处理一下。另外如果用户使用了默认导入语句,我们将抛出错误,提醒用户不能使用默认导入.

基本实现如下:

// 要识别的模块

const MODULE = ‘foo’

traverse(ast, {

// 访问导入语句

ImportDeclaration(path) {

if (path.node.source.value !== MODULE) {

return

}

// 如果是空导入则直接删除掉

const specs = path.node.specifiers

if (specs.length === 0) {

path.remove()

return

}

// 判断是否包含了默认导入和命名空间导入

if (specs.some(i => t.isImportDefaultSpecifier(i) || t.isImportNamespaceSpecifier(i))) {

// 抛出错误,Babel会展示出错的代码帧

throw path.buildCodeFrameError(“不能使用默认导入或命名空间导入”)

}

// 转换命名导入

const imports = []

for (const spec of specs) {

const named = MODULE + ‘/’ + spec.imported.name

const local = spec.local

imports.push(t.importDeclaration([t.importDefaultSpecifier(local)], t.stringLiteral(named)))

imports.push(t.importDeclaration([], t.stringLiteral(`${named}/style.css`)))

}

// 替换原有的导入语句

path.replaceWithMultiple(imports)

}

})

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

逻辑还算简单,babel-plugin-import可比这复杂得多。

接下来,我们将它封装成标准的 Babel 插件。 按照规范,我们需要创建一个babel-plugin-*前缀的包名:

mkdir babel-plugin-toy-import

cd babel-plugin-toy-import

yarn init -y

touch index.js

复制代码

深入浅出 Babel 上篇:架构和原理 + 实战

你也可以通过 generator-babel-plugin 来生成项目模板.

在 index.js 文件中填入我们的代码。index.js默认导出一个函数,函数结构如下:

// 接受一个 babel-core 对象

export default function(babel) {

const {types: t} = babel

return {

pre(state) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

在这里插入图片描述

在这里插入图片描述

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
el-plugin 来生成项目模板.

在 index.js 文件中填入我们的代码。index.js默认导出一个函数,函数结构如下:

// 接受一个 babel-core 对象

export default function(babel) {

const {types: t} = babel

return {

pre(state) {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-RuSHbM5F-1713747666997)]

[外链图片转存中…(img-o74aNiif-1713747666997)]

[外链图片转存中…(img-9XOcLKVu-1713747666997)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

[外链图片转存中…(img-xrTzmCHu-1713747666998)]

[外链图片转存中…(img-PoUChQOY-1713747666998)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值