广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买
去阿里面试,三面的时候被问到了这个问题,当时思路虽然正确,可惜表述的不够清晰
后来花了一些时间整理了下思路,那么如何实现给所有的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 节点类型对照表
FunctionDeclaration
函数声明声明一个函数,例如 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 = ()=> {}AwaitExpression
await表达式例如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节点,可以通过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登录后复制添加用户选项
用户引入插件时,可以设置exclude
、include
、 customLog
选项
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建站博客其它相关文章!
发表评论