最近看到了一个非常有意思的所谓的大厂的面试题,在网上也引起了很大的争议。那就是async/await消除异步的传染性,以fetch函数为例,通过侵入式修改fetch方法,来实现消除异步的传染性的方案。
这种方案其实是一股脑的借鉴一些框架的实现,如react框架中的父组件加载子组件的实现。
- 在react环境中是大量应用这种方式的。react内置组件Suspense,它的作用就是当它子组件出现异步的时候可以等待,并在fallback属性显示一个等待的提示或loading。Suspense内部会捕获promise错误,一旦捕获了就会等待promise完成,在等待期间就会渲染fallback内容,直到promise完成再重新去渲染,也就是会重新调用一次这个函数组件得到新的内容。
但是框架这样设计有框架的定位,我们可以根据这种方法扩展思维,但是不建议在生产环境中使用。这种方法其实仅仅只是为了消除所谓的传染性,对于实际业务实现没有实质效益。
接下来看下实现思路吧
当一个 fetch 请求返回 promise 时,需要使用 await 来获取数据。而一旦使用了 await,当前函数就必须是 async 函数。如此循环往复地调用,接下来所有的方法都得加上 async await,这就是所谓的“异步传染性”。
async function reqRes() {
return await fetch('https://jsonplaceholder.typicode.com/posts/1');
}
async function async() {
return await reqRes();
}
async function main() {
let user = await a();
console.log(user)
}
main();
如何消除上面的现象呢?这就不得不说下面的这种方案啦。
由于fetch需要等待导致所有相关的函数都要等待,那么只能在fetch这里做一些操作了,如何让fetch不等待,就只能报错了。
在调用fetch的时候不等待了而是报错,这样所有函数都终止了,调用栈层层弹出,调用结束。但是我们最终的目的是要拿到结果的,前面虽然报错了,网络线程仍然还在继续网络请求它不会停止,直到拿到结果。
拿到结果后我们把它放在一个缓存中,接着再去恢复整个调用链的执行。再执行fetch时,结果已经缓存在cache了,取出数据就可以直接交付不用等待了从而变成了同步函数。整个过程会走两次,第一次以错误结束,第二次以成功结束,这两次都是同步的。
在这个过程中fetch的逻辑就发生了变化:fetch时要判断是否有缓存,如果有缓存则返回缓存,如果没有缓存则发送真实请求同时抛出错误,然后把请求的结果保存。抛出的错误为发送请求返回的Promise对象,目的是为了在请求完成后再去恢复调用。
代码实现
function start(func) {
const oldFetch = fetch;
const cache = {
status: 'PENDING',
value: null
}
function newFetch(...args) {
if (cache.status === 'FULFILLED') {
return cache.value;
}
if (cache.status === 'REJECTED') {
throw cache.value;
}
throw oldFetch(...args).then(function (res) {
return res.json();
}).then(function (data) {
cache.status = 'FULFILLED';
cache.value = data;
}).catch(function (error) {
cache.status = 'REJECTED';
cache.value = error;
});
}
window.fetch = newFetch;
try {
func();
} catch (error) {
if (error instanceof Promise) {
error.finally(function () {
window.fetch = newFetch;
func();
window.fetch = oldFetch;
})
}
}
window.fetch = oldFetch;
}
function reqRes() {
return fetch('https://jsonplaceholder.typicode.com/posts/1');
}
function a() {
return reqRes();
}
function main() {
console.log('main');
let res = a();
console.log(res)
}
start(main)
对代码的进一步分析
在 start
函数中:
- 它先保存了原始的
fetch
方法。 - 定义了一个
cache
对象来记录异步操作的状态和结果。 newFetch
函数根据cache
的状态来决定直接返回结果或执行原始fetch
并处理其后续的解析和状态更新。如果遇到错误则更新状态并抛出。- 然后在执行
func
(即main
函数)时,如果遇到一个Promise
类型的异常,在其finally
中先恢复newFetch
,重新执行func
,最后再恢复原始fetch
。
整体来说
- 这个代码试图通过一些自定义的逻辑来控制和管理异步
fetch
操作及其相关的状态和流程,可能是为了在特定场景下实现一些特殊的行为或控制机制。但这种方式可能会引入一些复杂性和潜在的问题,比如对fetch
的修改可能会影响到其他依赖于标准fetch
行为的部分,并且异常处理的方式也需要谨慎考虑其正确性和合理性。