阿里面试官:如何给所有的async函数添加try/catch?

简介: 阿里面试官:如何给所有的async函数添加try/catch?

前言

大厂面试题分享 面试题库

前端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库


三面的时候被问到了这个问题,当时思路虽然正确,可惜表述的不够清晰

后来花了一些时间整理了下思路,那么如何实现给所有的async函数添加try/catch呢?

async如果不加 try/catch 会发生什么事?


// 示例asyncfunctionfn() {
  let value = awaitnewPromise((resolve, reject) => {
    reject('failure');
  });
  console.log('do something...');
}
fn()
复制代码

导致浏览器报错:一个未捕获的错误

在开发过程中,为了保证系统健壮性,或者是为了捕获异步的错误,需要频繁的在 async 函数中添加 try/catch,避免出现上述示例的情况

可是我很懒,不想一个个加,懒惰使我们进步😂

下面,通过手写一个babel 插件,来给所有的async函数添加try/catch

babel插件的最终效果


原始代码:

asyncfunctionfn() {
  awaitnewPromise((resolve, reject) =>reject('报错'));
  awaitnewPromise((resolve) =>resolve(1));
  console.log('do something...');
}
fn();
复制代码

使用插件转化后的代码:

asyncfunctionfn() {
  try {
    awaitnewPromise((resolve, reject) =>reject('报错'));
    awaitnewPromise(resolve =>resolve(1));
    console.log('do something...');
  } catch (e) {
    console.log("\nfilePath: E:\\myapp\\src\\main.js\nfuncName: fn\nError:", e);
  }
}
fn();
复制代码

打印的报错信息:

通过详细的报错信息,帮助我们快速找到目标文件和具体的报错方法,方便去定位问题

babel插件的实现思路


1)借助AST抽象语法树,遍历查找代码中的await关键字

2)找到await节点后,从父路径中查找声明的async函数,获取该函数的body(函数中包含的代码)

3)创建try/catch语句,将原来async的body放入其中

4)最后将async的body替换成创建的try/catch语句

babel的核心:AST


先聊聊 AST 这个帅小伙🤠,不然后面的开发流程走不下去

词法分析

词法分析阶段把字符串形式的代码转换为令牌(tokens) ,可以把tokens看作是一个扁平的语法片段数组,描述了代码片段在整个代码中的位置和记录当前值的一些信息

比如let a = 1,对应的AST是这样的

语法分析

语法分析阶段会把token转换成 AST 的形式,这个阶段会使用token中的信息把它们转换成一个 AST 的表述结构,使用type属性记录当前的类型

例如 let 代表着一个变量声明的关键字,所以它的 type 为 VariableDeclaration,而 a = 1 会作为 let 的声明描述,它的 type 为 VariableDeclarator

再举个🌰,加深对AST的理解

functiondemo(n) {
  return n * n;
}
复制代码

转化成AST的结构

{"type":"Program",// 整段代码的主体"body":[{"type":"FunctionDeclaration",// function 的类型叫函数声明;"id":{// id 为函数声明的 id"type":"Identifier",// 标识符 类型"name":"demo"// 标识符 具有名字 },"expression":false,"generator":false,"async":false,// 代表是否 是 async function"params":[// 同级 函数的参数 {"type":"Identifier",// 参数类型也是 Identifier"name":"n"}],"body":{// 函数体内容 整个格式呈现一种树的格式"type":"BlockStatement",// 整个函数体内容 为一个块状代码块类型"body":[{"type":"ReturnStatement",// return 类型"argument":{"type":"BinaryExpression",// BinaryExpression 二进制表达式类型"start":30,"end":35,"left":{// 分左 右 中 结构"type":"Identifier","name":"n"},"operator":"*",// 属于操作符"right":{"type":"Identifier","name":"n"}}}]}}],"sourceType":"module"}复制代码

常用的 AST 节点类型对照表


image.png

image.png

image.png

await节点对应的AST结构


1)原始代码

async function fn() {
   await f()
}
复制代码

对应的AST结构

2)增加try catch后的代码

asyncfunctionfn() {
    try {
        awaitf()
    } catch (e) {
        console.log(e)
    }
}
复制代码

对应的AST结构

通过AST结构对比,插件的核心就是将原始函数的body放到try语句中

babel插件开发


插件的基本格式示例

module.exports = function (babel) {
   let t = babel.typereturn { 
     visitor: {
       // 设置需要范围的节点类型CallExression: (path, state) => { 
         do soming ……
       }
     }
   }
 }
复制代码

1)通过 babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等

2)visitor:定义了一个访问者,可以设置需要访问的节点类型,当访问到目标节点后,做相应的处理来实现插件的功能

寻找await节点

