「React进阶」我在函数组件中可以随便写 —— 最通俗异步组件原理

发表于 3年以前  | 总阅读数:321 次

前言

接下来的几篇文章将围绕一些‘猎奇’场景,从原理颠覆对 React 的认识。每一个场景下背后都透漏出 React 原理,

我可以认真的说,看完这篇文章,你将掌握:

  • 1 componentDidCatch 原理
  • 2 susponse 原理
  • 3 异步组件原理。

不可能的事

我的函数组件中里可以随便写,很多同学看到这句话的时候,脑海里应该浮现的四个字是:怎么可能?因为我们印象中的函数组件,是不能直接使用异步的,而且必须返回一段 Jsx 代码。

1.jpg

那么今天我将打破这个规定,在我们认为是组件的函数里做一些意想不到的事情。接下来跟着我的思路往下看吧。

首先先来看一下 jsx ,在 React JSX<div /> 代表 DOM 元素,而 <Index> 代表组件, Index 本质是函数组件类组件

<div />
<Index />

透过现象看本质,JSX 为 React element 的表象,JSX 语法糖会被 babel 编译成 React element 对象 ,那么上述中:

  • <div /> 不是真正的 DOM 元素,是 type 属性为 div 的 element 对象。
  • 组件 Index 是 type 属性为类或者组件本身的 element 对象。

言归正传,那么以函数组件为参考,Index 已经约定俗成为这个样子:

function Index(){
    /* 不能直接的进行异步操作 */
    /* return 一段 jsx 代码 */
    return <div></div>
}

如果不严格按照这个格式写,通过 jsx <Index />形式挂载,就会报错。看如下的例子:

/* Index  不是严格的组件形式 */
function Index(){
    return {
       name:'《React进阶实践指南》'
    }
}
/* 正常挂载 Index 组件 */
export default class App extends React.Component{
    render(){
        return <div>
            hello world , let us learn React! 
            <Index />
        </div>
    }
}

2.jpg

我们通过报错信息,不难发现原因,children 类型错误,children 应该是一个 React element 对象,但是 Index 返回的却是一个普通的对象。

既然不能是普通的对象,那么如果 Index 里面更不可能有异步操作了,比如如下这种情况:

 /* 例子2 */
function Index(){
    return new Promise((resolve)=>{
        setTimeout(()=>{
            resolve({ name:'《React进阶实践指南》'  })
        },1000)
    })
}

同样也会报上面的错误,所以在一个标准的 React 组件规范下:

  • 必须返回 jsx 对象结构,不能返回普通对象。
  • render 执行过程中,不能出现异步操作。

不可能的事变为可能

那么如何破局,将不可能的事情变得可能。首先要解决的问题是 报错问题 ,只要不报错,App 就能正常渲染。不难发现产生的错误时机都是在 render 过程中。那么就可以用 React 提供的两个渲染错误边界的生命周期 componentDidCatchgetDerivedStateFromError

因为我们要在捕获渲染错误之后做一些骚操作,所以这里选 componentDidCatch。接下来我们用 componentDidCatch 改造一下 App。

export default class App extends React.Component{
    state = {
       isError:false
    }
    componentDidCatch(e){
         this.setState({ isError:true })
    }
    render(){
        return <div>
            hello world , let us learn React!
            {!this.state.isError &&  <Index />}
        </div>
    }
}
  • componentDidCatch 捕获异常,渲染异常

3.jpg

可以看到,虽然还是报错,但是至少页面可以正常渲染了。现在做的事情还不够,以第一 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 的对象。接下来用子组件抛出的对象渲染。

5.jpeg

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 <div>
            hello world , let us learn React!
            {!this.state.isError ?  <Index /> : <div> {this.state.childThrowMes.name} </div>}
        </div>
    }
}
  • 捕获到 Index 抛出的异常对象,用对象里面的数据重新渲染。

效果:

6.jpg

大功告成,子组件 throw 错误,父组件 componentDidCatch 接受并渲染,这波操作是不是有点...

4.gif

但是 throw 的所有对象,都会被正常捕获吗?于是我们把第二个 Index 抛出的 Promise 对象用 componentDidCatch 捕获。看看会是什么吧?

