babel: 轻松将箭头函数转换为普通函数-源码

我们写的箭头函数,如果要通过babel转换为普通的function 函数, 可以在babel配置中添加

shell 复制代码
npm install --save-dev @babel/plugin-transform-arrow-functions

babel.config.json

json 复制代码
{  
    "plugins": ["@babel/plugin-transform-arrow-functions"]  
}

源码实现

我们看一下源码实现, 箭头函数babel-官方插件-源码地址

ts 复制代码
import { declare } from "@babel/helper-plugin-utils";

export interface Options {
  spec?: boolean;
}

export default declare((api, options: Options) => {
// 设置上下文babel版本
  api.assertVersion(
    process.env.BABEL_8_BREAKING && process.env.IS_PUBLISH
      ? PACKAGE_JSON.version
      : 7,
  );

  const noNewArrows = api.assumption("noNewArrows") ?? !options.spec;

  return {
     // babel插件所需要的插件名
    name: "transform-arrow-functions",

    visitor: {
    // 箭头函数钩子
      ArrowFunctionExpression(path) {
        // In some conversion cases, it may have already been converted to a function while this callback
        // was queued up.
        if (!path.isArrowFunctionExpression()) return;

        if (process.env.BABEL_8_BREAKING) {
          path.arrowFunctionToExpression({
            // While other utils may be fine inserting other arrows to make more transforms possible,
            // the arrow transform itself absolutely cannot insert new arrow functions.
            allowInsertArrow: false,
            noNewArrows,
          });
        } else {
          path.arrowFunctionToExpression({
            allowInsertArrow: false,
            noNewArrows,

            // This is only needed for backward compat with @babel/traverse <7.13.0
            // @ts-ignore(Babel 7 vs Babel 8) Removed in Babel 8
            specCompliant: !noNewArrows,
          });
        }
      },
    },
  };
});

上面代码行数不多,主要是通过 path.arrowFunctionToExpression 这个方法实现的, 这个方法是谁提供的呢 ?

答: @babel/traverse

动手实现

现在我们重新写一个demo, 手动通过 @babel/traverse 实现实现一下,学习@babel/traverse 的使用

基础环境安装

建议使用 typescript编写, babel相关插件的文档已经断层, babel-handbook(非官网)已经很久不更新了,使用ts能检查出语法错误,参数提示

shell 复制代码
pnpm init

安装babel相关的核心包

shell 复制代码
pnpm add @babel/generator @babel/parser @babel/traverse @babel/types -S
  • @babel/parser 把字符串解析成 AST
  • @babel/traverse 操作/新增/删除/修改 AST节点
  • @babel/types 手动构建AST节点
  • @babel/generator 把处理完成后的AST节点树,变成代码, 最终是一个字符串

安装ts, 不想使用ts, 可以不装

shell 复制代码
pnpm add typescript tslib ts-node -D
  • tslib 与 ts-node 搭档, ts-node依赖的
  • ts-node 直接运行ts文件

安装类型声明的包

sql 复制代码
pnpm add @types/node @types/babel-types @types/babel__generator @types/babel__traverse -D

代码

测试代码 demo/07.js

js 复制代码
const test = () => {
  console.log(1);
}

var obj = {
  say: () => {
    // 包裹在对象里面
    console.log('say log 哈哈哈');
  }
}

function gogo() {
  let a = 1

  console.log(a);

  return () => {
    console.log(222);
  }
}

src/07.ts

ts 复制代码
/**
 * 将箭头函数转换为普通函数
 */

import generate from '@babel/generator'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'

import fs from 'node:fs'
import path from 'node:path'

const target = path.resolve(__dirname, '../demo/07.js')

const content = fs.readFileSync(target, {
    encoding: 'utf-8',
})

const ast = parse(content)

traverse(ast, {
    /**
     * 箭头函数钩子
     */
    ArrowFunctionExpression(path) {
        // 官方解释: 在某些转换情况下,此回调函数已经从箭头函数转成普通函数 function ,则不需要进一步处理
        if (!path.isArrowFunctionExpression()) return

        path.arrowFunctionToExpression({
                allowInsertArrow: false
        })
    },
})

const dir = path.resolve(__dirname, '../demo/07-1.js')
fs.writeFileSync(dir, generate(ast).code)

执行 ts-node ./src/07.ts

js 复制代码
const test = function () {
  console.log(1);
};
var obj = {
  say: function () {
    // 包裹在对象里面
    console.log('say log 哈哈哈');
  }
};
function gogo() {
  let a = 1;
  console.log(a);
  return function () {
    console.log(222);
  };
}

