解读Promise & Async Functions & Koa@v2
Opened this issue · 1 comments
[TOC]
在ES6 正式确定Promise
规范,以及ES2016+ 确定Async functions
规范后,相对之前的callback hell,在新语法光环下,Javascript 提供了相对比较优雅的方式来编写异步链逻辑。
一个简单的timeout 队列操作
简单举个例子,如果我要按顺序执行四个5 秒的timeout 的操作,可以用下面几种方法来实现:
Thunk
第一种是传统的callback chain,也就是传入cb 函数参数到异步函数中,异步函数完成任务时主动调用cb 函数。
该方式的错误处理机制机制一般是,当异步出错时,调用cb 函数,并将error 信息当做第一个参数传输。若没有出错,则建议cb 函数调用时,第一个参数设为null。
这种方式很容易在需要多个异步队列逻辑时产生回调地狱(callback hell),对代码编写者和阅读者造成极大的困扰。
// callback hell
setTimeout(() => {
// do something in timeout callback of No.1
setTimeout(() => {
// do something in timeout callback of No.2
setTimeout(() => {
// do something in timeout callback of No.3
setTimeout(() => {
// do something in timeout callback of No.4
}, 5000)
}, 5000)
}, 5000)
}, 5000)
Promise Chain
第二种简单利用了Promise Chain 的特性,在then 中注册fulfilled 或rejected 回调,并返回新的Promise 来延续异步队列。
该方式的错误处理机制主要在catch
回调中完成。
这种方法虽然不会产生异步队列,但由于代码中参杂着then
、catch
等promise 方法,也避免不了降低代码的可读性。
// Promise Chain
new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
}).then(() => {
// do something in timeout callback of No.1
return new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
}).then(() => {
// do something in timeout callback of No.2
return new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
}).then(() => {
// do something in timeout callback of No.3
return new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
}).then(() => {
// do something in timeout callback of No.4
})
Generator & Co
第三种方法同时使用了co 与Generator 函数来实现同步编写异步逻辑。co 函数的调用会返回一个Promise 实例。当generator 函数顺利运行所有逻辑并成功返回时,Promise 为fulfilled 状态,而当generator 函数内部抛出了错误,且错误没被内部消化,则会导致Promise 转为rejected 状态。
相较前两种方法,co 与Generator 的组合提供的书写方式已经足够优雅了。更多的关于co 与Generator 组合的解析参考:品味Koa v1.x & Co
// Generator & Co
const co = require('co');
co(function* asyncList() {
yield new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.1
yield new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.2
yield new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.3
yield new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.4
})
Async Functions
第四种方法其实与第三种方法类似,只是不需要借助第三方库co,而是使用Javascript 原生支持的async function 与Promise,来跟踪异步逻辑的状态并提供错误处理机制。
// Async Function
async function asyncList() {
await new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.1
await new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.2
await new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.3
await new Promise((resolve, reject) => {
setTimeout(resolve, 5000);
})
// do something after timeout callback of No.4
}
下文会着重介绍第四种方法,及其在node.js webserver 应用中的实践。
要理解第四种方法,需要对Promise 和Async Functions 有些基础的认识。
使用Promise
Promise 状态机
个人对Promise 的理解类似于单向状态机,Promise 只允许以下三种状态:
- pending:初始状态,未完成或拒绝;
- fulfilled:意味着操作成功完成;
- rejected:意味着操作失败;
所谓单向状态机,即Promise 的状态只允许按一个方向变化一次,变换方向为:
- pending -> fulfilled:Promise 执行的操作成功完成,则状态由初始状态变为已填充状态;
- pending -> rejected:Promise 执行的操作失败,则状态由初始状态变为拒绝状态;
需要注意两点:
- 当Promise 指定的操作还没完成或还没宣告失败前,Promise 都会处于pending 状态;
- Promise 实例的状态一旦发生改变就不会再次发生变化;
另外,每个Promise 的fulfilled
、rejected
状态都有一个对应的状态值,该值在Promise 状态改变时赋值且不再改变。
了解了Promise 的状态机概念后,我们需要继续学习来解决以下三个问题:
- Promise 的状态机跟异步逻辑间的关系是什么呢?
- 如何改变Promise 的状态?
- 如何获取Promise 状态的改变并做响应处理?
初始化Promise 实例
以下通过展示五种不同创建Promise 实例的方法,并介绍每种方法产生的Promise 的初始状态和改变状态的方法,来全面的解答问题2:如何改变Promise 的状态
构造函数
第一种方法使用最基本的构造函数来创建一个Promise 实例,直接看例子:
// new a promise instance
let timeoutPromise = new Promise(function executor(resolve, reject) {
setTimeout(() => {
resolve('succeed');
}, 1000);
})
上面的例子简单的使用new constructor 的方式创建了一个Promsie 实例,注意实例化时传入的函数,本例中命名为executor
,该函数内部即为当前Promise 需要处理的逻辑。同时,函数executor
需要接收两个函数类型的参数,习惯上分别命名为resolve
、reject
。在函数executor
内部为一个简单的timeout 函数,回调中调用了resolve
函数。
现在我们来解答第2 个问题:如何改变Promsie 的状态。
上面例子中的,executor
函数接受的两个函数参数:resolve
、reject
,通过调用这两个函数,即可改变Promise,顾名思义:
- 调用
resolve
,Promise 从pending 转为fulfilled,并将调用时传入的值作为fulfilled 的状态值; - 调用
reject
,Promise 从pending转为rejected,并将调用时传入的值作为rejected
的状态值;
而一旦通过调用resolve
或reject
方法来改变Promise 的状态,Promise 的所有操作都会马上停止。,举个例子:
// promise will pause execution after fulfilled or rejected
let pauseExecutionPromise = new Promsie((resolve, reject) => {
console.log('log before promise fulfilled')
resolve(2);
console.log('log after promise fulfilled')
console.log('log before promise rejected')
reject(3);
console.log('log after promise rejected')
})
上面代码有如下输出:
log before promsie fulfilled
以上输出表明,pauseExecutionPromise
中的逻辑在resolve(2)
执行完之后就停止了,即,后面的所有console.log()
和reject(3)
都跳过了。
构造函数是最基础的初始化方式:new Promsie((resolve, reject) => {})
,详见上文:timeoutPromise
、pauseExecutionPromise
两个Promise 的创建。
该方法所创建的Promise 初始化时处于pending 状态,并等待resolve 或reject 的调用来改变状态。
Promise.resolve(value)
调用Promise.resolve()
方法,并传入一个任意类型的参数value
来创建一个Promise。返回的Promise 状态与所传的参数value
息息相关:
- 若value 是个promise 或thenable 的对象,则Promise 的状态会跟随value 的状态;
- 若value 不是promise 且不是thenable 对象,则Promise 的状态为fulfilled,且对应的状态值等于value;
Promise.reject(value)
调用Promise.reject()
方法,并传入一个任意类型的参数value
来创建一个状态为rejected 的Promise,对应的状态值等于value。
Promise.all([])
Promise.all() 接受一个可遍历的对象,比如数组,或更广层面的来说就是实现了ES6 Iterable 接口的对象。
该方法同样会初始化一个Promise 实例,该实例状态为pending。
当数组中所有子Promise 元素状态都变为fulfilled 时,Promise.all()
自身返回的Promise 的状态就会转为fulfilled,对应的状态值为一个数组,数组中每个元素分别对应于所传入的可遍历对象返回的值,Promise 类型的子元素指的是其fulfilled 状态值。
而一旦数组参数中有任意一个子Promise 状态变为rejected,则整个Promise.all() 返回的Promise 状态都会转为rejected。
Promise.race([])
Promise.race() 接受一个可遍历的对象,比如数组,或更广层面的来说就是实现了ES6 Iterable 接口的对象。
该方法同样会初始化一个Promise 实例,该实例状态为pending。
当数组中有任意一个子Promise 元素状态都变为fulfilled 或rejected 时,Promise.race()
自身返回的Promise 的状态就跟着转为fulfilled 或rejected,对应的状态值为一个数组,数组中每个元素分别对应于所传入的可遍历对象返回的值,Promise 类型的元素指的是其fulfilled 或rejected 状态值。
接下来,我们来解答问题3:如何获取Promise 状态的改变并做响应处理?
响应Promise 状态
Promise 规范提供两种方法来响应Promise 状态:
Promise.prototype.then(resolveCb, rejectCb)
,传入resolveCb
、rejectCb
回调函数来分别响应fulfilled
、rejected
状态Promise.prototype.catch(rejectCb)
,传入rejectCb
回调函数来响应rejected
状态
以上两个方法的使用中,需要注意几点:
- then、catch 方法为Promise 的实例方法;
- Promise 需要响应的状态只有
fulfilled
、rejected
,所以,从上面的方法参数名可以看到,then 方法可以同时响应两个状态,而catch 方法只用来响应rejected
状态; - 当Promise 处于
pending
状态时添加响应回调,则当Promise 状态更改时才会触发响应的回调; - 当Promise 处于
fulfilled
时添加的resolveCb
会马上触发,而rejectCb
回调则永远不会触发; - 当Promise 处于
rejected
时添加的rejectCb
会马上触发,而resolveCb
回调则永远不会触发; - Promise 的状态变化虽然只有一次,但响应回调的订阅则可以有多次,且都能成功触发;
针对上面第6 点,举个例子:
let p1 = Promise.resolve(2);
p1.then(v => {
console.log(`p1 resolved with ${v}`);
});
p1.then(v => {
console.log(`p1 resolved with ${v}`);
});
let p2 = Promise.reject(2);
p2.catch(v => {
console.log(`p2 rejected with ${v}`);
});
p2.catch(v => {
console.log(`p2 rejected with ${v}`);
});
例子的输出为:
p1 resolved with 2
p1 resolved with 2
p2 rejected with 2
p2 rejected with 2
Promise Chains
在第一节的__一个简单的timeout 队列操作__中,我们使用了Promise Chains 的写法来实现队列操作,简单抽象以下,形如:
new Promise().then().then().then()
我们可以使用then
链,将很多步骤以前后顺序串联起来。
Promise Chains 写法的实现基础是,Promise.prototype.then()
、Promise.prototype.catch()
会返回一个新Promise 实例。新的Promise 的状态取决于被触发的状态响应回调的返回值:
- 若回调返回一个子Promise,则新Promise 的状态则跟随子Promise;
- 若回调返回一个非Promise 类型的值,则新Promise 状态为fulfilled,且状态值为该返回值;
举个例子:
Promise.resolve(2).then(v => {
console.log(v);
return 3;
}).then(v => {
console.log(v);
return Promise.reject(4);
}).catch(v => {
console.log(v);
return Promise.resolve(5);
}).then(v => {
console.log(v);
})
以上例子的输出为:
2
3
4
5
另外要说一句就是,Promise Chains 与Promise.all([])
之间的差别是队列执行与并发执行。
最后,来解答下第一个问题:Promise 的状态机跟异步逻辑间的关系是什么呢?
平时遇到的异步逻辑,无论是timer 函数、xhr 请求、node.js 的异步操作,大部分都依赖于回调,即在异步完成时,主动调用回调函数,也就是上文提到的callback chains。
而为了使用上Promise,一般我们需要将Thunk 函数操作转为Promise,可以直接采用Promise 构造函数,并在异步回调触发时根据异步操作是否成功来选择调用resolve 或reject 函数,举个例子:
// 将node.js 的fs.readFile() 异步操作转为Promise
new Promise((resolve, reject) => {
fs.readFile('/tmp/file.txt', (err, body) => {
// 若异步行为有出错,则主动调用reject 函数来将Promise 状态改为rejected
if (err != null) reject(err);
// 若异步行为正确返回,则主动调用resolve 函数来将Promise 状态改为fulfilled
resolve(body);
})
}).then(body => {
console.log(body);
}, err => {
console.log(err.message);
})
当掌握了Thunk 与Promise 的转换写法后,我们就可以用Promise 将异步逻辑包装起来,并使用的使用Promise.all()
和Promise.prototype.then()
来实现异步操作的并发与队列执行。
// todo: unhandleRejection in node.js
Promise 的存在是其他异步写法能实现的基础,所以,花了一些功夫去尝试理解Promise 的一些概念和用法。
下面,我们就来看看Promise 是如何被Async Functions 利用,并形成终极的Javascript 异步逻辑写法。
使用Async Functions
ES2016+ 在Promise 的基础上,制定了新的函数类型:Async Functions 来提供终极的异步逻辑书写语法。
下面简单介绍下Async Functions 的用法:
- 在函数表达式之前添加
async
修饰符来表明所声明的是一个async function; - 函数内部使用
await
运算符来等待一个Promise; - async 函数的调用会返回一个Promise;
- async 函数的调用会马上在当前事件循环执行函数内部逻辑:
- 若async 函数内部全是同步操作且没有错误抛出,则不需要等待,并返回一个状态为fulfilled 的Promise;
- 若async 函数内部全是同步操作但有错误抛出,则不需要等待,并返回一个状态为rejected 的Promise;
- 若async 函数内部需要await 等待异步操作,则会马上返回一个状态为pending 的Promise;
- 若async 函数内部抛出没被捕获的错误,或没有捕获一个状态为
rejected
的promise,则async 函数返回的Promise 状态会变成rejected; - 若async 函数内部运行时没有出现上面第5 点的情况,则返回的Promise 会以函数的返回值为
fulfilled
状态值; - async 函数不能用做构造函数来实例化对象;
- async 函数建议不要使用
this
上下文变量,严格模式下会出错,非严格模式下会污染全局作用域,除非能确保每次async 函数的调用都能bind 到一个确定的对象;
下文尝试对上面8 个用法给出更详细的解释。
1)async 修饰符
声明async 函数与普通函数的区别在于需要使用async
修饰符,同时,async 函数类型继承自普通的函数:
// node.js env
const assert = require('assert');
// a common function
function commonFunction() {}
// an async function
async function asyncFunction(){}
let asyncProto = Object.getPrototypeOf(asyncFunction);
assert.equal(asyncFunction[Symbol.toStringTag], 'AsyncFunction');
assert.equal(Object.prototype.toString.call(asyncFunction), '[object AsyncFunction]');
assert.equal(Object.getPrototypeOf(asyncProto), Function.prototype, 'common function prototype is inside of async function prototype chain');
2)await 运算符
async 函数内部使用await
运算符来等待一个Promise 状态的改变。当async 函数内部执行到await
运算符语句时,会暂停,待await 的操作数返回后才继续往下执行。这跟Generator 的yield 运算符是类似的。
await 运算符的返回值为后接表达式的计算值,若表达式是个Promise,则返回值为Promsie fulfilled 状态值。
await 运算符的使用是非常灵活的,可以接各种类型的操作数,但谨记,运算符使用时一定要带操作数,否则语法上会出错,举些例子:
async function awaitEveryThingEsceptBlank() {
// do not use: `await;`
console.log(await 1);
console.log(await 'foo');
console.log(await null);
console.log(await undefined);
console.log(await false);
console.log(await {foo: 'bar'});
console.log(await [1, 2, 3]);
console.log(await Promise.resolve('promise resolved'));
console.log(await { then: (cb) => cb('thenable object resolved') });
}
awaitEveryThingEsceptBlank();
以上例子的输出是:
1
foo
null
undefined
false
{foo: 'bar'}
[1, 2, 3]
promise resolved
thenable object resolved
上述例子中有一种成为thenable 类型的对象是值得留意下的。若对象中有一个叫then
的方法,则可称该类对象为thenable 对象,这种对象在Javascript 看来,与Promise 类型的对象是类似的。在很多宽松的第三方库中,判断一个对象是否为Promise 类型对象,最简单的方法就是判断该对象是否有then
方法。而then 方法需要接受一个函数类型的参数,假如命名为cb
,当then 方法内主动调用cb 函数时,就相当于告知外界,当前的thenable 对象成功返回了。
await 运算符遇到这种thenable 类型对象时,会传入一个函数到then 方法中,当函数参数被调用时,意味着,thenable 对象返回了,async 函数恢复执行。
3)async 函数的返回与运行时机
async 函数的调用会返回一个Promise 类型对象,因此可以使用then
、catch
等方法来响应async 函数内部逻辑是否完成或是否有出错。
就async 函数本身的运行时机而言,与普通的函数是没有差别的,即,当调用async 函数时,函数内部的逻辑会马上运行。差别在于,当async 函数内部遇到await 一个异步逻辑时,会暂停,此时async 函数会将主线程的执行权返回给外部,让余下的逻辑继续执行。举个例子:
async function a1() {
console.log(`1: async function executing`)
await new Promise(resolve => {
setTimeout(resolve, 200);
});
console.log(`2: async do nothing anymore`);
}
console.log('3: before invoke async function');
a1().then(() => {
console.log('4: async function resolve');
})
console.log('5: after invoke async function');
例子输出为:
3: before invoke async function
1: async function executing
5: after invoke async function
2: async do nothing anymore
4: async function resolve
来解释下上面的输出顺序:
- async 函数a1 声明本身时不会有任何输出的;
- 遇到第一句会输出的语句:
console.log('3: before invoke async function');
; - 触发async 函数a1 的调用:
a1()
,并返回一个状态为pending 的Promise 类型对象,然后使用then 方法响应该Promise 的状态改变; - a1 函数内部逻辑马上开始执行,执行语句并输出内容:
console.log(
1: async function executing)
; - a1 函数内部await 了一个200ms 的异步timer,此时暂停a1 内部的执行,并将主线程的执行权返回给外部;
- 执行语句并输出内容:
5: after invoke async function
; - 主线程的逻辑运行完了,此时事件队列为空;
- async 内部触发的200ms 异步timer 时间到了,并将回调函数中的逻辑添加到事件队列中;
- 事件队列开始运行
resolve
函数,即async 函数内部第一个await 的Promise 成功resolve,状态由pending 转为fulfilled
,此时await 运算符成功返回,主线程的执行权交还给async 函数,开始执行函数a1 内部剩余的逻辑; - 执行语句并输出内容:
console.log(2: async do nothing anymore);
。当执行到改行语句时,async 函数内部也执行完成了,此时a1 函数调用返回的Promise 也resolve 了,状态由pending
转为fulfilled
; - a1 函数调用返回的Promise 触发resolve 回调,此处即执行以下语句并输出内容:
console.log('4: async function resolve');
4)错误捕获
async 函数的返回结果是一个Promise,而让该Promise 状态变为rejected 的一般有两种场景:
- async 函数内部有未被捕获的错误抛出;
- await 运算符等待的子Promise 状态变为rejected,且未被捕获;
举个例子:
// async promise will be rejected if error throw with no try-catch
async function throwWithoutTryCatch() {
throw new Error('error occur');
}
throwWithoutTryCatch().catch(err => {
console.log(`throw error without try-catch will change promise to rejected: ${e.message}`);
})
// async promise will be rejected if await a rejected sub promise with no try-catch
async function subPromiseRejectedWithoutTryCatch() {
await Promise.reject('error occur');
}
subPromiseRejectedWithoutTryCatch().catch(err => {
console.log(`await a rejected sub promise without try-catch will change promise to rejected: ${e.message}`);
})
例子的输出是:
throw error without try-catch will change promise to rejected: error occur
await a rejected sub promise without try-catch will change promise to rejected: error occur
相反,如果async 函数内用try-catch 成功捕获所有抛出的错误或rejected sub promise,或根本就没出错,则返回的promise 状态会变为fulfilled
。再举个例子:
async function throwWithTryCatch() {
try {
throw new Error('error occur');
} catch (e) {
console.log(`1. error catch: ${e.message}`);
}
return 2;
}
throwWithTryCatch().then(v => {
console.log(`2. async promise resolved with ${v}`);
})
async function subPromiseRejectedWithTryCatch() {
try {
throw Promise.reject('error occur');
} catch (e) {
console.log(`3. error catch: ${e.message}`);
}
return 2;
}
subPromiseRejectedWithTryCatch().then(v => {
console.log(`4. async promise resolved with ${v}`);
})
例子输出是:
1. error catch: error occur
2. async promise resolved with 2
3. error catch: error occur
4. async promise resolved with 2
以上介绍完Promise 与Async Functions 的一些概念和用法之后,就大致上可以理解上文中使用Async Functions
来实现一个简单的timeout 队列操作的思路了。趁热,我们接着来学习下目前为止,Async Functions 在node.js web server 上的实践,具体而言,指的就是:Koa@v2。
Koa@v2 的Async Functions 实践
在node.js web server 的框架支持中,Koa 团队一直都尝试提供更优雅的中间件运行逻辑、异步逻辑写法。从v2 之前,使用的方案是:Generator + co.js。而当v8 引擎正式支持Async Functions 之后,Koa 也正式发布v2,提供了新的解决方案:Async Functions + koa-compose。
这套方案的核心点是Async Functions,而附加的koa-compose 作用是将洋葱式的中间件调用顺序融入到Async Functions 的特点中,并提供相对顶层的错误处理。
在深入了解koa-compose 之前,先来看看koa@v2 中的中间件书写规范。kao@v2 建议每个中间件都是async 函数,并接受两个参数:context(当此请求的上下为)、next(触发下一个中间件运行的函数)。举个例子:
const Koa = require('koa');
const assert = require('assert');
const app = new Koa;
app.use(async function xResponseTime(context, next) {
let startHrTime = process.hrtime();
await next();
let intervalHrTime = process.hrtime(startHrTime);
console.log(`request span ${intervalHrTime[0]} seconds and ${intervalHrTime[1]} nanoseconds`);
});
app.use(async (context, next) => {
console.log(await Promise.resolve(3));
let i = await next();
assert.equal(i, 'foo', 'the result of await next() is equal to the result return from the next middleware.');
});
app.use(async (context, next) => {
await next();
return 'foo';
});
上面的例子一共定义了3 个中间件:
- 第一个中间件是一个典型的x-response-time 中间件,用来记录请求时长,时长的计算分别需要在下一个中间件运行前后统计;
- 第二个中间件在于保存了
await next()
返回的值,该值来自于第三个中间件asycn 函数的return 语句; - 第三个中间件只是简单的返回了一个值给第二个中间件;
另外,在日常的中间件书写时有两个点需要留意一下:
- 不推荐第二个中间件中,直接保存
await next()
返回值的做法,而是尝试将返回值保存在context
中,除非你能完全保证中间件的次序不会发生改变,且日后不会有其它中间件插入到中间; - 除非当前中间件的某个分支必定是当此请求的终点,否则,建议还是主动调用
next()
来完成整个中间件队列。参考第三个中间件,很明显在它之后已经没有中间件了,但难保以后不会有,所以,还是会调用:await next()
;
接下来看下koa-compose 的核心源码来分析koa 的中间件运行逻辑:
// koa-compose/index.js
function compose (middleware) {
// 确保middleware 参数是数组类型
// 且,数组内每个元素都是函数类型
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// 记录最近被调用的中间件在middleware 数组中的索引
let index = -1
// 触发第一个中间件的执行
return dispatch(0)
// 每个中间件的执行以及错误处理都包装在dispatch 函数中
// 调用dispatch 函数时传入的参数i 指的是要运行的中间件在middleware 数组中的索引
// dispatch 函数会返回一个Promise
function dispatch (i) {
// 会出现 i <= index 的原因一般是中间件编写者多次调用了next()。
// 在Koa 的规范中,每个中间件在每次请求中指会运行一次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 更新index
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
// 这里需要try-catch 的原因是:
// 当中间件不是async 函数,而是普通函数时,默认时会往上抛出错误的,所以,需要捕抓
// 而如果中间件是async 函数,则函数调用时抛出的错误默认会被捕捉,并随着Promise 一起rejected
try {
// fn 函数即为中间件函数
// 当fn 是async 函数时,fn() 的调用会返回一个Promise,
// 当fn 是普通函数时,fn() 的调用可能会返回任意类型的值
// 而将fn() 的返回传给Promise.resolve() 来创建一个Promise 实例,
// 好处时,Promise.resolve() 创建的实例状态会依据参数而定,具体参见上文对Promise.resolve() 的解析
// 留意传给fn 的第二个参数,即next 函数
// 该函数内部会递归调用dispatch,来触发下一个中间件的运行,并返回Promise
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
// 当有错误被捕抓到,则主动返回一个状态为rejected 的Promise
return Promise.reject(err)
}
}
}
}
逐行分析完核心代码,最后稍微再理顺一下思路即可:
- 每个中间件运行都被dispatch 函数所封装,最大的好处是保证返回一个promise 实例;
- 当要运行启动下一个中间件时,调用next:
await next()
。next()
运行dispatch 封装函数,返回一个promise 实例; - dispatch 函数的运行实质上是调用了对应中间件async 函数:
- 若async 函数同步执行时抛错误,则会dispatch 的try-catch 捕获到,并导致dispatch 返回一个状态为rejected 的promise;
- 若async 函数同步执行时没有抛错误,则async 函数能返回一个状态为pending 的promise;
- 当async 函数内部出现未捕获的错误或未捕获的状态为rejected 的子promise,async 函数返回的promise 状态转为rejected。这种情况一般是某个await 的异步逻辑出错了,或下层中间件promise 状态为rejected;
- 若async 函数内部运行的所有异步逻辑都fulfilled 了,则当前中间件promise 状态为fulfilled;
- 反观第3 点,当下层中间件reject 了,可能会导致上层中间件也reject,依次类推,直到有上层中间件在
await next()
时使用了try-catch
,这就是Koa 中一直沿用的错误处理机制:Error 可以冒泡到上层中间件负责处理,如果一直没有中间件处理,则会冒泡到Koa 本身,也就是说,Koa 框架本身提供了最顶层的冒泡处理,避免错误信息丢失或进程被中断;
参考文章
赞👍