babel能够将es6的大部分语法转换成es5,例如箭头函数转成普通函数等。今天就利用babel的一些库,手写一版箭头函数转换成普通函数,感受下ast语法树的作用。
依赖库介绍(重点)
@babel/core
- @babel/core是babel的核心包,但本身不知道如何转换代码,所以要借助插件,比如下面的babel-plugin-transform-es2015-arrow-functions用于转换箭头函数
babel-types
- babel-types是babel的工具包,比如用于判断某个几点是不是某个类型,动态创建某个类型的节点等
babel-plugin-transform-es2015-arrow-functions
- 该插件用于将es6的箭头函数转成es5的普通函数
插件介绍(重点)
- 类似上面的
babel-plugin-transform-es2015-arrow-functions
插件其实本质上是一个钩子函数,在遍历语法树的过程中,可以捕获牟特特别类型的节点并进行转换 - 每个es6语法都会对应一个转换插件
- 每个插件都会捕获自己的语法节点,转成对应的es5语法
- 所有插件打包成一个包,就是preset,比如
@babel/preset-env
就是插件的集合,包含着基本上大部分的es6语法转换插件 - 插件的格式是定死的,就是一个对象,对象里有一个visitor的访问器(设计模式:访问者模式)
babel如何将箭头函数转成普通函数(了解)
- 原先的es6函数
const sum = (a, b) => {
console.log(this)
return a + b;
}
- babel转换的过程
// @babel/core是babel的核心包
// @babel/core本身不知道如何转换代码,所以要借助插件,比如下面的babel-plugin-transform-es2015-arrow-functions用于转换箭头函数
let babelCore = require('@babel/core')
// babel-types是babel的工具包,判断某个节点是不是某个类型,动态创建某个类型的节点
let types = require('babel-types');
// 箭头函数转换插件
// 插件其实就是一个钩子函数,在遍历语法树的过程中,可以捕获某些特别类型的节点并进行转换
// 每个es6的语法都会对应一个插件
// 每个插件都会捕获自己的语法节点,转成对应的es5语法
// 所有的插件打成一个包,就是preset @babel/preset-env 是插件集合,包含着基本上大部分的es6语法转换插件
let ArrowFunctionsPlugin = require('babel-plugin-transform-es2015-arrow-functions');
let sourceCode = `
const sum = (a, b) => {
console.log(this)
return a + b;
}
`;
let targetCode = babelCore.transform(sourceCode, {
// presets: ["@babel/preset-env"], // 可以写成这种预设,也可以写成下面的插件数组,预设是插件的集合
plugins: [ArrowFunctionsPlugin]
})
console.log(targetCode.code)
/*
结果:
var _this = this;
const sum = function (a, b) {
console.log(_this);
return a + b;
};
*/
自己实现箭头函数转换插件
自己转换实现箭头函数转换插件,其实就是重写一个箭头函数转换插件
// @babel/core是babel的核心包
// @babel/core本身不知道如何转换代码,所以要借助插件,比如下面的babel-plugin-transform-es2015-arrow-functions用于转换箭头函数
let babelCore = require('@babel/core')
// babel-types是babel的工具包,判断某个节点是不是某个类型,动态创建某个类型的节点
let types = require('babel-types');
// 箭头函数转换插件
// 插件其实就是一个钩子函数,在遍历语法树的过程中,可以捕获某些特别类型的节点并进行转换
// 每个es6的语法都会对应一个插件
// 每个插件都会捕获自己的语法节点,转成对应的es5语法
// 所有的插件打成一个包,就是preset @babel/preset-env 是插件集合,包含着基本上大部分的es6语法转换插件
let ArrowFunctionsPlugin = require('babel-plugin-transform-es2015-arrow-functions');
let sourceCode = `
const sum = (a, b) => {
console.log(this)
return a + b;
}
`
/*
节点替换的原理简单举例:
let node = {
type: "Identifier",
identifierName: "sum",
name: "sum"
}
let nodePath = {
node:node,
replaceWith(newNode){
nodePath.node = newNode
},
remove(){
}
}
nodePath.replaceWith(newNode);
上面这句replaceWith的意思就是:
nodePath.node = newNode
*/
// 自己写一个箭头函数转换插件
// 插件的格式是定死的,就是一个对象,对象里有一个visitor的访问器(设计模式:访问者模式)
let ArrowFunctionsPlugin2 = {
// 每个插件都有自己的访问器
visitor: {
// 处理所有箭头函数节点, ArrowFunctionExpression其实就是AST语法树中的节点的type
ArrowFunctionExpression(nodePath){ // 参数是节点所在的路径
let { node } = nodePath;
// 处理this指向的问题
hoistFunctionEnvironment(nodePath); // 提升函数的运行环境
// 将箭头函数改成普通函数
node.type = 'FunctionExpression' // 类型变成普通函数,这样箭头函数就变成了普通函数
}
}
}
// 提升函数的运行环境
function hoistFunctionEnvironment(fnPath){
// 从当前的路径向上查找,查找this定义的位置
const thisEnvFn = fnPath.findParent(p => {
// es5代码中自由两种作用域:全局和函数
// 所以要找到 不是箭头函数的普通函数,或者 跟作用域
return (p.isFunction()&&!p.isArrayFunctionExpression()) || p.isProgram(); // ||之前的表示 node是函数,但是不是箭头函数,也就是为普通函数; ||之后的表示 跟作用域
});
// 找一找当前作用于内哪些地方用到了this
let thisPaths = getScopeInfoInformation(fnPath);
// 先声明一个变量 this=>_this ,真正插件中,遇到this会变成_this,但是有可能用户自己定义了_this,那么会给用户自己定义的_this再加一个_,也就是用户自己定义的_this变成了__this
let thisBinding = '_this';
// 如果子节点里有this的话
if (thisPaths.length > 0) {
// 生成 var _this = this;
// 向此路径内的作用于内添加一个变量
thisEnvFn.scope.push({
id: types.identifier(thisBinding), // types是工具包 生成一个标识符,即_this
init: types.thisExpression() // 生成一个this调用 this
})
// 修改变量 把作用域中的this改成_this
thisPaths.forEach(thisPath => {
let thisBindingIdentifer = types.identifier(thisBinding);// _this
// 把此路径上面挂在的节点进行替换,把this节点替换成_this节点
thisPath.replaceWith(thisBindingIdentifer)
})
}
}
function getScopeInfoInformation(fnPath){
let thisPaths = []; // 存放this的path的数组u
// 遍历当前路径的子路径
fnPath.traverse({
// this在节点中的type就是ThisExpression
ThisExpression(thisPath){
thisPaths.push(thisPath)
}
});
return thisPaths;
}
let targetCode = babelCore.transform(sourceCode, {
// presets: ["@babel/preset-env"], // 可以写成这种预设,也可以写成下面的插件数组,预设是插件的集合
plugins: [ArrowFunctionsPlugin2]
})
console.log(targetCode.code)
/*
结果:
var _this = this;
const sum = function (a, b) {
console.log(_this);
return a + b;
};
*/
- 节点替换原理其实就是:
let node = {
type: "Identifier",
identifierName: "sum",
name: "sum"
}
let nodePath = {
node:node,
replaceWith(newNode){
nodePath.node = newNode
},
remove(){
}
}
nodePath.replaceWith(newNode);
上面这句replaceWith的意思就是:
nodePath.node = newNode