7.jpg

如上所示,Promise 对象没有被正常捕获,捕获的是异常的提示信息。在异常提示中,可以找到 Suspense 的字样。那么 throw PromiseSuspense 之间肯定存在着关联,换句话说就是 Suspense 能够捕获到 Promise 对象。而这个错误警告,就是 React 内部发出找不到上层的 Suspense 组件的错误。

到此为止,可以总结出:

  • componentDidCatch 通过 try{}catch(e){} 捕获到异常,如果我们在渲染过程中,throw 出来的普通对象,也会被捕获到。但是 Promise 对象,会被 React 底层第 2 次抛出异常。
  • Suspense 内部可以接受 throw 出来的 Promise 对象,那么内部有一个 componentDidCatch 专门负责异常捕获。

鬼畜版——我的组件可以写异步

即然直接 throw Promise 会在 React 底层被拦截,那么如何在组件内部实现正常编写异步操作的功能呢?既然 React 会拦截组件抛出的 Promise 对象,那么如果把 Promise 对象包装一层呢? 于是我们把 Index 内容做修改。

function Index(){
    throw {
        current:new Promise((resolve)=>{
            setTimeout(()=>{
                resolve({ name:'《React进阶实践指南》'  })
            },1000)
        })
    }
}
  • 如上,这回不在直接抛出 Promise,而是在 Promise 的外面在包裹一层对象。接下来打印错误看一下。

8.jpg

可以看到,能够直接接收到 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 <div>
            hello world , let us learn React!
            {!this.state.isError ?  <Index /> : <div> {this.state.childThrowMes.name} </div>}
        </div>
    }
}
  • 在 componentDidCatch 的参数 e 中获取 Promise ,Promise.resolve 执行 Promise 获取数据并渲染。

效果:

9.jpg

可以看到数据正常渲染了,但是面临一个新的问题:目前的 Index 不是一个真正意义上的组件,而是一个函数,所以接下来,改造 Index 使其变成正常的组件,通过获取异步的数据。

function Index({ isResolve = false , data }){
    const [ likeNumber , setLikeNumber ] = useState(0)
    if(isResolve){
        return <div>
            <p> 名称:{data.name} </p>
            <p> star:{likeNumber} </p>
            <button onClick={()=> setLikeNumber(likeNumber+1)} >点赞</button>
        </div>
    }else{
        throw {
            current:new Promise((resolve)=>{
                setTimeout(()=>{
                    resolve({ name:'《React进阶实践指南》'  })
                },1000)
            })
        }
    }
}
  • Index 中通过 isResolve 判断组件是否加在完成,第一次的时候 isResolve = false 所以 throw Promise
  • 父组件 App 中接受 Promise ,得到数据,改变状态 isResolve ,二次渲染,那么第二次 Index 就会正常渲染了。看一下 App 如何写:
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 <div>
            hello world , let us learn React!
            <Index data={data} isResolve={isResolve} />
        </div>
    }
}
  • 通过 componentDidCatch 捕获错误,然后进行第二次渲染。

效果:

10.gif

达到了目的。这里就简单介绍了一下异步组件的原理。上述引入了一个 Susponse 的概念,接下来研究一下 Susponse。

飞翔版——实现一个简单 Suspense

Susponse 是什么?Susponse 英文翻译 悬停。在 React 中 Susponse 是什么呢?那么正常情况下组件染是一气呵成的,在 Susponse 模式下的组件渲染就变成了可以先悬停下来。

首先解释为什么悬停?

Susponse 在 React 生态中的位置,重点体现在以下方面。

  • code splitting(代码分割) :哪个组件加载,就加载哪个组件的代码,听上去挺拗口,可确实打实的解决了主文件体积过大的问题,间接优化了项目的首屏加载时间,我们知道过浏览器加载资源也是耗时的,这些时间给用户造成的影响就是白屏效果。
  • spinner 解耦:正常情况下,页面展示是需要前后端交互的,数据加载过程不期望看到 无数据状态->闪现数据的场景,更期望的是一种spinner数据加载状态->加载完成展示页面状态。比如如下结构:
