css写箭头-嘿嘿,异步函数不要写那么多try/catch(部分原理)

2023-08-26 0 4,601 百度已收录

原来的:

前言

在开发过程中,您是否经常在异步函数中编写 try/catch 逻辑以提高系统鲁棒性或捕获异步错误?

async function func() {
try {
let res = await asyncFunc()
} catch (e) {
//......
}
}

我曾经在《合格的高级后端工程师必须掌握的28项JavaScript技能》中提到过处理async/await的高尚方式

这样我们就可以用一个helper函数来包装这个async函数来实现错误捕获

async function func() {
let [err, res] = await errorCaptured(asyncFunc)
if (err) {
//... 错误捕获
}
//...
}

但这样做有一个缺陷。 每次使用时,必须引入辅助函数errorCaptured。 有没有“偷懒”的办法呢?

答案一定是肯定的。 在那篇博客之后我提出了一个新想法,可以通过 webpack 加载器手动注入 try/catch 代码。 最后的结果希望是这样的

// development
async function func() {
let res = await asyncFunc()
//...其他逻辑
}

// release
async function func() {
try {
let res = await asyncFunc()
} catch (e) {
//......
}
//...其他逻辑
}

不是很好吗? 开发环境中不需要任何多余的代码,让webpack自动将错误捕捉逻辑注入到生产环境的代码中,然后我们会逐步实现这个loader

装载机原理

在实现这个webpack加载器之前,我们先简单介绍一下加载器的原理。 我们在 webpack 中定义的加载器本质上只是函数。 在定义loader的时候,我们会同时定义一个test属性。 Webpack 会遍历所有模块名称。 当匹配到 test 属性定义的正则时,这个模块会作为源参数传递给加载器执行

{
test: /.vue$/,
use: "vue-loader",
}

当匹配到以 .vue 结尾的文件名时,该文件将作为源参数传递给 vue-loader。 use 属性前面可以是字符串或路径。 当为字符串时,默认会被视为一个nodejs模块,去node_modules中查找

css写箭头-嘿嘿,异步函数不要写那么多try/catch(部分原理)

虽然此类文件本质上都是字符串(图片、视频都是 Buffer 对象),但以 vue-loader 为例,loader 接收到文件时,会通过字符串匹配将其分为 3 部分,模板字符串将会是 vue-loader编译成render函数,脚本部分会交给babel-loader,样式部分交给css-loader。 同时,装载机遵循单一原则,即一台装载机只做一件事,这样多个装载机可以灵活组合,互不干扰。

实施思路

因为加载器可以读取匹配到的文件,并处理成想要的输出,所以我们可以自己实现一个加载器,接受js文件,遇到await关键字时,用一层try/catch包装代码

那么如何才能将try/catch包裹在await及其旁边的表达式上呢?这里就需要用到抽象语法树(AST)的知识了

谷草转氨酶

抽象语法树是源代码[1]语法[2]结构的具体表示。它以树[3]的形式表达编程语言[4]的句子结构,树上的每个节点代表源代码中的结构

很多特别有用的功能都可以通过AST来实现,比如将ES6之后的代码转换为ES5、eslint检测、代码美化,甚至js引擎也是依靠AST来实现的。 仅限于js之间的转换,scss、less等css预处理器也会通过AST转换浏览器识别的css代码。 我们举个反例

let a = 1
let b = a + 5

将其转换为抽象语法树后,它看起来像这样

将字符串转换为 AST 树需要两个步骤:词法分析和句子分析

词法分析将代码片段转换为标记(词法单元)并删除空格注释。 例如,第一行将 let、a、=、1 转换为标记。 token是一个对象,描述了代码片段在整个代码中的位置以及日志当前值的一些信息

语法分析会将token结合当前语言(JS)的句型转换为Node(节点),Node中包含一个type属性来记录当前的类型。 例如,let在JS中代表变量声明的关键字,所以它的类型是VariableDeclaration,而a=1会作为let的声明描述,它的类型是VariableDeclarator,声明描述依赖于变量声明,所以这是一个层级关系

