转载本文请联系前端分享♂。前言接下来的几篇文章将围绕一些“猎奇”的场景展开,从原理上颠覆对React的理解。React的原理在每一个场景背后都有揭示。我可以认真的说,看完这篇文章,你将掌握:1.componentDidCatch的原理。2.暂停原则。3.异步组件原理。不可能我可以在我的功能组件中写任何东西。很多同学看到这句话,脑海中应该浮现的四个字就是:怎么可能?因为我们印象中的函数组件是不能直接使用异步的,必须要返回一段Jsx代码。所以今天我要打破这个规则,在我们认为是组件的函数中做一些意想不到的事情。接下来跟着我的思路往下看。我们先来看看jsx。在ReactJSX中,
代表一个DOM元素,而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(){returnhelloworld,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(){returnhelloworld,letuslearnReact!{!this.state.isError? :
}}捕获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{this.state.childThrowMes.name}
}helloworld,letuslearnReact!{!this.state.isError? :
}}获取componentDidCatch的参数e中的Promise,Promise.resolve执行Promise获取数据并渲染。效果:可以看到数据正常渲染了,但是又出现了新的问题:当前的Index不是一个真正的组件,而是一个函数,所以接下来,通过异步获取数据,改造Index使其成为一个正常的组件。functionIndex({isResolve=false,data}){const[likeNumber,setLikeNumber]=useState(0)if(isResolve){return{this.state.childThrowMes.name}
}名称:{data.name}
星号:{likeNumber}
helloworld,letuslearnReact!
}}通过componentDidCatch捕获错误,然后二次渲染。结果:目标达成。这里简单介绍一下异步组件的原理。上面介绍了Susponse的一个概念,接下来我们来研究一下Susponse。飞行版——实现一个简单的Susponse什么是Susponse?Susponse英文翻译Hover。React中的暂停是什么?一般情况下,组件渲染都是一气呵成的,Susponse模式下的组件渲染可以先悬停。先解释一下为什么悬停?Susponse在React生态中的地位主要体现在以下几个方面。代码拆分(codesplitting):加载哪个组件,就加载哪个组件的代码。听起来很别扭,但确实解决了主文件过大的问题,间接优化了项目首屏的加载时间。我们知道,浏览器加载资源也是耗时的,这个时间给用户带来的影响就是白屏效果。Spinner解耦:一般情况下,页面展示需要前后端交互。数据加载过程并不希望看到无数据状态->闪烁数据的场景,而是一个spinner数据加载状态->加载完成显示页面状态。比如下面的结构: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(){returnhello,worldloading...
}>