<List1 />
<List2 />

List1List2 都使用服务端请求数据,那么在加载数据过程中,需要 Spin 效果去优雅的展示 UI,所以需要一个 Spin 组件,但是 Spin 组件需要放入 List1List2 的内部,就造成耦合关系。现在通过 Susponse 来接耦 Spin,在业务代码中这么写道:

<Suspense fallback={ <Spin /> }  >
    <List1 />
    <List2 />
</Suspense>

List1List2 数据加载过程中,用 Spin 来 loading 。把 Spin 解耦出来,就像看电影,如果电影加载视频流卡住,不期望给用户展示黑屏幕,取而代之的是用海报来填充屏幕,而海报就是这个 Spin 。

  • render data:整个 render 过程都是同步执行一气呵成的,那样就会 组件 Render => 请求数据 => 组件 reRender ,但是在 Suspense 异步组件情况下允许调用 Render => 发现异步请求 => 悬停,等待异步请求完毕 => 再次渲染展示数据。这样无疑减少了一次渲染。

接下来解释如何悬停

上面理解了 Suspense 初衷,接下来分析一波原理,首先通过上文中,已经交代了 Suspense 原理,如何悬停,很简单粗暴,直接抛出一个异常;

异常是什么,一个 Promise ,这个 Promise 也分为二种情况:

  • 第一种就是异步请求数据,这个 Promise 内部封装了请求方法。请求数据用于渲染。
  • 第二种就是异步加载组件,配合 webpack 提供的 require() api,实现代码分割。

悬停后再次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 <div>
        name: {data.name}
        author:{data.author}
    </div>
}
  • 通过 createFetcher 封装请求函数。请求函数 getData 返回一个 Promise ,这个 Promise 的使命就是完成数据交互。
  • 一个模拟的异步组件,内部使用 createFetcher 创建的请求函数,请求数据。

接下来就是 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
    }
}
  • 这里要注意的是 fn 就是 getData, getDataPromise 就是 getData返回的 Promise。
  • 返回一个函数 fetch ,在 Text 内部执行,第一次组件渲染,由于 status = pedding 所以抛出异常 fetcher 给 Susponse,渲染中止。
  • Susponse 会在内部 componentDidCatch 处理这个fetcher,执行 getDataPromise.then, 这个时候status已经是resolve状态,数据也能正常返回了。
  • 接下来Susponse再次渲染组件,此时就能正常的获取数据了。

既然有了 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 内部 componentDidCatch 通过 Promise.resolve 捕获 Promise 成功的状态。成功后,取缔 fallback UI 效果。

大功告成,接下来就是体验环节了。我们尝试一下 MySusponse 效果。

export default function Index(){
    return <div>
        hello,world
       <MySusponse fallback={<div>loading...</div>} >
            <Text />
       </MySusponse>
    </div>
}

效果:

11.gif

虽然实现了效果,但是和真正的 Susponse 还差的很远,首先暴露出的问题就是数据可变的问题。上述编写的 MySusponse 数据只加载一次,但是通常情况下,数据交互是存在变数的,数据也是可变的。

衍生版——实现一个错误异常处理组件

言归正传,我们不会在函数组件中做如上的骚操作,也不会自己去编写 createFetcherSusponse。但是有一个场景还是蛮实用的,那就是对渲染错误的处理,以及 UI 的降级,这种情况通常出现在服务端数据的不确定的场景下,比如我们通过服务端的数据 data 进行渲染,像如下场景:

<div>{ data.name }</div>

如果 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 :
             <div style={styles.errorBox} >
                 <img url={require('../../assets/img/error.png')}
                     style={styles.erroImage}
                 />
                 <span style={styles.errorText}  >出现错误</span>
             </div>
    }
}
  • 如果 children 出错,那么降级 UI。

使用

<RenderControlError>
    <Index />
</RenderControlError>

总结

本文通过一些脑洞大开,奇葩的操作,让大家明白了 Susponse ,componentDidCatch 等原理。我相信不久之后,随着 React 18 发布,Susponse 将崭露头角,未来可期。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/K3K0zBKfRTXFCaUqVUTI3g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录