自定义Babel插件-控制箭头函数this指向当前模块

346 阅读4分钟

关于This指向

起因是抖音的大雷老师问了 btn上面的this指向哪里。

<button @click="btnClick">点击</button>

const btnClick=()=>{
    console.log(this)
}

大雷老师给出的答案是在严格模式下指向undefined、非严格模式指向window

然后我就想了一个问题,可不可能我们每个this都指向当前这个模块内部呢?不让箭头函数的this指向window?所以我就想到了AST

抽象语法树分析

image.png

可以看到箭头函数在抽象语法树中是这样表示的箭头函数表示为:ArrowFunctionExpression

知道这个结构之后的话呢,我们开始配置babel插件,配置遇到ArrowFunctionExpression的代码块的时候我们就做一次处理 获取到他的节点数据进行处理

怎么通过nodejs解析抽象语法树?

  • 安装依赖 npm install -D esprima estraverse escodegen

  • 怎么通过安装的模块解析js代码为ast数据?

let esprima=require('esprima')
let estraverse=require('estraverse')
let escodegen=require('escodegen')
let sourceCode=`const ast=()=>{}`
let ast=esprima.parse(sourceCode)
console.log(ast)

打印结果

Script {                          
  type: 'Program',                
  body: [                         
    VariableDeclaration {         
      type: 'VariableDeclaration',
      declarations: [Array],      
      kind: 'const'               
    }                             
  ],                              
  sourceType: 'script'            
}        
  • 怎么遍历抽象语法树的代码块
estraverse.traverse(ast,{
    enter(node){
        console.log(padding()+"进入"+node.type)
        indent+=2
    },
    leave(node){
        indent-=2
        console.log(padding()+"离开"+node.type)
    }
})

打印结果

进入Program
  进入VariableDeclaration
    进入VariableDeclarator
      进入Identifier
      离开Identifier
      进入ArrowFunctionExpression
        进入BlockStatement
        离开BlockStatement
      离开ArrowFunctionExpression
    离开VariableDeclarator
  离开VariableDeclaration
离开Program

自定义Babel转换插件

插件功能,遇到模块中的箭头函数都去找其最上层非箭头函数的部分,保存其this赋值给一个变量_this,然后将箭头函数中所有用到this的地方都换成_this不指向window

  • 新建一个文件夹
  • npm init -y
  • 安装@babel/core babel-types用来解析代码

这里我们先引入babel-plugin-transform-es2015-arrow-functions官方es6箭头函数转换插件做个对比

let core=require('@babel/core')
let types=require('babel-types')
let BabelPluginTransformEs2015ArrowFunctions=require('babel-plugin-transform-es2015-arrow-functions')
const sourceCode=`
   const sum=(a,b)=>{
    return a+b
   }
`
/**
 * babel-core本身就是为了生成语法树,遍历语法树,生成新代码的
 * 它本身并不支持转换语法树
 */
let targetCode=core.transform(sourceCode,{
    plugins: [BabelPluginTransformEs2015ArrowFunctions]
})
console.log(targetCode.code)
  • node运行得到结果 可以看到它把箭头函数转换成匿名函数了
const sum = function (a, b) {
  return a + b;
};

这里使用的是它官方的插件 我们仿照官方的插件源码进行改造

let BabelPluginTransformEs2015ArrowFunction2={
    visitor: {
        ArrowFunctionExpression(nodePath){
            let node=nodePath.node
            const thisBinding=hoistFunctionEnvironment(nodePath)
            node.type='FunctionExpression'
        }
    }
}
function hoistFunctionEnvironment(fnPath){
    // 往上找有自己this的祖宗
    const thisEnvFn=fnPath.findParent(p=>{
        // 如果他的父亲是函数,那不能是箭头函数
        return (p.isFunction()&& p.isArrowFunctionExpression())||p.isProgram()
    })
    // thisEnvFn指向program 哪些地方用到了this 如果用到了 那么就需要在thisEnvfn上添加一个语句 let _this=this
    let thisPaths=getScopeInfoInformation(fnPath)
    let thisBinding='_this' // 把this变成下划线this
    if(thisPaths.length>0){
        // 表示在this环境中添加一个变量,变量的名字叫做_this, 初始值为thisExpression
        thisEnvFn.scope.push({
            id:types.identifier('_this'),
            init:types.thisExpression()
        })
        // 遍历所有使用到this的路径节点 把所有的this expression变成_this标识符
        thisPaths.forEach(thisChlid=>{
            let thisRef=types.identifier(thisBinding)
            thisChlid.replaceWith(thisRef)
        })
    }
}
// 获取自身的作用域信息
function getScopeInfoInformation(fnPath){
    let thisPaths=[]
    fnPath.traverse({
        ThisExpression(thisPath){
            thisPaths.push(thisPath)
        }
    })
    return thisPaths
}
  1. 首先我们获取到他的节点数据,在这个自定义的babel转换插件里面的nodePath.node
  2. 获取当前this的指向,将this的指向做作用域提升,指向他的上层
  3. 找他的上层
// 往上找有自己this的祖宗
const thisEnvFn=fnPath.findParent(p=>{
    // 如果他的父亲是函数,那不能是箭头函数
    return (p.isFunction()&& p.isArrowFunctionExpression())||p.isProgram()
})
  1. 获取模块中哪些地方用到了this
function getScopeInfoInformation(fnPath){
    let thisPaths=[]
    fnPath.traverse({
        ThisExpression(thisPath){
            thisPaths.push(thisPath)
        }
    })
    return thisPaths
}
  1. 如果说模块中有地方用到了this的话,那么我们就要将模块最顶层加上一个_this=this
// 表示在this环境中添加一个变量,变量的名字叫做_this, 初始值为thisExpression
thisEnvFn.scope.push({
    id:types.identifier('_this'),
    init:types.thisExpression()
})
  1. 遍历所有用到了this的地方,将this替换为_this
// 遍历所有使用到this的路径节点 把所有的this expression变成_this标识符
thisPaths.forEach(thisChlid=>{
    let thisRef=types.identifier(thisBinding)
    thisChlid.replaceWith(thisRef)
})
  1. 最后将箭头函数改为普通函数
// 碰到箭头函数就把它变成普通函数
ArrowFunctionExpression(nodePath){
    let node=nodePath.node
    const thisBinding=hoistFunctionEnvironment(nodePath)
    node.type='FunctionExpression'
}

最终实现效果

  • 解析以下代码块:
const btnClick=()=>{
    console.log(this)
    const a=()=>{
        console.log(this)
        const b=()=>{
            console.log(this)
        }
    }
}

  • 解析结果

image.png