面试题:如何给所有的async函数添加try、catch?

广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买

面试题:如何给所有的async函数添加try、catch?

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

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

async如果不加 try/catch 会发生什么事?
// 示例async function fn() {  let value = await new Promise((resolve, reject) => {    reject('failure');  });  console.log('do something...');}fn()
登录后复制

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

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

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

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

babel插件的最终效果

原始代码:

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();
登录后复制

使用插件转化后的代码:

async function fn() {  try {    await new Promise((resolve, reject) => reject('报错'));    await new Promise(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 这个帅小伙?,不然后面的开发流程走不下去

AST是代码的树形结构,生成 AST 分为两个阶段:词法分析语法分析

词法分析

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

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

语法分析

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

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

AST在线查看工具:AST explorer

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

function demo(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 节点类型对照表类型原名称中文名称描述Program程序主体整段代码的主体VariableDeclaration变量声明声明一个变量,例如 var let constFunctionDeclaration函数声明声明一个函数,例如 functionExpressionStatement表达式语句通常是调用一个函数,例如 console.log()BlockStatement块语句包裹在 {} 块内的代码,例如 if (condition){var a = 1;}BreakStatement中断语句通常指 breakContinueStatement持续语句通常指 continueReturnStatement返回语句通常指 returnSwitchStatementSwitch 语句通常指 Switch Case 语句中的 SwitchIfStatementIf 控制流语句控制流语句,通常指 if(condition){}else{}Identifier标识符标识,例如声明变量时 var identi = 5 中的 identiCallExpression调用表达式通常指调用一个函数,例如 console.log()BinaryExpression二进制表达式通常指运算,例如 1+2MemberExpression成员表达式通常指调用对象的成员,例如 console 对象的 log 成员ArrayExpression数组表达式通常指一个数组,例如 [1, 3, 5]FunctionExpression函数表达式例如const func = function () {}ArrowFunctionExpression箭头函数表达式例如const func = ()=> {}AwaitExpressionawait表达式例如let val = await f()ObjectMethod对象中定义的方法例如 let obj = { fn () {} }NewExpressionNew 表达式通常指使用 New 关键词AssignmentExpression赋值表达式通常指将函数的返回值赋值给变量UpdateExpression更新表达式通常指更新成员值,例如 i++Literal字面量字面量BooleanLiteral布尔型字面量布尔值,例如 true falseNumericLiteral数字型字面量数字,例如 100StringLiteral字符型字面量字符串,例如 vansenbSwitchCaseCase 语句通常指 Switch 语句中的 Caseawait节点对应的AST结构

1)原始代码

async function fn() {   await f()}
登录后复制

对应的AST结构

2)增加try catch后的代码

async function fn() {    try {        await f()    } catch (e) {        console.log(e)    }}
登录后复制

对应的AST结构

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

babel插件开发

我曾在之前的文章中聊过如何开发一个babel插件

这里简单回顾一下

插件的基本格式示例
module.exports = function (babel) {   let t = babel.type   return {      visitor: {       // 设置需要范围的节点类型       CallExression: (path, state) => {          do soming ……       }     }   } }
登录后复制

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

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

寻找await节点

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

module.exports = function (babel) {   let t = babel.type   return {      visitor: {       // 设置AwaitExpression       AwaitExpression(path) {         // 获取当前的await节点         let node = path.node;       }     }   } }
登录后复制向上查找 async 函数

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

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

async 节点的AST结构

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

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();0
登录后复制

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

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();1
登录后复制利用babel-template生成try/catch节点

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

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();2
登录后复制async函数体替换成try语句
async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();3
登录后复制

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

若函数已存在try/catch,则不处理
// 示例代码,不再添加try/catchasync function fn() {    try {        await f()    } catch (e) {        console.log(e)    }}
登录后复制

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

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();5
登录后复制添加报错信息

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

获取文件路径

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();6
登录后复制

获取报错的方法名称

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();7
登录后复制添加用户选项

用户引入插件时,可以设置excludeincludecustomLog选项

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

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

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

最终代码

入口文件index.js

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();8
登录后复制

util.js

async function fn() {  await new Promise((resolve, reject) => reject('报错'));  await new Promise((resolve) => resolve(1));  console.log('do something...');}fn();9
登录后复制

github仓库

babel插件的安装使用

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

有兴趣的朋友可以下载玩一玩

babel-plugin-await-add-trycatch

总结

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

【相关推荐:javascript视频教程、编程视频】

以上就是面试题:如何给所有的async函数添加try/catch?的详细内容,更多请关注9543建站博客其它相关文章!

广告:SSL证书一年128.66元起,点击购买~~~

9543建站博客
一个专注于网站开发、微信开发的技术类纯净博客。
作者头像
admin创始人

肥猫,知名SEO博客站长,14年SEO经验。

上一篇:浅谈块状元素和内联元素之间的区别
下一篇:详解使用uniapp需要引入哪些资源

发表评论

关闭广告
关闭广告