本文转载自微信公众号「前端Sharing」,作者前端Sharing。转载本文请联系前端Sharing公众号。
上林网站制作公司哪家好,找创新互联建站!从网页设计、网站建设、微信开发、APP开发、响应式网站开发等网站项目制作,到程序开发,运营维护。创新互联建站从2013年成立到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联建站。
接下来的几篇文章将围绕一些‘猎奇’场景,从原理颠覆对 React 的认识。每一个场景下背后都透漏出 React 原理,
我可以认真的说,看完这篇文章,你将掌握:
1 componentDidCatch 原理
2 susponse 原理
3 异步组件原理。
我的函数组件中里可以随便写,很多同学看到这句话的时候,脑海里应该浮现的四个字是:怎么可能?因为我们印象中的函数组件,是不能直接使用异步的,而且必须返回一段 Jsx 代码。
那么今天我将打破这个规定,在我们认为是组件的函数里做一些意想不到的事情。接下来跟着我的思路往下看吧。
首先先来看一下 jsx ,在 React JSX 中
代表 DOM 元素,而透过现象看本质,JSX 为 React element 的表象,JSX 语法糖会被 babel 编译成 React element 对象 ,那么上述中:
言归正传,那么以函数组件为参考,Index 已经约定俗成为这个样子:
- function Index(){
- /* 不能直接的进行异步操作 */
- /* return 一段 jsx 代码 */
- return
- }
如果不严格按照这个格式写,通过 jsx 形式挂载,就会报错。看如下的例子??:
- /* Index 不是严格的组件形式 */
- function Index(){
- return {
- name:'《React进阶实践指南》'
- }
- }
- /* 正常挂载 Index 组件 */
- export default class App extends React.Component{
- render(){
- return
- hello world , let us learn React!
- }
- }
我们通过报错信息,不难发现原因,children 类型错误,children 应该是一个 React element 对象,但是 Index 返回的却是一个普通的对象。
既然不能是普通的对象,那么如果 Index 里面更不可能有异步操作了,比如如下这种情况:
- /* 例子2 */
- unction Index(){
- return new Promise((resolve)=>{
- setTimeout(()=>{
- resolve({ name:'《React进阶实践指南》' })
- },1000)
- })
同样也会报上面的错误,所以在一个标准的 React 组件规范下:
那么如何破局,将不可能的事情变得可能。首先要解决的问题是 报错问题 ,只要不报错,App 就能正常渲染。不难发现产生的错误时机都是在 render 过程中。那么就可以用 React 提供的两个渲染错误边界的生命周期 componentDidCatch 和 getDerivedStateFromError。
因为我们要在捕获渲染错误之后做一些骚操作,所以这里选 componentDidCatch。接下来我们用 componentDidCatch 改造一下 App。
- export default class App extends React.Component{
- state = {
- isError:false
- }
- componentDidCatch(e){
- this.setState({ isError:true })
- }
- render(){
- return
- hello world , let us learn React!
- {!this.state.isError &&
} - }
- }
用 componentDidCatch 捕获异常,渲染异常
可以看到,虽然还是报错,但是至少页面可以正常渲染了。现在做的事情还不够,以第一 Index 返回一个正常对象为例,我们想要挂载这个组件,还要获取 Index 返回的数据,那么怎么办呢?
突然想到 componentDidCatch 能够捕获到渲染异常,那么它的内部就应该像 try{}catch(){} 一样,通过 catch 捕获异常。类似下面这种:
- try{
- // 尝试渲染
- }catch(e){
- // 渲染失败,执行componentDidCatch(e)
- componentDidCatch(e)
- }
那么如果在 Index 中抛出的错误,是不是也可以在 componentDidCatch 接收到。于是说干就干。我们把 Index 改变由 return 变成 throw ,然后在 componentDidCatch 打印错误 error。
- function Index(){
- throw {
- name:'《React进阶实践指南》'
- }
- }
将 throw 对象返回。
- componentDidCatch(e){
- console.log('error:',e)
- this.setState({ isError:true })
- }
通过 componentDidCatch 捕获错误。此时的 e 就是 Index throw 的对象。接下来用子组件抛出的对象渲染。
- export default class App extends React.Component{
- state = {
- isError:false,
- childThrowMes:{}
- }
- componentDidCatch(e){
- console.log('error:',e)
- this.setState({ isError:true , childThrowMes:e })
- }
- render(){
- return
- hello world , let us learn React!
- {!this.state.isError ?
: {this.state.childThrowMes.name}}- }
- }
捕获到 Index 抛出的异常对象,用对象里面的数据重新渲染。
效果:
大功告成,子组件 throw 错误,父组件 componentDidCatch 接受并渲染,这波操作是不是有点...
但是 throw 的所有对象,都会被正常捕获吗?于是我们把第二个 Index 抛出的 Promise 对象用 componentDidCatch 捕获。看看会是什么吧?
如上所示,Promise 对象没有被正常捕获,捕获的是异常的提示信息。在异常提示中,可以找到 Suspense 的字样。那么 throw Promise 和 Suspense 之间肯定存在着关联,换句话说就是 Suspense 能够捕获到 Promise 对象。而这个错误警告,就是 React 内部发出找不到上层的 Suspense 组件的错误。
到此为止,可以总结出:
即然直接 throw Promise 会在 React 底层被拦截,那么如何在组件内部实现正常编写异步操作的功能呢?既然 React 会拦截组件抛出的 Promise 对象,那么如果把 Promise 对象包装一层呢? 于是我们把 Index 内容做修改。
- function Index(){
- throw {
- current:new Promise((resolve)=>{
- setTimeout(()=>{
- resolve({ name:'《React进阶实践指南》' })
- },1000)
- })
- }
- }
如上,这回不在直接抛出 Promise,而是在 Promise 的外面在包裹一层对象。接下来打印错误看一下。
可以看到,能够直接接收到 Promise 啦,接下来我们执行 Promise 对象,模拟异步请求,用请求之后的数据进行渲染。于是修改 App 组件。
- export default class App extends React.Component{
- state = {
- isError:false,
- childThrowMes:{}
- }
- componentDidCatch(e){
- const errorPromise = e.current
- Promise.resolve(errorPromise).then(res=>{
- this.setState({ isError:true , childThrowMes:res })
- })
- }
- render(){
- return
- hello world , let us learn React!
- {!this.state.isError ?
: {this.state.childThrowMes.name}}- }
- }
在 componentDidCatch 的参数 e 中获取 Promise ,Promise.resolve 执行 Promise 获取数据并渲染。
效果:
可以看到数据正常渲染了,但是面临一个新的问题:目前的 Index 不是一个真正意义上的组件,而是一个函数,所以接下来,改造 Index 使其变成正常的组件,通过获取异步的数据。
- function Index({ isResolve = false , data }){
- const [ likeNumber , setLikeNumber ] = useState(0)
- if(isResolve){
- return
名称:{data.name}
star:{likeNumber}
- }else{
- throw {
- current:new Promise((resolve)=>{
- setTimeout(()=>{
- resolve({ name:'《React进阶实践指南》' })
- },1000)
- })
- }
- }
- }
- export default class App extends React.Component{
- state = {
- isResolve:false,
- data:{}
- }
- componentDidCatch(e){
- const errorPromise = e.current
- Promise.resolve(errorPromise).then(res=>{
- this.setState({ data:res,isResolve:true })
- })
- }
- render(){
- const { isResolve ,data } = this.state
- return
- hello world , let us learn React!
- }
- }
通过 componentDidCatch 捕获错误,然后进行第二次渲染。
效果:
达到了目的。这里就简单介绍了一下异步组件的原理。上述引入了一个 Susponse 的概念,接下来研究一下 Susponse。
Susponse 是什么?Susponse 英文翻译 悬停。在 React 中 Susponse 是什么呢?那么正常情况下组件染是一气呵成的,在 Susponse 模式下的组件渲染就变成了可以先悬停下来。
首先解释为什么悬停?
Susponse 在 React 生态中的位置,重点体现在以下方面。
List1 和 List2 都使用服务端请求数据,那么在加载数据过程中,需要 Spin 效果去优雅的展示 UI,所以需要一个 Spin 组件,但是 Spin 组件需要放入 List1 和 List2 的内部,就造成耦合关系。现在通过 Susponse 来接耦 Spin,在业务代码中这么写道:
} >
当 List1 和 List2 数据加载过程中,用 Spin 来 loading 。把 Spin 解耦出来,就像看电影,如果电影加载视频流卡住,不期望给用户展示黑屏幕,取而代之的是用海报来填充屏幕,而海报就是这个 Spin 。
接下来解释如何悬停
上面理解了 Suspense 初衷,接下来分析一波原理,首先通过上文中,已经交代了 Suspense 原理,如何悬停,很简单粗暴,直接抛出一个异常;
异常是什么,一个 Promise ,这个 Promise 也分为二种情况:
悬停后再次render
在 Suspense 悬停后,如果想要恢复渲染,那么 rerender 一下就可以了。
如上详细介绍了 Suspense 。接下来到了实践环节,我们去尝试实现一个 Suspense ,首先声明一下这个 Suspense 并不是 React 提供的 Suspense ,这里只是模拟了一下它的大致实现细节。
本质上 Suspense 落地瓶颈也是对请求函数的的封装,Suspense 主要接受 Promise,并 resolve 它,那么对于成功的状态回传到异步组件中,对于开发者来说是未知的,对于 Promise 和状态传递的函数 createFetcher,应该满足如下的条件。
- const fetch = createFetcher(function getData(){
- return new Promise((resolve)=>{
- setTimeout(()=>{
- resolve({
- name:'《React进阶实践指南》',
- author:'alien'
- })
- },1000)
- })
- })
- function Text(){
- const data = fetch()
- return
- name: {data.name}
- author:{data.author}
- }
接下来就是 createFetcher 函数的编写。
- function createFetcher(fn){
- const fetcher = {
- status:'pedding',
- result:null,
- p:null
- }
- return function (){
- const getDataPromise = fn()
- fetcher.p = getDataPromise
- getDataPromise.then(result=>{ /* 成功获取数据 */
- fetcher.result = result
- fetcher.status = 'resolve'
- })
- if(fetcher.status === 'pedding'){ /* 第一次执行中断渲染,第二次 */
- throw fetcher
- }
- /* 第二次执行 */
- if(fetcher.status==='resolve')
- return fetcher.result
- }
- }
既然有了 createFetcher 函数,接下来就要模拟上游组件 Susponse 。
- class MySusponse extends React.Component{
- state={
- isResolve:true
- }
- componentDidCatch(fetcher){
- const p = fetcher.p
- this.setState({ isResolve:false })
- Promise.resolve(p).then(()=>{
- this.setState({ isResolve:true })
- })
- }
- render(){
- const { fallback, children } = this.props
- const { isResolve } = this.state
- return isResolve ? children : fallback
- }
- }
我们编写的 Susponse 起名字叫 MySusponse 。
大功告成,接下来就是体验环节了。我们尝试一下 MySusponse 效果。
- export default function Index(){
- return
} >- hello,world
loading...
效果:
虽然实现了效果,但是和真正的 Susponse 还差的很远,首先暴露出的问题就是数据可变的问题。上述编写的 MySusponse 数据只加载一次,但是通常情况下,数据交互是存在变数的,数据也是可变的。
言归正传,我们不会在函数组件中做如上的骚操作,也不会自己去编写 createFetcher 和 Susponse。但是有一个场景还是蛮实用的,那就是对渲染错误的处理,以及 UI 的降级,这种情况通常出现在服务端数据的不确定的场景下,比如我们通过服务端的数据 data 进行渲染,像如下场景:
{ data.name }
如果 data 是一个对象,那么会正常渲染,但是如果 data 是 null,那么就会报错,如果不加渲染错误边界,那么一个小问题会导致整个页面都渲染不出来。
那么对于如上情况,如果每一个页面组件,都加上 componentDidCatch 这样捕获错误,降级 UI 的方式,那么代码过于冗余,难以复用,无法把降级的 UI 从业务组件中解耦出来。
所以可以统一写一个 RenderControlError 组件,目的就是在组件的出现异常的情况,统一展示降级的 UI ,也确保了整个前端应用不会奔溃,同样也让服务端的数据格式容错率大大提升。接下来看一下具体实现。
- class RenderControlError extends React.Component{
- state={
- isError:false
- }
- componentDidCatch(){
- this.setState({ isError:true })
- }
- render(){
- return !this.state.isError ?
- this.props.children :
- style={styles.erroImage}
- />
- 出现错误
- }
- }
如果 children 出错,那么降级 UI。
使用
本文通过一些脑洞大开,奇葩的操作,让大家明白了 Susponse ,componentDidCatch 等原理。我相信不久之后,随着 React 18 发布,Susponse 将崭露头角,未来可期。
分享标题:「React进阶」我在函数组件中可以随便写-通俗异步组件原理
标题网址:http://www.shufengxianlan.com/qtweb/news33/203833.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联