Node 中间件的简析及应用
前言
如果你有 express ,koa, redux 的使用经验,就会发现他们都有 中间件(middlewares) 的概念,中间件是一种拦截器的思想,用于在某个特定的输入输出之间添加一些额外处理,同时不影响原有操作。
先来个🌰
import express from 'express';
const app = express();
// Middleware 1
app.use((req, res, next) => {
console.log('第一层 - 开始');
next();
console.log('第一层 - 结束');
});
// Middleware 2
app.use((req, res, next) => {
console.log('第二层 - 开始');
next();
console.log('第二层 - 结束');
});
app.listen(3001, () => {
console.log('server is running on port 3001');
});
import Koa from 'koa';
const app = new Koa();
// Middleware 1
app.use(async (ctx, next) => {
console.log('第一层 - 开始');
await next();
console.log('第一层 - 结束');
});
// Middleware 2
app.use(async (ctx, next) => {
console.log('第二层 - 开始');
await next();
console.log('第二层 - 结束');
});
app.listen(3002, () => {
console.log('Koa app listening on 3002')
});
上面两段程序运行结果是否一样?打印内容是什么?
server is running on port 3001/3002
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束
两段程序同时加上和减去 async/await 时结果如何?
同上
两段程序在 async 内部增加异步操作时结果如何?
import express from 'express';
const app = express();
function delay(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
app.use(async (req, res, next) => {
console.log('第一层 - 开始');
// 情况一 await delay(1000);
await next();
console.log('第一层 - 结束');
});
app.use(async (req, res, next) => {
console.log('第二层 - 开始');
// 情况二 await delay(1000);
await next();
console.log('第二层 - 结束');
});
app.listen(3001, () => {
console.log('server is running on port 3001');
});
情况一运行结果
# Express 情况一
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束
情况二运行结果
# Express 情况二
第一层 - 开始
第二层 - 开始
第一层 - 结束
第二层 - 结束
import Koa from 'koa';
const app = new Koa();
function delay(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
app.use(async (ctx, next) => {
console.log('第一层 - 开始');
// 情况一 await delay(1000);
await next();
console.log('第一层 - 结束');
});
app.use(async (ctx, next) => {
console.log('第二层 - 开始');
// 情况二 await delay(1000);
await next();
console.log('第二层 - 结束');
});
app.listen(3002, () => {
console.log('server is running on port 3002');
});
情况一运行结果
# Koa 情况一
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束
情况二运行结果
# Koa 情况二
第一层 - 开始
第二层 - 开始
第二层 - 结束
第一层 - 结束
Express 和 Koa 中间件模型
-
express中间件是一个接一个的顺序执行,一种线性的逻辑,在同一个线程上完成所有的 HTTP 请求。
express中我们是通过res.send来向客户端响应数据的(习惯于将响应写在最后一个中间件中),不能在多个中间件里面res.send。(res.send之后及时继续调用 next,也已经和返回给客户端的结果无关了)
优点:线性逻辑,通过中间件形式把业务逻辑细分、简化,一个请求进来经过一系列中间件处理后再响应给用户,清晰明了。
缺点:基于 callback 组合业务逻辑,业务逻辑复杂时嵌套过多,异常捕获困难。 -
koa洋葱模型
在koa中我们是通过ctx.body,可以在多个中间件里面修改它,所有中间件都执行完了之后,才会响应给客户端。
优点:首先,借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。其次,koa把express中内置的 router、view 等功能都移除了,使得框架本身更轻量。
缺点:相对于express社区较小,实现路由等更多功能需要配合其他插件
实际上,express 的中间件也可以形成“洋葱圈”模型,在 next 调用后写的代码同样会执行到,不过express中一般不会这么做,因为 express的response一般在最后一个中间件,那么其它中间件 next 后的代码已经影响不到最终响应结果了。
// Koa 实现(放在中间件的那个位置?)
async function responseTime(ctx, next) {
const started = Date.now();
await next();
const ellapsedTime = (Date.now() - started) + 'ms';
ctx.set('X-ResponseTime', ellapsedTime);
}
app.use(responseTime);
Express 中怎么实现?
eg. 把开始时间挂载到 req 上,在中间件的最后一个计算时间间隔并返回结果(一个中间件实现不了)…
如果只是后端记录耗时,可以通过 Express 的事件机制,监听 finish 和 close 事件
function responseTime() {
return function (req: Request & any, res: Response & any, next: NextFunction) {
req._startTime = new Date().valueOf(); // 获取时间 t1
const callResponseTime = function () {
const now = new Date().valueOf(); // 获取时间 t2
const ellapsedTime = now - req._startTime;
console.log('finish time', ellapsedTime);
// ...
};
res.once('finish', callResponseTime);
res.once('close', callResponseTime);
return next();
};
}
// 不需要发送给请求响应时可以用一个中间件实现
app.use(responseTime());
Express 和 Koa 错误处理
Express 错误处理
在 Express 里面是使用的一个接收四个参数的中间件来处理的。这个中间件作为是最后一个中间件,其他中间件运行的时候,如果报错,将错误使用 next 传递给错误出来,上边这个中间件就能捕获到,进行处理。
猜一下下面同步和异步的错误捕获情况?
// 1. 同步
app.use((req, res, next) => {
const a = c; // c 没有定义
next();
});
// 2. 异步
app.use((req, res, next) => {
try {
setTimeout(() => {
const a = c; // c 没有定义
next();
}, 0);
} catch(e) {
console.log('异步错误,能 catch 到么??')
}
});
// 3. 异步 + async/await
app.use(async (req, res, next) => {
try {
await (() => new Promise((resolve, reject) => {
http.get('http://www.example.com//123', res => {
reject('假设错误了');
next();
}).on('error', (e) => {
throw new Error(e);
})
}))();
} catch(e) {
console.log('异步错误,能 catch 到么??', e)
}
});
// Express 错误捕获
app.use((err, req, res, next) => {
const errLog = `
+---------------+-------------------------+
错误名称:${err.name},\n
错误信息:${err.message},\n
错误时间:${new Date()},\n
错误堆栈:${err.stack},\n
+---------------+-------------------------+
`;
fs.appendFile(path.join(__dirname, "error.log"), errLog, ()=>{
res.writeHead(500, {'Content-Type': 'text/html;charset=utf-8'});
res.end(`500 服务器内部错误`);
});
});
// Node 错误捕获
process.on("uncaughtException", (err) => {
console.log("uncaughtException message is::", err);
})
结论及分析
-
同步的时候,不会触发
uncaughtException,而进入了错误处理的中间件。
同步逻辑错误获取的底层逻辑: Express 内部对同步发生的错误进行了拦截,所以不会传到负责兜底的 node 事件uncaughtException,如果发生了错误,则直接绕过其它中间件,进入错误处理中间件,即使没有错误处理中间件做兜底,也不会进入uncaughtException,会直报500错误。 -
异步的时候,不会触发错误处理中间件, 而会触发
uncaughtException。
异步逻辑错误获取的底层逻辑:还是因为 Express 的中间件执行是同步顺序执行的。 所以如果有异步的,那么错误处理中间件实际是兜不住的,所以,Express 对这种中间件中的异步处理错误无能为力。
除了错误处理中间件没有触发,我们当中的try catch 也没有触发。 -
异步 +
async await,我们不仅try catch可以获取到,uncaughtException也可以获取到。
async/await 是使用生成器、promise 和协程实现的, **wait 操作符还存储返回事件循环之前的执行上下文**,以便允许 promise 操作继续进行。当内部通知解决等待的承诺时,它会在继续之前恢复执行上下文。
所以说,async await能够回到最外层的上下文, 那就可以用 try catch 了。
await/async 是 Generator 语法糖
Koa 错误处理
上面提过洋葱模型,特点是最开始的中间件,在最后才执行完毕,所以,在 Koa 中,可以把错误处理中间件放到中间件逻辑最前面。
const catchError = async(ctx, next) => {
try{
await next();
} catch(error) {
ctx.status = 400
ctx.body = `Uh-oh: ${err.message}`
console.log('Error handler:', err.message)
}
}
app.use(catchError)
Express 和 Koa 的实现源码简析
Express 的实现分析
// express.js
var proto = require('./application');
var mixin = require('merge-descriptors');
exports = module.exports = createApplication;
function createApplication() {
// app 同时是一个方法,作为 http.createServer 的处理函数
var app = function(req, res, next) {
app.handle(req, res, next)
}
mixin(app, proto, false);
return app
}
就是一个 createApplication方法用于创建 express实例,要注意返回值 app既是实例对象,上面挂载了很多方法,同时它本身也是一个方法,作为 http.createServer的处理函数。
// application.js
var http = require('http');
var flatten = require('array-flatten');
var app = exports = module.exports = {}
app.listen = function listen() {
var server = http.createServer(this)
return server.listen.apply(server, arguments)
}
app.listen 调用 http 模块创建一个服务服务,可以看到这里 var server = http.createServer(this) 其中 this 即 app 本身,然后真正的处理程序是 app.handle。
中间件处理
express 本质上就是一个中间件管理器,当进入到 app.handle 的时候就是对中间件进行执行的时候。
两个函数:app.use 和 app.handle。
- app.use 添加中间件
app.use = function(fn) {
this.stack.push(fn)
}
app.use全局维护一个stack数组用来存储所有中间件。
express 的真正实现当然不会这么简单,它内置实现了路由功能(router),app.use 方法实际调用的是 router 实例的 use 方法。
- app.handle 尾递归调用中间件处理 req 和 res
// router/index.js
app.handle = function(req, res, callback) {
var stack = this.stack;
var idx = 0;
function next(err) {
if (idx >= stack.length) {
callback('err')
return;
}
var mid;
while(idx < stack.length) {
layer = stack[idx++];
route = layer.route;
// 递归调用执行
layer.handle_request(req, res, next)
}
}
next();
}
// router/layer.js
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
“尾递归调用” :next 方法不断的取出stack中的中间件函数进行调用,同时把next 本身传递给中间件作为第三个参数,每个中间件约定的固定形式为 (req, res, next) => {},这样每个中间件函数中只要调用 next 方法即可传递调用下一个中间件。
之所以说是”尾递归“是因为递归函数的最后一条语句是调用函数本身,”尾递归“相对于普通”递归“的好处在于节省内存空间,不会形成深度嵌套的函数调用栈。原理分析可以阅读下阮一峰老师的尾调用优化。
尾调用节省内存的原因归纳
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
Koa 的实现分析
相比较 express而言,koa的整体设计和代码实现显得更高级、更精炼。代码基于ES6实现,支持generator(async await), 没有内置的路由实现和任何内置中间件,context的设计也很是巧妙。
一共只有4个文件:
- application.js 入口文件,
koa应用实例的类; - context.js
ctx实例,代理了很多request和response的属性和方法,作为全局对象传递; - request.js
koa对原生req对象的封装; - response.js
koa对原生res对象的封装。
中间件机制是 Koa 的核心。
Koa 的中间件使用很简单:使用 app.use 去给 Koa 注册一个中间件即可。
use方法就是做了一件事,维护得到 middleware中间件数组
// application.js
const http = require('http');
const Emitter = require('events');
const response = require('./response')
const compose = require('koa-compose')
const context = require('./context')
const request = require('./request')
module.exports = class Koa extends Emitter {
constructor() {
super();
this.middleware = []
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)
}
use(fn) {
this.middleware.push(fn);
return this;
}
listen(...arg) {
// 同样的在 listen 方法中创建 web 服务
const server = http.createServer(this.callback);
return server.listen(...arg);
}
callback() {
// 调用 koa-compose 核心方法
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
handleRequest(ctx, fnMiddleware) {
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
};
koa-compose 函数
将中间件函数队列组合成一个函数并且可以异步顺序执行。
compose 1.0 版本:在没有 async 的情况下,compose的实现其实相当复杂,利用了generator + co来进行异步管理。
compose 2.0 版本:基于 Promise 实现
function compose(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!')
}
return function(context, next) {
// 这里 next 指的是洋葱模型的中心函数
// context 是一个配置对象,保存着一些配置,也可以利用 context 将一些参数往下一个中间传递
// last called middleware #
let index = -1 // 记录执行的中间件的索引
// 取出第一个中间件函数执行,然后通过第一个中间件递归调用下一个中间件
return dispatch(0)
function dispatch(i) {
// 这里是保证同个中间件中一个 next 不被调用多次调用
// 第一次 i 为 0,index 为 -1,可以继续走下去,index 被赋值为 0
// 当 next 函数被调用两次的时候,i 会小于 index,然后抛出错误
// eg. dispatch(0) => dispatch(1) + dispatch(1),执行第二个 dispatch(1) 时 index 为 1,1 <= 1 成立
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 取出数组里的 fn1, fn2, fn3...
let fn = middleware[i]
// 当 middleware 为空或者到了洋葱模型的中心(最后一个中间件)时将 next 赋值给 fn
if (i === middleware.length) fn = next
// 如果中间件为空,即直接 resolve
if (!fn) return Promise.resolve()
try {
// bind 函数是返回一个新的函数。
// i + 1 是为了 let fn = middleware[i] 取 middleware 中的下一个函数。
// 也就是 next 是下一个中间件里的函数。
// 所以使用中间件时需要「await next()」
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
简化成下面这样就可能更好理解了:
Promise.resolve( // 第一个中间件
function (context, next) { // 这里的 next 第二个中间件也就是 dispatch(1)
// await next 上的代码 (中间件1)
Promise.resolve( // 第二个中间件
function (context, next) { // 这里的 next 第二个中间件也就是 dispatch(2)
// await next 上的代码 (中间件2)
Promise.resolve( // 第三个中间件
function (context, next) { // 这里的 next 第二个中间件也就是 dispatch(3)
// await next 上的代码 (中间件3)
Promise.resolve();
// await next 下的代码 (中间件3)
}
);
// await next 下的代码 (中间件2)
}
);
// await next 下的代码 (中间件2)
}
);
和express中的next 很类似,只不过他是 promise 形式的,因为要支持异步:每个中间件是一个**async (ctx, next) => {}**(Express 为**(req, res, next) => {}**)。 执行后返回的是一个promise,第二个参数 next的值为 dispatch.bind(null, i + 1) ,用于传递中间件的执行,一个个中间件向里执行,直到最后一个中间件执行完,resolve 掉,它前一个中间件接着执行 await next() 后的代码,然后 resolve 掉,在不断向前直到第一个中间件 resolve掉,最终使得最外层的promise resolve掉。
这里和 express 很不同的一点就是 koa 的响应的处理并不在中间件中,而是在中间件执行完返回的 promise resolve 后:return fnMiddleware(ctx).then(handleResponse).catch(onerror),通过 handleResponse 最后对响应做处理,中间件会设置 ctx.body,handleResponse 也会主要处理 ctx.body ,所以 koa 的”洋葱圈“模型才会成立,await next() 后的代码也会影响到最后的响应。respond 源码如下:
// const handleResponse = () => **respond**(ctx)
// return fnMiddleware(ctx).then(handleResponse).catch(...)
function respond (ctx) {
const res = ctx.res
let body = ctx.body
const code = ctx.status
// status body
if (body == null) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code)
} else {
body = ctx.message || String(code)
}
return res.end(body)
}
// responses
if (Buffer.isBuffer(body)) return res.end(body)
if (typeof body === 'string') return res.end(body)
if (body instanceof Stream) return body.pipe(res)
// body: json
body = JSON.stringify(body)
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body)
}
res.end(body)
}
koa: 中间件执行完后通过 handleResponse(respond) 中的 ctx.res.end(…) 返回相应结果。
express: 每个中间件都可以拿到 res 执行 res.end(…) 返回相应结果。
Redux 中的 middleware
Redux 的基本流程:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。
Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
怎么才能 Reducer 在异步操作结束后自动执行呢(异步操作至少要送出两个 Action:用户触发第一个 Action,这个跟同步操作一样;异步完成后,触发第二个 Action 去改变 store )?
这就要用到新的工具:中间件(middleware)
Redux middleware 解决的问题与 Express 或 Koa middleware 不同,但在概念上是相似的。它在 dispatch action 的时候和 action 到达 reducer 那一刻之间提供了三方的逻辑拓展点( 主要增强 store.dispatch 的能力 ) 。可以使用 Redux middleware 进行日志记录、故障监控上报、与异步 API 通信、路由等。
以添加日志功能举例,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造:
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
从上图中得出结论,middleware 通过 next(action) 一层层处理和传递 action 直到 redux 原生的 dispatch。而如果某个 middleware 使用 store.dispatch(action) 来分发 action,就相当于重新来一遍。
在 middleware 中使用 dispatch 的场景一般是接受一个定向 action,这个 action 并不希望到达原生的分发 action,往往用在一步请求的需求里,如 redux-thunk,就是直接接受 dispatch。
如果一直简单粗暴调用 store.dispatch(action),就会形成无限循环。
中间件执行顺序
应用了如下的中间件: [A, B, C],
整个执行 action 的过程为 A -> B -> C -> dispatch -> C -> B -> A
const mid1 = ({getState, dispatch}) => next => action => {
console.log(`middleware1 before next action `)
next(action)
console.log(`middleware1 after next action `)
}
const mid2 = ({getState, dispatch}) => next => action => {
console.log(`middleware2 before next action `)
next(action)
console.log(`middleware2 after next action `)
}
const mid3 = ({getState, dispatch}) => next => action => {
console.log(`middleware3 before next action `)
next(action)
console.log(`middleware3 after next action `)
}
const sagaMiddleware = createSagaMiddleware()
// 实际的执行是次序是 store.dispatch -> Mid3 -> Mid2 -> Mid1
const store = createStore(reducer, applyMiddleware(mid1, mid2, mid3))
中间件实现原理:compose
export default function compose(...funcs) {
// 没传任何参数,则默认返回一个空的函数
if (funcs.length === 0) {
return (arg) => arg
}
// 只传一个那就直接返回这一个咯
if (funcs.length === 1) {
return funcs[0]
}
// 将所有函数组合,返回一个函数
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
eg. compose(f, g, h) 返回 () => f(g(h(..args)))
现在再理解增强 store.dispatch 的能力:
dispatch = compose(...chain)(store.dispatch) ⇒ dispatch = f1(f2(f3(store.dispatch))))
原生的 store.dispatch传入最后一个中间件,返回一个新的 dispatch,再向外传递到前一个中间件,直至返回最终的 dispatch,当覆写后的dispatch 调用时,每个中间件的执行又是从外向内的”洋葱圈“模型。
Nestjs 中的“洋葱模型”
在 Nestjs 中在 service 的基础上,按处理的层次补充了中间件(middleware)、异常处理(Exception filters)、管道(Pipes)、守卫(Guards),以及拦截器(interceptors)在请求到打真正的处理函数之间进行了层层的处理。
- Pipes 一般用户验证请求中参数是否符合要求,起到一个校验参数的功能。
- Guards 守卫,其作用就是决定一个请求是否应该被处理函数接受并处理,当然我们也可以在 middleware 中间件中来做请求的接受与否的处理,与 middleware 相比,Guards 可以获得更加详细的关于请求的执行上下文信息。
通常 Guards 守卫层,位于 middleware 之后,请求正式被处理函数处理之前。 - Interceptors 拦截器可以给每一个需要执行的函数绑定,拦截器将在该函数执行前或者执行后运行。可以转换函数执行后返回的结果等。
Interceptors 拦截器在函数执行前或者执行后可以运行,如果在执行后运行,可以拦截函数执行的返回结果,修改参数等。** - Exception filters 异常过滤器可以捕获在后端接受处理任何阶段所跑出的异常,捕获到异常后,然后返回处理过的异常结果给客户端(比如返回错误码,错误提示信息等等)。
我们可以自定义一个异常过滤器,并且在这个异常过滤器中可以指定需要捕获哪些异常,并且对于这些异常应该返回什么结果等,举例一个自定义过滤器用于捕获 HttpException 异常的例子。
在 Nestjs 中的中间件完全跟 Express 的中间件一摸一样。不仅如此,我们还可以直接使用 Express 中的中间件,比如在我的应用中需要处理 cors 跨域:
import * as cors from 'cors';
async function bootstrap() {
const app = await NestFactory.create(/* 创建app的业务逻辑*/);
app.use(cors({
origin:'http://localhost:8080',
credentials:true
}));
await app.listen(3000)
}
bootstrap();
初此之外,跟 Nestjs 的中间件也完全保留了 Express 中的中间件的特点:
- 在中间件中接受 response 和 request 作为参数,并且可以修改请求对象 request 和结果返回对象 response。
- 可以结束对于请求的处理,直接将请求的结果返回,也就是说可以在中间件中直接 res.send 等。
- 在该中间件处理完毕后,如果没有将请求结果返回,那么可以通过 next 方法,将中间件传递给下一个中间件处理。
在 Nestjs 中,中间件跟 Express 中完全一样,除了可以复用 Express 中间件外,在 Nestjs 中针对某一个特定的路由来使用中间件也十分的方便:
class ApplicationModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
上面就是对于特定的路由 url 为 /cats 的时候,使用 LoggerMiddleware 中间件。
问答
- Express 和 Koa 的中间件处理请求时,都可以在 next 方法前面或者后面异步操作?
import express from 'express';
const app = express();
app.use(async (req, res, next) => {
console.log('第一层 - 开始');
await next();
console.log('第一层 - 结束');
});
app.use(async (req, res, next) => {
console.log('第二层 - 开始');
await delay(1000); // 异步操作
await next();
console.log('第二层 - 结束');
});
app.listen(3001, () => {
console.log('server is running on port 3001');
});
import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
console.log('第一层 - 开始');
await next();
console.log('第一层 - 结束');
});
app.use(async (ctx, next) => {
console.log('第二层 - 开始');
await delay(1000); // 异步操作
await next();
console.log('第二层 - 结束');
});
app.listen(3002, () => {
console.log('Koa app listening on 3002')
});
Koa 可以,Express 不可以
- 下面的错误能被 catch 到吗?
try{
setTimeout(() => {
const a = c; // c 未定义
}, 100)
} catch(e) {
console.log('能获取到错误么', e);
}
不能
最外层的 try catch 是在一个task中,我们定义它为我们 js 文件的同步主任务,从上到下执行到这里了,然后会把里面的 setTimeout 推到一个任务队列中, 这个队列是存储在内存中的,由V8来管理。
然后主 task 就继续向下执行,一直到结束。当该 setTimeout 时间到了,且没有其它的 task 执行了, 那么,就将这个 setTimeout 的代码推入执行栈开始执行。 当执行到错误代码的时候,也就是这个 const a = c,因为 c 未定义,所以就会报错。
但问题的本质是,这个错误跟最外层的 try catch 并不在一个执行栈中,当里面执行的时候,外边的这个 task 早已执行完, 他们的上下文已经完全不同了。 所以,会直接报错,甚至程序崩溃。
不能
原因其实与上面的 setTimeout 是一样的,执行栈上下文已经不同了。
能
async/await 是使用生成器、promise 和协程实现的, wait 操作符还存储返回事件循环之前的执行上下文,以便允许 promise 操作继续进行。当内部通知解决等待的承诺时,它会在继续之前恢复执行上下文。
所以说,能够回到最外层的上下文, 那就可以用 try catch 了。
try {
new Promise((resolve, reject) => {
reject('promise error');
})
} catch(e) {
console.log('异步错误,能 catch 到么', e);
}
不能
原因其实与上面的 setTimeout 是一样的,执行栈上下文已经不同了。
const asyncFn = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('asyncFn执行时出现错误了')
}, 100);
})
}
const executedFn = async () => {
try {
await asyncFn();
} catch(e) {
console.log('异步错误,能 catch 到么', e);
}
}
能
async/await 是使用生成器、promise 和协程实现的, wait 操作符还存储返回事件循环之前的执行上下文,以便允许 promise 操作继续进行。当内部通知解决等待的承诺时,它会在继续之前恢复执行上下文。
所以说,能够回到最外层的上下文, 那就可以用 try catch 了。