回到业务需求,现在需要找到await节点,可以通过AwaitExpression表达式获取

module.exports = function (babel) {
   let t = babel.typereturn { 
     visitor: {
       // 设置AwaitExpressionAwaitExpression(path) {
         // 获取当前的await节点let node = path.node;
       }
     }
   }
 }
复制代码

向上查找 async 函数

通过findParent方法,在父节点中搜寻 async 节点

// async节点的属性为trueconst asyncPath = path.findParent(p => p.node.async)
复制代码

async 节点的AST结构

这里要注意,async 函数分为4种情况:函数声明 、箭头函数 、函数表达式 、函数为对象的方法

// 1⃣:函数声明asyncfunctionfn() {
  awaitf()
}
// 2⃣:函数表达式const fn = asyncfunction () {
  awaitf()
};
// 3⃣:箭头函数constfn = async () => {
  awaitf()
};
// 4⃣:async函数定义在对象中const obj = {
  asyncfn() {
      awaitf()
  }
}
复制代码

需要对这几种情况进行分别判断

module.exports = function (babel) {
   let t = babel.typereturn { 
     visitor: {
       // 设置AwaitExpressionAwaitExpression(path) {
         // 获取当前的await节点let node = path.node;
         // 查找async函数的节点const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
       }
     }
   }
 }
复制代码

利用babel-template生成try/catch节点

babel-template可以用以字符串形式的代码来构建AST树节点,快速优雅开发插件

// 引入babel-template
const template = require('babel-template');
// 定义try/catch语句模板
let tryTemplate = `
try {
} catch (e) {
console.log(CatchError:e)
}`;
// 创建模板
const temp = template(tryTemplate);
// 给模版增加key,添加console.log打印信息
let tempArgumentObj = {
   // 通过types.stringLiteral创建字符串字面量
   CatchError: types.stringLiteral('Error')
};
// 通过temp创建try语句的AST节点
let tryNode = temp(tempArgumentObj);复制代码

async函数体替换成try语句

module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       AwaitExpression(path) {
         let node = path.node;
         const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
         let tryNode = temp(tempArgumentObj);
         // 获取父节点的函数体body
         let info = asyncPath.node.body;
         // 将函数体放到try语句的body中
         tryNode.block.body.push(...info.body);
         // 将父节点的body替换成新创建的try语句
         info.body = [tryNode];
       }
     }
   }
 }
复制代码

到这里,插件的基本结构已经成型,但还有点问题,如果函数已存在try/catch,该怎么处理判断呢?

若函数已存在try/catch,则不处理

// 示例代码,不再添加try/catchasyncfunctionfn() {
    try {
        awaitf()
    } catch (e) {
        console.log(e)
    }
}
复制代码

通过isTryStatement判断是否已存在try语句

module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       AwaitExpression(path) {
        // 判断父路径中是否已存在try语句,若存在直接返回
        if (path.findParent((p) => p.isTryStatement())) {
          return false;
        }
         let node = path.node;
         const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
         let tryNode = temp(tempArgumentObj);
         let info = asyncPath.node.body;
         tryNode.block.body.push(...info.body);info.body = [tryNode];
       }
     }
   }
 }
复制代码

添加报错信息

获取报错时的文件路径 filePath 和方法名称 funcName,方便快速定位问题

获取文件路径

// 获取编译目标文件的路径,如:E:\myapp\src\App.vueconst filePath = this.filename || this.file.opts.filename || 'unknown';
复制代码

获取报错的方法名称

// 定义方法名
let asyncName = '';
// 获取async节点的type类型
let type = asyncPath.node.type;
switch (type) {
// 1⃣函数表达式
// 情况1:普通函数,如const func = async function () {}
// 情况2:箭头函数,如const func = async () => {}
case 'FunctionExpression':
case 'ArrowFunctionExpression':
  // 使用path.getSibling(index)来获得同级的id路径
  let identifier = asyncPath.getSibling('id');
  // 获取func方法名
  asyncName = identifier && identifier.node ? identifier.node.name : '';
  break;
// 2⃣函数声明,如async function fn2() {}
case 'FunctionDeclaration':
  asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
  break;
// 3⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }
case 'ObjectMethod':
  asyncName = asyncPath.node.key.name || '';
  break;
}
// 若asyncName不存在,通过argument.callee获取当前执行函数的name
let funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';复制代码

添加用户选项

用户引入插件时,可以设置exclude、include、 customLog选项

exclude: 设置需要排除的文件,不对该文件进行处理

include: 设置需要处理的文件,只对该文件进行处理

customLog: 用户自定义的打印信息

最终代码

入口文件index.js