另外可以发现,一个token并没有对应一个Node。 等号两边必须有值才能构成声明语句,否则会发出警告。 这就是eslint的基本原理。最后将所有Node组合起来生成AST语法树

css写箭头-嘿嘿,异步函数不要写那么多try/catch(部分原理)

推荐一款非常实用的AST查看工具,AST explorer,可以更直观地查看代码是如何转换成抽象语法树的

回到代码的实现,我们只需要通过AST树找到await表达式,并用try/catch Node节点包裹await即可。

async function func() {
await asyncFunc()
}

对应的AST树:

async function func() {
try {
await asyncFunc()
} catch (e) {
console.log(e)
}
}

对应的AST树:

装载机开发

有了具体的想法,接下来我们就开始编译loader。 当我们的加载器接收到源文件时,@babel/parser包可以将文件转换为AST抽象语法树,那么如何找到对应的await表达式呢?

这需要另一个 babel 包 @babel/traverse。 通过@babel/traverse,可以传入一棵AST树和一些钩子函数,然后深度遍历传入的AST树。 当遍历到的节点与钩子函数同名时,会执行对应的反弹

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default

module.exports = function (source) {
let ast = parser.parse(source)

traverse(ast, {
AwaitExpression(path) {
//...
}
})
//...
}

通过@babel/traverse,我们是不是可以很轻松的找到await表达式对应的Node节点,下一步就是创建一个TryStatement类型的Node节点,最后将await放入其中。这里我们还需要依赖另一个包@babel /types,可以理解为babel版本的loadsh库。 它提供了很多与AST的Node节点相关的辅助功能。 我们需要使用tryStatement方法,即创建一个TryStatement节点。 节点

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
let ast = parser.parse(source)

traverse(ast, {
AwaitExpression(path) {
let tryCatchAst = t.tryStatement(
//...
)
//...
}
})
}

try语句接受3个参数,第一个是try子句,第二个是catch子句,第三个是finally子句。 一个完整的try/catch语句对应的Node节点如下所示

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
let ast = parser.parse(source)

traverse(ast, {
AwaitExpression(path) {
let tryCatchAst = t.tryStatement(
// try 子句(必需项)
t.blockStatement([
t.expressionStatement(path.node)
]),
// catch 子句
t.catchClause(
//...
)
)
path.replaceWithMultiple([
tryCatchAst
])
}
})
//...
}

使用 blockStatement 和 expressionStatement 方法创建块级作用域和携带等待表达式的 Node 节点。 @babel/traverse 会给每个钩子函数传递一个路径参数,其中包含了当前遍历的一些信息,比如当前节点、上次遍历的路径对象以及对应的节点,最重要的是有一些方法操作Node节点,我们需要使用replaceWithMultiple方法将当前Node节点替换为try/catch语句的Node节点

另外,我们还要考虑到await表达式可能会被用作声明语句

let res = await asyncFunc()

它也可能是一个赋值语句

res = await asyncFunc()

也可以是简单的表达

await asyncFunc()

这三种情况对应的AST也不同,所以我们需要分别处理。 @bable/types 提供了丰富的区分函数。 在AwaitExpression钩子函数中,我们只需要判断上级节点是哪种类型的Node节点即可,另外还可以使用AST explorer查看最终需要生成的AST树的结构

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
let ast = parser.parse(source)

traverse(ast, {
AwaitExpression(path) {
if (t.isVariableDeclarator(path.parent)) { // 变量声明
let variableDeclarationPath = path.parentPath.parentPath
let tryCatchAst = t.tryStatement(
t.blockStatement([
variableDeclarationPath.node // Ast
]),
t.catchClause(
//...
)
)
variableDeclarationPath.replaceWithMultiple([
tryCatchAst
])
} else if (t.isAssignmentExpression(path.parent)) { // 赋值表达式
let expressionStatementPath = path.parentPath.parentPath
let tryCatchAst = t.tryStatement(
t.blockStatement([
expressionStatementPath.node
]),
t.catchClause(
//...
)
)
expressionStatementPath.replaceWithMultiple([
tryCatchAst
])
} else { // await 表达式
let tryCatchAst = t.tryStatement(
t.blockStatement([
t.expressionStatement(path.node)
]),
t.catchClause(
//...
)
)
path.replaceWithMultiple([
tryCatchAst
])
}
}
})
//...
}

