当前位置: 首页 > 科技观察

《React进阶》说说React异步组件的前世今生

时间:2023-03-12 14:22:00 科技观察

本文转载自微信公众号《前端分享》,作者前端分享。转载本文请联系前端分享♂。APreface今天我们来谈谈React中异步组件的现状和未来。异步组件很可能是未来从数据交互到UI展示的一种平滑的技术方案。因此,既然我们要深入了解React,推进React,那么了解异步组件是很有必要的。老规矩,我们今天还带着问题开始思考?(自测掌握程度)1什么是React异步组件,它解决什么问题?2componentDidCatch如何捕捉渲染阶段的错误并进行弥补。3React.lazy是如何实现动态加载的?4为什么React.lazy应该在Supsonse里面。5Supsonse的原理是什么?二次理解:异步组件1什么是异步组件?我们先想想现在的React应用使用ajax或者fetch进行数据交互的场景。基本上是这样的。在类组件中,componentDidMount和函数组件在effect中进行数据交互,拿到数据后渲染UI视图。那么,组件的渲染是否可以等待异步数据请求完成,拿到数据后再进行渲染呢?对于上面的情况,第一感觉是难以置信。如果可以中断渲染,等待数据请求,再渲染?那就是Susponse,上面说的不可能的事情,Susponse做到了,React16.6是新的,Susponse允许组件“等待”一个异步操作,直到异步操作结束才渲染。传统模式:渲染组件->请求数据->再次渲染组件。异步模式:请求数据->渲染组件。2打开暂停模式。传统模式下的数据交互应该是这样的。functionIndex(){const[userInfo,setUserInfo]=React.useState(0)React.useEffect(()=>{/*请求数据交互*/getUserInfo().then(res=>{setUserInfo(res)})},[])return

{userInfo.name}

;
}exportdefaultfunctionHome(){return
}过程:页面初始化挂载,RequestuseEffect中的数据,通过useState改变数据,两次更新组件渲染数据。那么如果使用Susponse异步模式,可以这样写:functionFutureAsyncComponent(){constuserInfo=getUserInfo()return

{userInfo.name}

;
}/*future异步模式*/exportdefaultfunctionHome(){return
loading...
}>
}当数据还没有被loadedyet,会在Suspense中展示回退的内容,弥补请求数据中的转场效果。虽然目前版本还不能正式使用这种模式,但React未来会支持这样的代码形式。三溯源:从componentDidCatch到SuspenseSuspense是如何让上述不可能变成可能的?这从componentDidCatch开始。React在推出v16时,新增了一个生命周期函数componentDidCatch。如果一个组件定义了componentDidCatch,那么当该组件中的所有子组件在渲染过程中都抛出异常时,就会调用componentDidCatch函数。componentDidCatch使用componentDidCatch来捕获异常,它接受两个参数:1error-抛出的错误。2info-具有componentStack键的对象,其中包含有关引发错误的组件的堆栈信息。我们来模拟一个子组件渲染失败:/*正常组件可以渲染*/functionChildren(){return
hello,letuslearnReact
}/*非React组件将无法正常渲染*/functionChildren1(){return}exportdefaultclassIndexextendsReact.Component{componentDidCatch(error,info){console.log(error,info)}render(){return
}}如上,我们模拟在渲染失败的场景下,将一个非React组件Children1渲染成一个普通的React组件,这样渲染阶段就会报错,报错信息会被componentDidCatch捕获。报错信息如下:对于上述,如果子组件在渲染时出错,会导致整个组件渲染失败,无法显示。普通组件Children也会受到牵连。这时候我们就需要在componentDidCatch中进行一些补救措施。比如我们发现componentDidCatch失败了,我们可以给Children1添加一个状态控制。如果渲染失败,则终止Children1的渲染。functionErroMessage(){return
渲染出错~
}exportdefaultclassIndexextendsReact.Component{state={errorRender:false}componentDidCatch(error,info){/*补救措施*/this.setState({errorRender:true})}render(){return
{this.state.errorRender?:}
}}如果出错,通过setState重新渲染并移除失败的组件,让组件可以正常渲染,也不影响子组件的挂载。componentDidCatch一方面捕获渲染阶段发生的错误,另一方面可以在生命周期内执行sideeffects来挽回渲染异常带来的损失。componentDidCatch的原理componentDidCatch的原理应该很好理解。在内部,try{}catch(error){}可用于捕获渲染错误并处理渲染错误。try{//尝试渲染子组件}catch(error){//出现错误,componentDidCatch被调用了,}能否将componentDidCatch的思想迁移到Suspense,再回到我们的异步组件,如果异步代码是同步执行的,肯定不会正常渲染。我们还是需要先请求数据,等待数据返回,然后再使用返回的数据进行渲染。那么重点就是这句话,如何停止同步渲染等待异步数据请求呢?可以抛出异常吗?异常可以停止代码的执行,当然也可以停止渲染。Suspense是通过抛出异常暂停的渲染。Suspense需要一个createFetcher函数来封装异步操作。当试图从createFetcher返回的结果中读取数据时,有两种可能:一种是数据已经准备好了,那么直接返回结果;还有一种可能是异步操作还没有结束,数据还没有准备好。这时候createFetcher就会抛出一个“异常”。这个“异常”是正常的代码错误吗?不是的,这个异常是一个封装了请求数据的Promise对象,里面包含了真正的数据请求方法。由于Suspense可以抛出异常,所以可以通过类似componentDidCatch}的try{}catch{来获取异常。得到这个异常后怎么办?我们知道这个异常是一个Promise,那么接下来就是执行这个Promise了。成功状态后,获取数据,然后再次渲染组件。这时候渲染中已经读取到了正常的数据,那么就可以正常渲染了。接下来我们来模拟一下createFetcher和Suspense。我们来模拟一个简单的createFetcher/****@param{*}fn函数,请求数据交互,返回一个Promise*/functioncreateFetcher(fn){constfetcher={status:'pedding',result:null,p:null}returnfunction(){constgetDataPromise=fn()fetcher.p=getDataPromisegetDataPromise.then(result=>{/*成功获取数据*/fetcher.result=resultfetcher.status='resolve'})if(fetcher.status==='pedding'){/*第一次执行中断渲染,第二次*/throwfetcher}/*第二次执行*/if(fetcher.status)returnfetcher.result}}返回一个函数,在渲染阶段执行,第一个组件rendering,因为status=pedding,异常fetcher被抛给Susponse,渲染中止。Susponse会在内部componentDidCatch处理fetcher并执行getDataPromise.then。此时状态已经是resolve状态,可以正常返回数据。接下来,Susponse再次渲染该组件,此时可以正常获取数据。我们模拟一个简单的SuspenseexportclassSuspenseextendsReact.Component{state={isRender:true}componentDidCatch(e){/*异步请求,渲染fallback*/this.setState({isRender:false})const{p}=ePromise.resolve(p).then(()=>{/*数据请求后,渲染真正的组件*/this.setState({isRender:true})})}render(){const{isRender}=this.stateconst{children,fallback}=this.propsreturnisRender?children:fallback}}使用componentDidCatch捕获异步请求,如果有异步请求渲染fallback,等到异步请求执行完毕,渲染真正的组件,这样整个异步过程就是完全的。但是为了让大家理解流程,只是模拟了一个异步流程,实际流程要比这复杂的多。流程图:四种实践:从Suspense到React.lazyReact.lazy简介Suspense带来的异步组件革命尚未取得实质性成果,目前版本还未正式投入使用,但React.lazy是最佳实践对于当前版本的Suspense。我们都知道React.lazy和Suspense可以实现懒加载和按需加载,非常有利于代码切分,不会让初始化时加载大量文件,减少首屏时间。React.lazy基本上使用constLazyComponent=React.lazy(()=>import('./text'))React.lazy接受一个动态调用import()的函数。它必须返回需要解析默认导出的React组件的Promise。先来看看基本用法:constLazyComponent=React.lazy(()=>import('./test.js'))exportdefaultfunctionIndex(){returnloading...
}>}我们用Promise来模拟import()的效果,将上面的LazyComponent改成如下:很快~
}constLazyComponent=React.lazy(()=>newPromise((resolve)=>{setTimeout(()=>{resolve({default:()=>})},2000)})):React.lazy原理解读React.lazy是如何配合Susponse实现动态加载的效果的?其实lazy内部做了一个createFetcher,上面说的createFetcher获取渲染后的数据,而lazy自带的createFetcher是异步请求组件的。lazy内部模拟了一个promiseA规范场景。我们完全可以理解为React.lazy使用Promise模拟了一个请求数据的过程,但是请求的结果不是数据,而是一个动态组件。接下来看看lazy是如何处理functionlazy(ctor){return{$$typeof:REACT_LAZY_TYPE,_payload:{_status:-1,//初始化status_result:ctor,},_init:function(payload){if(payload._status===-1){/*第一次执行会到这里*/constctor=payload._result;constthenable=ctor();payload._status=Pending;payload._result=thenable;thenable.then((moduleObject)=>{constdefaultExport=moduleObject.default;resolved._status=Resolved;//1成功状态resolved._result=defaultExport;/*defaultExport是我们动态加载的组件本身*/})}if(payload._status===Resolved){//成功状态returnpayload._result;}else{//第一次Promise异常会抛到Suspensethrowpayload._result;}}}}React.lazy包裹的组件会标记REACT_LAZY_TYPE类型的元素,并调和阶段将成为LazyComponent类型的纤程。React将对LazyComponent有单独的处理逻辑。第一次渲染会先执行_init方法。此时_init方法可以理解为createFetcher。看一下lazy中init函数的执行:react-reconciler/src/ReactFiberBeginWork.jsfunctionmountLazyComponent(){constinit=lazyComponent._init;letComponent=init(payload);}如上,执行时_init方法mountLazyComponent初始化完毕,会执行lazy的第一个函数获取一个Promise,绑定Promise.then回调成功,在回调中获取我们的组件defaultExport。这里需要注意的是,上面的函数在第二个if判断的时候,因为状态不是Resolved,所以会走else,抛出异常Promise,抛出异常会终止当前渲染。Susponse在内部处理这个promise,然后再次渲染该组件,下一次渲染会直接渲染该组件。达到了动态加载的目的。Flowchart5展望:Suspense未来可期如果你现在不使用Relay,暂时无法在app中试用Suspense。因为到目前为止,在实现Suspense的库中,Relay是唯一一个我们在生产中测试过并且确信它可以工作的库。目前,Suspense不可用。如果你想使用它,你可以在生产环境中尝试使用与Suspense集成的Relay。中继指南!Suspense能解决什么问题?Suspense使数据获取库与React紧密集成。如果一个数据请求库实现了对Suspense的支持,那么在React中使用Suspense将是一件很自然的事情。Suspense可以自由展示请求中的加载效果。允许对视图加载进行更主动的控制。Suspense可以让请求数据到渲染更加流畅和灵活。我们不需要在componentDidMount中请求数据并再次触发渲染。一切都由Suspense一次性处理。悬念面临挑战?至于Suspense未来能否作为主流的异步请求数据渲染方案,笔者认为Suspense未来还是充满期待的。至于Suspense的挑战,我个人的感受在于以下几个方面:1并发模式下的Susponse可以带来更好的用户体验,react团队可以让未来的Suspense更加灵活,并且有一个更加清晰明了的createFetcher生产手册,就是在Suspense未来的并发模式中脱颖而出的关键。2Suspense能否得到广泛应用,更多取决于Suspense生态的发展。有一个稳定的数据请求库,非常适合Suspense。3开发者对Suspense价值的认可。如果未来Suspense表现力更强,更多的开发者宁愿自己封装一套数据请求方法,为优秀的Suspense买单。6.小结本文讲述了ReactSusponse的由来、实现原理、现状以及未来展望。你怎么看待React的前世今生?