// babel-template 用于将字符串形式的代码来构建AST树节点const template = require('babel-template');
const { tryTemplate, catchConsole, mergeOptions, matchesFile } = require('./util');
module.exports = function (babel) {
  // 通过babel 拿到 types 对象,操作 AST 节点,比如创建、校验、转变等let types = babel.types;
  // visitor:插件核心对象,定义了插件的工作流程,属于访问者模式const visitor = {
    AwaitExpression(path) {
      // 通过this.opts 获取用户的配置if (this.opts && !typeofthis.opts === 'object') {
        returnconsole.error('[babel-plugin-await-add-trycatch]: options need to be an object.');
      }
      // 判断父路径中是否已存在try语句,若存在直接返回if (path.findParent((p) => p.isTryStatement())) {
        returnfalse;
      }
      // 合并插件的选项const options = mergeOptions(this.opts);
      // 获取编译目标文件的路径,如:E:\myapp\src\App.vueconst filePath = this.filename || this.file.opts.filename || 'unknown';
      // 在排除列表的文件不编译if (matchesFile(options.exclude, filePath)) {
        return;
      }
      // 如果设置了include,只编译include中的文件if (options.include.length && !matchesFile(options.include, filePath)) {
        return;
      }
      // 获取当前的await节点let node = path.node;
      // 在父路径节点中查找声明 async 函数的节点// async 函数分为4种情况:函数声明 || 箭头函数 || 函数表达式 || 对象的方法const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
      // 获取async的方法名let asyncName = '';
      lettype = asyncPath.node.type;
      switch (type) {
        // 1⃣函数表达式// 情况1:普通函数,如const func = async function () {}// 情况2:箭头函数,如const func = async () => {}case'FunctionExpression':
        case'ArrowFunctionExpression':
          // 使用path.getSibling(index)来获得同级的id路径let identifier = asyncPath.getSibling('id');
          // 获取func方法名
          asyncName = identifier && identifier.node ? identifier.node.name : '';
          break;
        // 2⃣函数声明,如async function fn2() {}case'FunctionDeclaration':
          asyncName = (asyncPath.node.id && asyncPath.node.id.name) || '';
          break;
        // 3⃣async函数作为对象的方法,如vue项目中,在methods中定义的方法: methods: { async func() {} }case'ObjectMethod':
          asyncName = asyncPath.node.key.name || '';
          break;
      }
      // 若asyncName不存在,通过argument.callee获取当前执行函数的namelet funcName = asyncName || (node.argument.callee && node.argument.callee.name) || '';
      const temp = template(tryTemplate);
      // 给模版增加key,添加console.log打印信息let tempArgumentObj = {
        // 通过types.stringLiteral创建字符串字面量CatchError: types.stringLiteral(catchConsole(filePath, funcName, options.customLog))
      };
      // 通过temp创建try语句let tryNode = temp(tempArgumentObj);
      // 获取async节点(父节点)的函数体let info = asyncPath.node.body;
      // 将父节点原来的函数体放到try语句中
      tryNode.block.body.push(...info.body);
      // 将父节点的内容替换成新创建的try语句
      info.body = [tryNode];
    }
  };
  return {
    name: 'babel-plugin-await-add-trycatch',
    visitor
  };
};
复制代码

util.js

const merge = require('deepmerge');
// 定义try语句模板let tryTemplate = `
try {
} catch (e) {
console.log(CatchError,e)
}`;
/*
 * catch要打印的信息
 * @param {string} filePath - 当前执行文件的路径
 * @param {string} funcName - 当前执行方法的名称
 * @param {string} customLog - 用户自定义的打印信息
 */letcatchConsole = (filePath, funcName, customLog) => `
