接下来的几篇文章将围绕一些‘猎奇’场景,从原理颠覆对 React 的认识。每一个场景下背后都透漏出 React 原理,
我可以认真的说,看完这篇文章,你将掌握:
我的函数组件中里可以随便写
,很多同学看到这句话的时候,脑海里应该浮现的四个字是:怎么可能?因为我们印象中的函数组件,是不能直接使用异步的,而且必须返回一段 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 组件规范下:
那么如何破局,将不可能的事情变得可能。首先要解决的问题是 报错问题 ,只要不报错,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 <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进阶实践指南》'
}
}
componentDidCatch(e){
console.log('error:',e)
this.setState({ isError:true })
}
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>
}
}
效果:
6.jpg
大功告成,子组件 throw 错误,父组件 componentDidCatch 接受并渲染,这波操作是不是有点...
4.gif
但是 throw
的所有对象,都会被正常捕获吗?于是我们把第二个 Index 抛出的 Promise
对象用 componentDidCatch 捕获。看看会是什么吧?
7.jpg
如上所示,Promise
对象没有被正常捕获,捕获的是异常的提示信息。在异常提示中,可以找到 Suspense 的字样。那么 throw Promise
和 Suspense
之间肯定存在着关联,换句话说就是 Suspense
能够捕获到 Promise
对象。而这个错误警告,就是 React 内部发出找不到上层的 Suspense 组件的错误。
到此为止,可以总结出:
try{}catch(e){}
捕获到异常,如果我们在渲染过程中,throw 出来的普通对象,也会被捕获到。但是 Promise
对象,会被 React 底层第 2 次抛出异常。componentDidCatch
专门负责异常捕获。即然直接 throw Promise 会在 React 底层被拦截,那么如何在组件内部实现正常编写异步操作的功能呢?既然 React 会拦截组件抛出的 Promise 对象,那么如果把 Promise 对象包装一层呢? 于是我们把 Index 内容做修改。
function Index(){
throw {
current:new Promise((resolve)=>{
setTimeout(()=>{
resolve({ name:'《React进阶实践指南》' })
},1000)
})
}
}
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>
}
}
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)
})
}
}
}
isResolve
判断组件是否加在完成,第一次的时候 isResolve = false
所以 throw Promise
。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>
}
}
效果:
10.gif
达到了目的。这里就简单介绍了一下异步组件的原理。上述引入了一个 Susponse 的概念,接下来研究一下 Susponse。
Susponse 是什么?Susponse 英文翻译 悬停。在 React 中 Susponse 是什么呢?那么正常情况下组件染是一气呵成的,在 Susponse 模式下的组件渲染就变成了可以先悬停下来。
首先解释为什么悬停?
Susponse 在 React 生态中的位置,重点体现在以下方面。
<List1 />
<List2 />
List1
和 List2
都使用服务端请求数据,那么在加载数据过程中,需要 Spin 效果去优雅的展示 UI,所以需要一个 Spin 组件,但是 Spin 组件需要放入 List1
和 List2
的内部,就造成耦合关系。现在通过 Susponse 来接耦 Spin,在业务代码中这么写道:
<Suspense fallback={ <Spin /> } >
<List1 />
<List2 />
</Suspense>
当 List1
和 List2
数据加载过程中,用 Spin 来 loading 。把 Spin 解耦出来,就像看电影,如果电影加载视频流卡住,不期望给用户展示黑屏幕,取而代之的是用海报来填充屏幕,而海报就是这个 Spin 。
接下来解释如何悬停
上面理解了 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
函数的编写。
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,渲染中止。既然有了 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 。
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 数据只加载一次,但是通常情况下,数据交互是存在变数的,数据也是可变的。
言归正传,我们不会在函数组件中做如上的骚操作,也不会自己去编写 createFetcher
和 Susponse
。但是有一个场景还是蛮实用的,那就是对渲染错误的处理,以及 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>
}
}
使用
<RenderControlError>
<Index />
</RenderControlError>
本文通过一些脑洞大开,奇葩的操作,让大家明白了 Susponse ,componentDidCatch 等原理。我相信不久之后,随着 React 18 发布,Susponse 将崭露头角,未来可期。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/K3K0zBKfRTXFCaUqVUTI3g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。