本质跟官方的一样,这个案例主要表达的是, 我们从字符串->AST -> 修改AST -> 还原代码 这个过程

  1. 我们使用fs 读取了文件
  2. 使用 parse 将字符串 解析为 AST
  3. 通过 traverse 处理 箭头函数转换为 普通函数
  4. 最后将AST 变成字符串,写入文件

官方写的插件,本质也是调用 @babel/traverse 里面的方法,只是我们将它单独拿出来学习。

问题

我怎么知道要使用什么钩子处理AST?

babel中有N个方法,对应方法名字比较冗长复杂, 我们并不需要记忆, 我们只需要知道,对应包有什么方法,做什么即可

@babel/types 提供了手动构建AST节点的能力,里面能告知我们很多创建节点的方法,这个方法都对应到 @babel/traverse 里面的 钩子函数,也就是上面我们可以看到的 ArrowFunctionExpression, 我们就知道要在这里拦截,并对代码进行处理

手动构建箭头函数AST

上面我们既然安装了 @babel/types, 现在我们手动构建一个箭头函数的AST, 看看如何实现

ts 复制代码
/**
 * 手动构建箭头函数 AST
 */
import generate from '@babel/generator'
import * as types from '@babel/types'

/**
 * 目标:手动构建一个如下的箭头函数 AST 树
let a = () => {
	console.log(1)
}
 */
 
const ast = types.variableDeclaration('let', [
    types.variableDeclarator(
        types.identifier('a'),
        types.arrowFunctionExpression(
            // 箭头函数 函数体body; 函数体body 可以有N条语句,所以是数组
            [],
            // 箭头函数 函数体body
            types.blockStatement([
                types.expressionStatement(
                    // console.log 属于函数调用,所有创建这样一个节点
                    types.callExpression(
                        types.memberExpression(
                            types.identifier('console'),
                            // log本身就是console对象上的属性
                            types.identifier('log')
                        ),
                        // console.log的参数
                        [types.numericLiteral(1)]
                    )
                ),
            ])
        )
    ),
])

// 将 AST 生成 代码, 这里的code 是字符串
// 剩下的,我们可以将代码写入文件等等操作, 也可以做其他改造
console.log(generate(ast).code)

执行后的效果

是不是有点懵逼, 这样的一个AST节点,我怎么知道,参数要传递什么, 我怎么知道要构建什么样的 AST节点?

答: 通过 AST explorer 我们将要生成的目标代码,简单写一下

js本身提示性不强,如果要编写手动构建AST节点的应用场景,建议使用ts, 能比较明确的告知我们要传递什么类型的节点

因为 babel本身在 plugin文档方面其实比较落后,插件的API比较多,无法一一列举,所以,需要自己学会举一反三

手动构建普通函数-AST

我一般会通过 ctrl+鼠标左键 跳进去看看类型

ts 复制代码
// 构建一个普通函数
const ast2 = types.functionDeclaration(
    // 创建一个标识符
    types.identifier('a'),
    // 函数的参数-形参
    [],
    
    // 函数体 - 存在多个的情况,所以是数组
    types.blockStatement([
        // 创建一个表达式
        types.expressionStatement(
            // 表达式调用
            types.callExpression(
                types.memberExpression(
                    // 调用的是 console 对象
                    types.identifier('console'),
                    // 第二个参数书属性,对应的是 console的log属性
                    types.identifier('log')
                ),
                // 调用,传递的参数
                [types.numericLiteral(1)]
            )
        ),
    ])
)

console.log(generate(ast2).code)

输出效果:

js 复制代码
function a() {   
  console.log(1);
}

本文中, 我们了解了 babel 将箭头函数转为普通函数的具体实现, 也从0开始使用 对应API, 手动实现了一下。

然后,我们通过在线网站,了解了 AST 树结构,根据树结构,我们一步一步通过手动的方式创建了 箭头函数, 普通函数的 AST 节点, 最后将AST还原成普通代码

希望对你学习AST有帮助。

喜欢就点个赞吧。😊😊😊

相关推荐
清灵xmf1 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨1 小时前
VUE+Vite之环境文件配置及使用环境变量
前端
GDAL1 小时前
npm入门教程1:npm简介
前端·npm·node.js
小白白一枚1112 小时前
css实现div被图片撑开
前端·css
薛一半2 小时前
PC端查看历史消息,鼠标向上滚动加载数据时页面停留在上次查看的位置
前端·javascript·vue.js
@蒙面大虾2 小时前
CSS综合练习——懒羊羊网页设计
前端·css
MarcoPage2 小时前
第十九课 Vue组件中的方法
前端·javascript·vue.js
.net开发2 小时前
WPF怎么通过RestSharp向后端发请求
前端·c#·.net·wpf
**之火3 小时前
Web Components 是什么
前端·web components
顾菁寒3 小时前
WEB第二次作业
前端·css·html