filePath: ${filePath}
funcName: ${funcName}${customLog}:`;
// 默认配置const defaultOptions = {
  customLog: 'Error',
  exclude: ['node_modules'],
  include: []
};
// 判断执行的file文件 是否在 exclude/include 选项内functionmatchesFile(list, filename) {
  return list.find((name) => name && filename.includes(name));
}
// 合并选项functionmergeOptions(options) {
  let { exclude, include } = options;
  if (exclude) options.exclude = toArray(exclude);
  if (include) options.include = toArray(include);
  // 使用merge进行合并return merge.all([defaultOptions, options]);
}
functiontoArray(value) {
  returnArray.isArray(value) ? value : [value];
}
module.exports = {
  tryTemplate,
  catchConsole,
  defaultOptions,
  mergeOptions,
  matchesFile,
  toArray
};

babel插件的安装使用


npm网站搜索babel-plugin-await-add-trycatch

总结


通过开发这个babel插件,了解很多 AST 方面的知识,了解 babel 的原理。实际开发中,大家可以结合具体的业务需求开发自己的插件

大厂面试题分享 面试题库

前端面试题库 (面试必备) 推荐:★★★★★

地址:前端面试题库

相关文章
|
22天前
|
存储 关系型数据库 MySQL
阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?
尼恩是一位资深架构师,他在自己的读者交流群中分享了关于MySQL索引的重要知识点。索引是帮助MySQL高效获取数据的数据结构,主要作用包括显著提升查询速度、降低磁盘I/O次数、优化排序与分组操作以及提升复杂查询的性能。MySQL支持多种索引类型,如主键索引、唯一索引、普通索引、全文索引和空间数据索引。索引的底层数据结构主要是B+树,它能够有效支持范围查询和顺序遍历,同时保持高效的插入、删除和查找性能。尼恩还强调了索引的优缺点,并提供了多个面试题及其解答,帮助读者在面试中脱颖而出。相关资料可在公众号【技术自由圈】获取。
|
12天前
|
SQL Oracle 关系型数据库
[Oracle]面试官:你举例几个内置函数,并且说说如何使用内置函数作正则匹配
本文介绍了多种SQL内置函数,包括单行函数、非空判断函数、日期函数和正则表达式相关函数。每种函数都有详细的参数说明和使用示例,帮助读者更好地理解和应用这些函数。文章强调了字符串操作、数值处理、日期计算和正则表达式的使用方法,并提供了丰富的示例代码。作者建议读者通过自测来巩固学习成果。
12 1
[Oracle]面试官:你举例几个内置函数,并且说说如何使用内置函数作正则匹配
|
2天前
|
SQL 关系型数据库 MySQL
阿里面试:1000万级大表, 如何 加索引?
45岁老架构师尼恩在其读者交流群中分享了如何在生产环境中给大表加索引的方法。文章详细介绍了两种索引构建方式:在线模式(Online DDL)和离线模式(Offline DDL),并深入探讨了 MySQL 5.6.7 之前的“影子策略”和 pt-online-schema-change 方案,以及 MySQL 5.6.7 之后的内部 Online DDL 特性。通过这些方法,可以有效地减少 DDL 操作对业务的影响,确保数据的一致性和完整性。尼恩还提供了大量面试题和解决方案,帮助读者在面试中充分展示技术实力。
|
29天前
|
消息中间件 存储 canal
阿里面试:canal+MQ,会有乱序的问题吗?
本文详细探讨了在阿里面试中常见的问题——“canal+MQ,会有乱序的问题吗?”以及如何保证RocketMQ消息有序。文章首先介绍了消息有序的基本概念,包括全局有序和局部有序,并分析了RocketMQ中实现消息有序的方法。接着,针对canal+MQ的场景,讨论了如何通过配置`canal.mq.partitionsNum`和`canal.mq.partitionHash`来保证数据同步的有序性。最后,提供了多个与MQ相关的面试题及解决方案,帮助读者更好地准备面试,提升技术水平。
阿里面试:canal+MQ,会有乱序的问题吗?
|
25天前
|
消息中间件 架构师 Java
阿里面试:秒杀的分布式事务, 是如何设计的?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试阿里、滴滴、极兔等一线互联网企业时,遇到了许多关于分布式事务的重要面试题。为了帮助大家更好地应对这些面试题,尼恩进行了系统化的梳理,详细介绍了Seata和RocketMQ事务消息的结合,以及如何实现强弱结合型事务。文章还提供了分布式事务的标准面试答案,并推荐了《尼恩Java面试宝典PDF》等资源,帮助大家在面试中脱颖而出。
|
28天前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
28天前
|
Kubernetes 架构师 算法
阿里面试:全国14亿人,统计出重名最多的前100个姓名
文章介绍了如何解决“从全国14亿人的数据中统计出重名人数最多的前100位姓名”的面试题,详细分析了多种数据结构的优缺点,最终推荐使用前缀树(Trie)+小顶堆的组合。文章还提供了具体的Java代码实现,并讨论了在内存受限情况下的解决方案,强调了TOP N问题的典型解题思路。最后,鼓励读者通过系统化学习《尼恩Java面试宝典》提升面试技巧。
阿里面试:全国14亿人,统计出重名最多的前100个姓名
|
1月前
|
存储 缓存 NoSQL
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
|
29天前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
1月前
|
Java 调度 Android开发
Android面试题之Kotlin中async 和 await实现并发的原理和面试总结
本文首发于公众号“AntDream”,详细解析了Kotlin协程中`async`与`await`的原理及其非阻塞特性,并提供了相关面试题及答案。协程作为轻量级线程,由Kotlin运行时库管理,`async`用于启动协程并返回`Deferred`对象,`await`则用于等待该对象完成并获取结果。文章还探讨了协程与传统线程的区别,并展示了如何取消协程任务及正确释放资源。
23 0