【重学webpack系列——webpack5.0】

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