当前位置: 首页 > Web前端 > JavaScript

如何使useEffect支持异步...等待?

时间:2023-03-27 00:07:23 JavaScript

本文是深入浅出ahooks源码系列文章的第六篇。本系列已整理成文档地址。我觉得还不错,点个关注支持一下,谢谢。本文已收录在我的个人博客中,欢迎关注~背景介绍使用useEffect时,如果在回调函数中使用async...await...,会报如下错误。看报错我们知道effect函数应该返回一个销毁函数(effect:指return返回的清理函数),如果将useEffect的第一个参数传给async,返回值就变成了Promise,会导致react调用销毁函数的时候报错。为什么React会这样做?useEffect作为Hooks中一个非常重要的Hook,可以让你在函数组件中执行副作用。它可以完成前面ClassComponent中生命周期的职责。它返回的函数的执行时机是这样的:第一次渲染不会清理,上一次的副作用会在下次渲染时清理。清理也在卸载阶段执行。不管是哪一种,我们都不希望这个返回值是异步的,这样就无法预测代码的执行情况,很容易发现难以定位的bug。所以React直接限制useEffect回调函数不支持async...await...useEffect如何支持async...await...连useEffect的回调函数都不能使用async...await,我就直接写了内部使用。方法一:创建一个异步函数(async...await方式),然后执行该函数。useEffect(()=>{constasyncFun=async()=>{setPass(awaitmockCheck());};asyncFun();},[]);方法二:也可以使用IIFE,如下:useEffect(()=>{(async()=>{setPass(awaitmockCheck());})();},[]);既然知道了自定义hooks的解决方法,我们就可以把它们封装成一个hook,使用起来更优雅。再来看看ahooks的useAsyncEffect,它支持所有的异步写法,包括generator函数。思路同上,入参和useEffect一样,一个回调函数(不过这个回调函数支持异步),还有一个依赖deps。里面还是useEffect,将异步逻辑放到它的回调函数中。functionuseAsyncEffect(effect:()=>AsyncGenerator|Promise,//Dependencydeps?:DependencyList,){//判断是AsyncGeneratorfunctionisAsyncGenerator(val:AsyncGenerator|Promise,):valisAsyncGenerator{//Symbol.asyncIterator:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator//Symbol.asyncIterator符号指定对象的默认异步迭代器。如果一个对象设置了这个属性,那么它就是一个异步的可迭代对象,并且可以在forawait...of循环中使用。返回isFunction(val[Symbol.asyncIterator]);}useEffect(()=>{conste=effect();//这个flag可以通过yield语句添加一些checkpoints//如果发现当前effect已经清理完毕,就会停止并继续执行。让取消=假;//执行函数asyncfunctionexecute(){//如果是Generator异步函数,全部由next()执行if(isAsyncGenerator(e)){while(true){constresult=awaite.next();//generate函数全部执行//或者当前effect已经被清理if(result.done||canceled){break;}}}else{awaite;}}执行();return()=>{//当前效果已经取消cancelled=true;};},deps);}async...await前面已经提到过,我们重点关注实现中取消的变量的实现。它的作用是中断执行。可以通过yield语句添加一些检查点。如果发现当前effect已经清理干净,就会停止,继续执行。试想一下,有一个用户进行频繁操作的场景。也许在本轮操作a执行完成之前,下一轮操作b已经开始了。此时操作a的逻辑已经失去作用,我们可以停止执行,直接进入操作b的下一轮逻辑执行。此取消用于取消当前正在执行的标识符。能不能也支持useEffect的清除机制?可以看到上面的useAsyncEffect,useEffect内部返回函数只返回如下:return()=>{//当前effect已经被清除cancelled=true;};这表明您没有通过useAsyncEffect函数在useEffect返回函数中执行明确的副作用。你可能觉得我们把effect的结果(useAsyncEffect的回调函数)放到useAsyncEffect中就够了吗?最终的实现看起来是这样的:)return()=>{cleanupPromise.then(cleanup=>cleanup&&cleanup())}},dependencies)}这种方式在本期讨论,同意上面一位大神的观点:他认为这种延迟的purge机制错了,应该是取消机制。否则,回调函数在钩子被取消后仍有机会影响外部状态。我也会贴出他的实现和例子,其实和useAsyncEffect是一样的,如下:{returnuseEffect(()=>{letcanceled=false;effect(()=>canceled);return()=>{canceled=true;}},dependencies)}Demo:useAsyncEffect(async(isCanceled)=>{constresult=awaitdoSomeAsyncStuff(stuffId);if(!isCanceled()){//TODO:仍然可以做一些效果,useEffect还没有被取消。}},[stuffId]);机制不应依赖于异步函数,否则很容易出现难以定位的错误。总结与思考由于useEffect负责在功能组件中进行副作用操作,其返回值的执行应该是可预测的,而不是异步函数,所以不支持回调函数async...await。我们可以把async...await的逻辑封装在useEffect回调函数里面。这就是ahooksuseAsyncEffect的实现思路,其适用范围更广。它支持所有异步函数,包括生成器函数。