AST的一次简单尝试

本文探讨了一位前端开发者在遇到复杂JS语法解析需求时,从最初使用正则表达式到采用AST(抽象语法树)进行函数名称、参数和返回值解析的过程。通过@babel/parser进行AST解析,实现了函数变更检测、参数解析等功能,解决了正则解析的局限性。同时,文章提到了AST在前端开发中的重要性,并分享了实现细节和部分代码示例。
摘要由CSDN通过智能技术生成

一、从一个需求开始

前几天接到一个新的需求,不是特别复杂如下图:

要求如下:

  1. 合并树结构下的所有事件与方法到同一个文本编辑器,动态生成方法名称与入参以及系统注释
  2. 文本编辑器内容变更,通知源数据变更
  3. 文本编辑器可以编辑当前入参与返参并需要解析出来
  4. 获取文本编辑器当前焦点,高亮显示当前编辑的树结构

对于一个有多年切图经验的cv仔,大致一设计,就有了实现思路:

  • function生成可以通过字符串拼接搞定
  • 变更通知在Vue下通过Watch可以监听到具体的数据变化,但是苦于前几天排查Watch造成的各种问题,尤其是通知的不确定性造成的性能问题,对Watch抱有一定的敬畏之心,顾此处没有采用,而是采用发布订阅模式,变更之后定向通知到订阅的function下
  • 后面两个字符串的解析问题必须是正则匹配去搞啊

既然有了实现方案,那就说干就干,首先递归解析树结构生成文本编辑器内容

实现订阅发布

 //发布变更
  notify(methods, data) {
    if (this.sub[methods]) {
      this.sub[methods].map(item => {
        item.notify(data);
        return item;
      });
    }
  }
  //订阅
  /**
   *
   * @param {sub} 订阅方法
   * @param {notify} 通知callback
   */
  subscribe(param) {
    if (typeof param.notify != "function") {
      return console.error("callback必须为function");
    }
    if (!this[param.sub]) {
      return console.error("不存在["+param.sub+"]订阅方法");
    }
    if (!this.sub[param.sub]) {
      this.sub[param.sub] = [];
    }
    this.sub[param.sub].push(param);
    // console.log(
    //   "事件【" + param.sub + "】订阅成功,通知到【" + param.notify + "】"
    // );
  }
}

在按照Vue模板解析语法实现一套Function解析,整体齐活

解析结果如下:

到目前为止,我们大致已经实现了当初的需求,可是这个解析模板真的能实现我们所有的语法解析么?

二、思考

  • JS语法过于活泛尤其是在function内部如下图,该如何去解析返回?
function test(name){
    ()=>{
        ()=>{
            (function(){
                return
            })()
        }
    }
}
  • function 名称被修改之后如何判断,原则上函数名称系统生成不可变更
function test(name){
    return name
}
//修改成

function getName(name){
    return name
}

基于上面两个痛点问题,如果单纯使用正则匹配的方式去实现,需要实现及其复杂且繁琐的匹配规则,而且场景覆盖可能也没有那么的完善,需要不断在实际运行中不断去完善解析规则,那么我们应该如何去处理?

答案是可以通过AST去替代我们自主完成的语法解析。

三、AST实现 JS 语法解析

在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。对前端开发而言AST你可能不熟悉但是这却是你日常开发必不可少的,如babel实现高版本JS原发转化为低版本语法,在这里我们就不详细介绍AST相关知识了,感兴趣可以参考陈栋老师的文章【JavaScript AST实现原理揭秘】。接下来我们一起看看如何通过AST去解析获取方法名称以及返回值。

1.引入AST解析依赖包,这里我们采用babel/parser

`
const parser = require("@babel/parser");
let configJsTree = parser.parse(`${StrFun}`,{
    sourceType: "module",
    plugins: [
      "jsx",
      "flow",
    ],
  });

通过@babel/parser解析之后我们得到如下的树结构

我们先来看看每个字段都代表了什么,才能继续向下分析

字段名称解释
CommentBlock块注释
CommentLine单行注释
FunctionDeclaration块状域,存放完整的代码块 如 { return a}
ReturnStatement返回域 程序中的return
Identifier(标志)对象,用来作为函数的唯一标志
ExpressionStatement表达式
VariableDeclarator等同于var
BinaryExpression表达式
ArrowFunctionExpression箭头函数
本次我们分析能用到的有 Identifier,FunctionDeclaration,ReturnStatement等,接下来我们实现语法的解析
 let body = configJsTree.program.body;
  let functionList = {};
  body.forEach(item => {
    //如果是函数体
    if (item.type == "FunctionDeclaration") {
      //获取方法名称
      let functionName = item.id.name;
      //获取入参
      let param = [];
      if (item.params) {
        item.params.forEach(jj => {
          param.push(jj.name);
        });
      }
      if (param.length > 0) {
        param = param.join(",");
      } else {
        param = "";
      }
      let hasReturn = false;
      //获取返参,存在return 并且不是空 return
      if (
        item.body.body.some(kk => kk.type == "ReturnStatement" && kk.argument)
      ) {
        hasReturn = true;
      }
      //获取程序开始位置,以及代码行数
      let position = `${item.loc.start.line},${item.loc.end.line}`;
      //获取方法体,由于AST需要逐层解析过于复杂,且业务无需该操作,顾解析当前方法体采用正则匹配
      let ContentString = res.split("\n");
      ContentString = ContentString.slice(
        Number(position.split(",")[0]) - 1,
        Number(position.split(",")[1])
      );
      ContentString = ContentString.join("\n");
      let content = parseStrinToFunctionContent(ContentString);
      functionList[functionName] = {
        functionName,
        param,
        hasReturn,
        position,
        function: content
      };
    }
  });

解析结果

函数名称变更监听

    ...
      if(this.oldFunctionList){
        //如果老数据列表不存在当前函数名称,启动check
        if(!this.oldFunctionList[functionName]){
          for(let i in this.oldFunctionList){
            if(this.oldFunctionList[i].position==position&&this.oldFunctionList[i].functionName!=functionName){
              functionName=this.oldFunctionList[i].functionName
              item.id.name=this.oldFunctionList[i].functionName
              nameBeChanged=true
            }
          }
        }
      }
      ....
      //AST重置回JavaScript,并更新视图
       if(nameBeChanged){
            const generate = require("@babel/generator").default
            configJsTree.program.body=body
            const result = generate(configJsTree, {minified: true})
            this.notify("updateEditor",result.code)
        }

到目前为止我们已经实现了上面的大部分要求,包括函数名称变更检测以及还原,参数解析,函数名称解析等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值