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

《React进阶》函数组件我想怎么写就怎么写-流行异步组件原理

时间:2023-03-19 18:17:00 科技观察

转载本文请联系前端分享♂。前言接下来的几篇文章将围绕一些“猎奇”的场景展开,从原理上颠覆对React的理解。React的原理在每一个场景背后都有揭示。我可以认真的说,看完这篇文章,你将掌握:1.componentDidCatch的原理。2.暂停原则。3.异步组件原理。不可能我可以在我的功能组件中写任何东西。很多同学看到这句话,脑海中应该浮现的四个字就是:怎么可能?因为我们印象中的函数组件是不能直接使用异步的,必须要返回一段Jsx代码。所以今天我要打破这个规则,在我们认为是组件的函数中做一些意想不到的事情。接下来跟着我的思路往下看。我们先来看看jsx。在ReactJSX中,

代表一个DOM元素,而代表一个组件。Index本质上是一个函数组件或者类组件。
通过现象看本质,JSX是React元素的表象,JSX语法糖会被babel编译成React元素对象,那么在上面:
并不是一个真正的元素DOM元素,而是一个type属性为div的元素对象。组件索引是一个元素对象,其类型属性是类或组件本身。言归正传,那么以函数组件为参照,Index按照惯例已经变成这样了:functionIndex(){/*不能直接进行异步操作*//*返回一段jsx代码*/return
}如果不严格按这种格式写,用jsx形式挂载,会报错。看下面的例子??:/*Index不是严格的组件形式*/functionIndex(){return{name:'?'}}/*正常挂载Index组件*/exportdefaultclassAppextendsReact.Component{render(){return
helloworld,letuslearnReact!
}}通过报错信息不难找到原因,children类型错误,children应该是一个React元素对象,但是Index返回了一个普通对象。既然不可能是普通对象,那么Index中就更不可能有异步操作,比如下面这种情况:/*Example2*/unctionIndex(){returnnewPromise((resolve)=>{setTimeout(()=>{resolve({name:'?'})},1000)})也会报上面的错误,所以在一个标准的React组件规范下:必须返回jsx对象结构,不能返回普通对象。在render的执行过程中,不能发生异步操作。不可能变成可能那么如何打破局面,化不可能为可能。首先要解决的问题是报错问题。只要不报错,App就可以正常渲染。不难发现,错误的时机是在渲染过程中。那么就可以使用React提供的两个渲染错误边界生命周期componentDidCatch和getDerivedStateFromError。因为我们想在捕捉到渲染错误后做一些取巧的操作,所以这里选择componentDidCatch。接下来我们用componentDidCatch改造App。exportdefaultclassAppextendsReact.Component{state={isError:false}componentDidCatch(e){this.setState({isError:true})}render(){return
helloworld,letuslearnReact!{!this.state.isError&&}}
}}使用componentDidCatch捕获异常,可以看到渲染异常。虽然还是报错,但至少页面可以正常渲染了。我们现在所做的还不够。以第一个Index返回一个普通对象为例,我们想挂载这个组件,获取Index返回的数据,那怎么办呢?突然想到componentDidCatch可以捕获渲染异常,所以它内部应该是类似try{}catch(){}的,通过catch来捕获异常。类似如下:try{//trytorender}catch(e){//renderingfailed,executecomponentDidCatch(e)componentDidCatch(e)}那么如果在Index中抛出错误,是否也可以在componentDidCatch中接收到。所以就去做吧。我们将Index从return更改为throw,并在componentDidCatch中打印errorerror。functionIndex(){throw{name:'《React进阶实践指南》'}}返回抛出对象。componentDidCatch(e){console.log('error:',e)this.setState({isError:true})}通过componentDidCatch捕获错误。这时候e就是Indexthrow的对象。接下来使用子组件抛出的对象进行渲染。exportdefaultclassAppextendsReact.Component{state={isError:false,childThrowMes:{}}componentDidCatch(e){console.log('error:',e)this.setState({isError:true,childThrowMes:e})}render(){return
helloworld,letuslearnReact!{!this.state.isError?:
{this.state.childThrowMes.name}
}
}}捕获Index并抛出An使用对象内部的数据重新呈现的异常对象。效果:大功告成,子组件抛错,父组件componentDidCatch接受并渲染。这波操作是不是有点……不过throw抛出的所有物体都会被正常捕获吗?所以我们使用第二个Index抛出的Promise对象给componentDidCatch捕获。让我们看看它会是什么?如上图,没有正常捕获到Promise对象,而是捕获到了异常的提示信息。在异常提示中,可以找到Suspense这个词。那么throwPromise和Suspense肯定是有关系的,也就是说Suspense可以捕获Promise对象。而这个错误提示是React内部找不到上层Suspense组件的错误。至此可以得出结论:componentDidCatch是通过try{}catch(e){}捕获异常的,如果我们在渲染过程中,抛出的普通对象也会被捕获。但是Promise对象会被React底层第二次抛出异常。Suspense可以接受抛出的Promise对象,所以里面有一个componentDidCatch负责异常捕获。鬼畜版-我的组件可以异步写,由于直接抛Promise会在React底层拦截,如何在组件内部实现正常写异步操作的功能?既然React会拦截组件抛出的Promise对象,那么如果Promise对象包装器呢?所以我们修改Index的内容。functionIndex(){throw{current:newPromise((resolve)=>{setTimeout(()=>{resolve({name:'?'})},1000)})}}同上,这次会不直接抛出Promise,而是在Promise外面包裹一层对象。接下来,查看打印错误。可以看到,可以直接接收Promise。接下来,我们执行Promise对象,模拟一个异步请求,用请求后的数据渲染。所以修改App组件。exportdefaultclassAppextendsReact.Component{state={isError:false,childThrowMes:{}}componentDidCatch(e){consterrorPromise=e.currentPromise.resolve(errorPromise).then(res=>{this.setState({isError:true,childThrowMes:res})})}render(){return
helloworld,letuslearnReact!{!this.state.isError?:
{this.state.childThrowMes.name}
}
}}获取componentDidCatch的参数e中的Promise,Promise.resolve执行Promise获取数据并渲染。效果:可以看到数据正常渲染了,但是又出现了新的问题:当前的Index不是一个真正的组件,而是一个函数,所以接下来,通过异步获取数据,改造Index使其成为一个正常的组件。functionIndex({isResolve=false,data}){const[likeNumber,setLikeNumber]=useState(0)if(isResolve){return

名称:{data.name}

星号:{likeNumber}

setLikeNumber(likeNumber+1)}>点赞
}else{throw{current:newPromise((resolve)=>{setTimeout(()=>{resolve({name:'?'})},1000)})}}}Index通过isResolve判断组件是否已经添加。第一次是Resolve=false,所以抛出Promise。在父组件App中接受Promise,获取数据,改变状态isResolve,进行第二次渲染,那么第二次Index就会正常渲染。看看App是怎么写的:({data:res,isResolve:true})})}render(){const{isResolve,data}=this.statereturn
helloworld,letuslearnReact!
}}通过componentDidCatch捕获错误,然后二次渲染。结果:目标达成。这里简单介绍一下异步组件的原理。上面介绍了Susponse的一个概念,接下来我们来研究一下Susponse。飞行版——实现一个简单的Susponse什么是Susponse?Susponse英文翻译Hover。React中的暂停是什么?一般情况下,组件渲染都是一气呵成的,Susponse模式下的组件渲染可以先悬停。先解释一下为什么悬停?Susponse在React生态中的地位主要体现在以下几个方面。代码拆分(codesplitting):加载哪个组件,就加载哪个组件的代码。听起来很别扭,但确实解决了主文件过大的问题,间接优化了项目首屏的加载时间。我们知道,浏览器加载资源也是耗时的,这个时间给用户带来的影响就是白屏效果。Spinner解耦:一般情况下,页面展示需要前后端交互。数据加载过程并不希望看到无数据状态->闪烁数据的场景,而是一个spinner数据加载状态->加载完成显示页面状态。比如下面的结构:List1和List2都是通过服务端请求数据的,所以在加载数据的过程中,需要Spin效果来优雅的展示UI,所以一个Spin组件就是需要,但是Spin组件需要放在List1和List2里面,建立耦合关系。现在用Susponse耦合Spin,在业务代码中写:}>List1和List2数据加载时,用Spin加载。解耦Spin就像看电影一样。如果电影加载视频流卡住,您不希望向用户显示黑屏。相反,用海报填满屏幕,海报就是旋转。Renderdata:整个render过程是同步执行的,所以组件Render=>requestdata=>componentreRender,但是在Suspense异步组件的情况下,允许调用Render=>discover异步请求=>hoverandwaitfor异步请求完成=>再次渲染显示数据。这无疑减少了一次渲染。接下来解释一下如何悬停,理解Suspense的初衷。接下来分析一波的原理。首先,通过上面,已经说明了Suspense的原理。hover的方式很简单粗暴,直接抛出异常;异常是什么,一个Promise,这个Promise也分为两种情况:第一种是异步请求数据,这个Promise里面封装了请求方法。请求渲染数据。二是异步加载组件,配合webpack提供的require()api实现代码拆分。悬停后再次渲染在Suspense中悬停后,如果要恢复渲染,只需重新渲染一次即可。上面详细描述了悬念。接下来是练习环节,我们来尝试实现一个Suspense。首先声明这个Suspense不是React提供的Suspense。这里我们只是模拟它的大致实现细节。Suspense落地的瓶颈本质上也是请求函数的封装。Suspense主要是接受Promise并解决它。然后成功状态返回给异步组件,开发者是不知道的。对于Promise和状态传递的功能,createFetcher应该满足以下条件。constfetch=createFetcher(functiongetData(){returnnewPromise((resolve)=>{setTimeout(()=>{resolve({name:'《React进阶实践指南》',author:'alien'})},1000)})})functionText(){constdata=fetch()return
name:{data.name}author:{data.author}
}通过createFetcher封装请求函数。请求函数getData返回一个Promise,这个Promise的任务是完成数据交互。一个模拟的异步组件,内部使用createFetcher创建的request函数来请求数据。下一步是编写createFetcher函数。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==='resolve')returnfetcher.result}}这里需要注意的是fn是getData,getDataPromise是getData返回的Promise。返回一个fetch函数,在Text内部执行,第一次渲染组件时,因为status=pedding,向Susponse抛出异常fetcher,渲染中止。Susponse会在内部componentDidCatch处理fetcher并执行getDataPromise.then。此时状态已经是resolve状态,可以正常返回数据。接下来,Susponse再次渲染该组件,此时可以正常获取数据。现在我们有了createFetcher函数,下一步就是模拟上游组件Susponse。classMySusponse扩展了React.Component{state={isResolve:true}componentDidCatch(fetcher){constp=fetcher.pthis.setState({isResolve:false})Promise.resolve(p).then(()=>{this.setState({isResolve:true})})}render(){const{fallback,children}=this.propsconst{isResolve}=this.statereturnisResolve?children:fallback}}我们写的Susponse取名为MySusponse。MySusponse的内部componentDidCatch通过Promise.resolve捕获Promise的成功状态。成功时,抑制回退UI效果。大功告成,接下来就是体验环节了。让我们试试MySusponse效果。exportdefaultfunctionIndex(){return
hello,worldloading...
}>
}效果:虽然实现了效果,但是和真正的悬念一样,还很遥远。首先暴露的问题是可变数据的问题。上面写的MySusponse数据只加载一次,但通常情况下,数据交互是可变的,数据也是可变的。衍生版——实现一个错误异常处理组件言归正传,我们不会在函数组件中做以上操作,也不会自己写createFetcher和Susponse。但是,有一个场景是比较实用的,那就是渲染错误和UI降级的处理。这种情况通常发生在服务端数据不确定的场景。比如我们通过服务端data数据渲染,如下场景:
{data.name}
如果data是一个对象,则正常渲染,但如果data为null,则报错会被举报。如果不加渲染错误边界,那么一个小问题就会导致整个页面无法渲染。所以对于上面的情况,如果每个页面组件都加上componentDidCatch来捕获错误,降级UI,那么代码就太冗余了,难以复用,降级后的UI也无法和业务组件解耦。所以可以统一写一个RenderControlError组件。目的是为了在组件异常时统一显示降级的UI。也保证了整个前端应用不会崩溃,也大大提高了服务端数据格式的容错率。接下来,我们来看一下具体的实现。classRenderControlErrorextendsReact.Component{state={isError:false}componentDidCatch(){this.setState({isError:true})}render(){return!this.state.isError?this.props.children:发生错误
}}如果孩子出错,则降级UI。使用总结本文通过一些脑洞大开的精彩操作,让大家了解Susponse和componentDidCatch的原理。相信在不久的将来,随着React18的发布,Susponse将应运而生,未来可期。