收到替换后的AST树后,使用@babel/core包中的transformFromAstSync方法将AST树转换为对应的代码字符串并返回

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")
const core = require("@babel/core")

module.exports = function (source) {
let ast = parser.parse(source)

traverse(ast, {
AwaitExpression(path) {
// 同上
}
})
return core.transformFromAstSync(ast).code
}

在此基础上还暴露了一些loader配置项,提高易用性。 例如,如果await语句已经被try/catch包裹,则不会再次注入。 原理也是基于AST。 使用path参数的findParent方法向下遍历所有父Node,判断是否被try/catch的Node包裹

traverse(ast, {
AwaitExpression(path) {
if (path.findParent((path) => t.isTryStatement(path.node))) return
// 处理逻辑
}
})

另外,catch子句中的代码片段也支持自定义,以便所有错误都使用统一的逻辑进行处理。 原理是将用户配置的代码片段转换为AST,并在创建TryStatement节点时作为参数传递给catch节点。

css写箭头-嘿嘿,异步函数不要写那么多try/catch(部分原理)

进一步改进

经过评论区交流css写箭头,我会默认在每个await语句中添加一个try/catch,并修改为对整个async函数进行包装try/catch。 原理是先找到await语句,然后递归向下遍历

当发现async函数时,创建一个try/catch Node节点,并将原async函数中的代码作为该Node节点的子节点,并替换async函数的函数体

当遇到try/catch时,说明已经被try/catch包裹了,取消注入,直接退出遍历,这样当用户有自定义的错误捕获代码时,加载器默认的捕获逻辑就不会了被处决

对应的AST树:

对应的AST树:

这只是最基本的async函数声明的node节点。 除此之外,还有函数表达式、箭头函数、作为对象的方式等其他表达形式。 当满足其中一个条件时,注入try/catch代码块

// 函数表达式
const func = async function () {
await asyncFunc()
}

// 箭头函数
const func2 = async () => {
await asyncFunc()
}

// 方法
const vueComponent = {
methods: {
async func() {
await asyncFunc()
}
}
}

总结

本文旨在做一个介绍。 在日常开发过程中,您可以结合自己的业务线css写箭头,开发出更适合您的加载器。 比如技术栈是jQuery的老项目,可以匹配$.ajax的Node节点,统一注入错误处理逻辑。 你甚至可以定制一些ECMA没有的新句型

抱歉,但是如果你明白了编译的原理,你真的可以为所欲为

css写箭头-嘿嘿,异步函数不要写那么多try/catch(部分原理)

通过开发这个loader,你不仅可以了解webpack loader是如何工作的,还可以学到很多关于AST和babel的原理。 更多方式可以查看babel官方文档或者babel手册

我已经在 npm 上发布了这个加载器。 有兴趣的同学可以直接调用 npm install async-catch-loader -D 来安装研究。 它的使用方式与通常的加载器相同。 记得将其放在 babel-loader 后面以优先执行。 注入后的结果继续被babel转义

{
test: /.js$/,
use: [
"babel-loader?cacheDirectory=true",
'async-catch-loader'
]
}

更多细节和源代码请查看github。 如果这篇文章对您有用,希望您能点个star。 非常感谢~

异步捕获加载器

参考

[1]

源代码:%E6%BA%90%E4%BB%A3%E7%A0%81

[2]

语法:%E8%AF%AD%E6%B3%95%E5%AD%A6

[3]

树(图论):%E6%A0%91_(%E5%9B%BE%E8%AE%BA)

[4]

编程语言:%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80

❤️爱心三连击

1.看到这里了就点个在看支持下吧,你的在看是我创作的动力。

2.关注公众号程序员成长指北「带你一起学Node」

3.特殊阶段,带好口罩,做好个人防护。

4.可以添加我微信【ikoala520】,拉你进技术交流群一起学习。

“在看转发”是最大的支持

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 css css写箭头-嘿嘿,异步函数不要写那么多try/catch(部分原理) https://www.wkzy.net/game/160999.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务