新聞中心
[[428609]]
本文轉(zhuǎn)載自微信公眾號(hào)「前端Sharing」,作者前端Sharing。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端Sharing公眾號(hào)。

上林網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、APP開(kāi)發(fā)、響應(yīng)式網(wǎng)站開(kāi)發(fā)等網(wǎng)站項(xiàng)目制作,到程序開(kāi)發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)建站從2013年成立到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)建站。
前言
接下來(lái)的幾篇文章將圍繞一些‘獵奇’場(chǎng)景,從原理顛覆對(duì) React 的認(rèn)識(shí)。每一個(gè)場(chǎng)景下背后都透漏出 React 原理,
我可以認(rèn)真的說(shuō),看完這篇文章,你將掌握:
1 componentDidCatch 原理
2 susponse 原理
3 異步組件原理。
不可能的事
我的函數(shù)組件中里可以隨便寫(xiě),很多同學(xué)看到這句話的時(shí)候,腦海里應(yīng)該浮現(xiàn)的四個(gè)字是:怎么可能?因?yàn)槲覀冇∠笾械暮瘮?shù)組件,是不能直接使用異步的,而且必須返回一段 Jsx 代碼。
那么今天我將打破這個(gè)規(guī)定,在我們認(rèn)為是組件的函數(shù)里做一些意想不到的事情。接下來(lái)跟著我的思路往下看吧。
首先先來(lái)看一下 jsx ,在 React JSX 中
代表 DOM 元素,而透過(guò)現(xiàn)象看本質(zhì),JSX 為 React element 的表象,JSX 語(yǔ)法糖會(huì)被 babel 編譯成 React element 對(duì)象 ,那么上述中:
- 不是真正的 DOM 元素,是 type 屬性為 div 的 element 對(duì)象。
- 組件 Index 是 type 屬性為類或者組件本身的 element 對(duì)象。
言歸正傳,那么以函數(shù)組件為參考,Index 已經(jīng)約定俗成為這個(gè)樣子:
- function Index(){
- /* 不能直接的進(jìn)行異步操作 */
- /* return 一段 jsx 代碼 */
- return
- }
如果不嚴(yán)格按照這個(gè)格式寫(xiě),通過(guò) jsx 形式掛載,就會(huì)報(bào)錯(cuò)。看如下的例子??:
- /* Index 不是嚴(yán)格的組件形式 */
- function Index(){
- return {
- name:'《React進(jìn)階實(shí)踐指南》'
- }
- }
- /* 正常掛載 Index 組件 */
- export default class App extends React.Component{
- render(){
- return
- hello world , let us learn React!
-
-
- }
- }
我們通過(guò)報(bào)錯(cuò)信息,不難發(fā)現(xiàn)原因,children 類型錯(cuò)誤,children 應(yīng)該是一個(gè) React element 對(duì)象,但是 Index 返回的卻是一個(gè)普通的對(duì)象。
既然不能是普通的對(duì)象,那么如果 Index 里面更不可能有異步操作了,比如如下這種情況:
- /* 例子2 */
- unction Index(){
- return new Promise((resolve)=>{
- setTimeout(()=>{
- resolve({ name:'《React進(jìn)階實(shí)踐指南》' })
- },1000)
- })
同樣也會(huì)報(bào)上面的錯(cuò)誤,所以在一個(gè)標(biāo)準(zhǔn)的 React 組件規(guī)范下:
- 必須返回 jsx 對(duì)象結(jié)構(gòu),不能返回普通對(duì)象。
- render 執(zhí)行過(guò)程中,不能出現(xiàn)異步操作。
不可能的事變?yōu)榭赡?/h3>
那么如何破局,將不可能的事情變得可能。首先要解決的問(wèn)題是 報(bào)錯(cuò)問(wèn)題 ,只要不報(bào)錯(cuò),App 就能正常渲染。不難發(fā)現(xiàn)產(chǎn)生的錯(cuò)誤時(shí)機(jī)都是在 render 過(guò)程中。那么就可以用 React 提供的兩個(gè)渲染錯(cuò)誤邊界的生命周期 componentDidCatch 和 getDerivedStateFromError。
因?yàn)槲覀円诓东@渲染錯(cuò)誤之后做一些騷操作,所以這里選 componentDidCatch。接下來(lái)我們用 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 捕獲異常,渲染異常
可以看到,雖然還是報(bào)錯(cuò),但是至少頁(yè)面可以正常渲染了?,F(xiàn)在做的事情還不夠,以第一 Index 返回一個(gè)正常對(duì)象為例,我們想要掛載這個(gè)組件,還要獲取 Index 返回的數(shù)據(jù),那么怎么辦呢?
突然想到 componentDidCatch 能夠捕獲到渲染異常,那么它的內(nèi)部就應(yīng)該像 try{}catch(){} 一樣,通過(guò) catch 捕獲異常。類似下面這種:
- try{
- // 嘗試渲染
- }catch(e){
- // 渲染失敗,執(zhí)行componentDidCatch(e)
- componentDidCatch(e)
- }
那么如果在 Index 中拋出的錯(cuò)誤,是不是也可以在 componentDidCatch 接收到。于是說(shuō)干就干。我們把 Index 改變由 return 變成 throw ,然后在 componentDidCatch 打印錯(cuò)誤 error。
- function Index(){
- throw {
- name:'《React進(jìn)階實(shí)踐指南》'
- }
- }
將 throw 對(duì)象返回。
- componentDidCatch(e){
- console.log('error:',e)
- this.setState({ isError:true })
- }
通過(guò) componentDidCatch 捕獲錯(cuò)誤。此時(shí)的 e 就是 Index throw 的對(duì)象。接下來(lái)用子組件拋出的對(duì)象渲染。
- 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 拋出的異常對(duì)象,用對(duì)象里面的數(shù)據(jù)重新渲染。
效果:
大功告成,子組件 throw 錯(cuò)誤,父組件 componentDidCatch 接受并渲染,這波操作是不是有點(diǎn)...
但是 throw 的所有對(duì)象,都會(huì)被正常捕獲嗎?于是我們把第二個(gè) Index 拋出的 Promise 對(duì)象用 componentDidCatch 捕獲??纯磿?huì)是什么吧?
如上所示,Promise 對(duì)象沒(méi)有被正常捕獲,捕獲的是異常的提示信息。在異常提示中,可以找到 Suspense 的字樣。那么 throw Promise 和 Suspense 之間肯定存在著關(guān)聯(lián),換句話說(shuō)就是 Suspense 能夠捕獲到 Promise 對(duì)象。而這個(gè)錯(cuò)誤警告,就是 React 內(nèi)部發(fā)出找不到上層的 Suspense 組件的錯(cuò)誤。
到此為止,可以總結(jié)出:
- componentDidCatch 通過(guò) try{}catch(e){} 捕獲到異常,如果我們?cè)阡秩具^(guò)程中,throw 出來(lái)的普通對(duì)象,也會(huì)被捕獲到。但是 Promise 對(duì)象,會(huì)被 React 底層第 2 次拋出異常。
- Suspense 內(nèi)部可以接受 throw 出來(lái)的 Promise 對(duì)象,那么內(nèi)部有一個(gè) componentDidCatch 專門(mén)負(fù)責(zé)異常捕獲。
鬼畜版——我的組件可以寫(xiě)異步
即然直接 throw Promise 會(huì)在 React 底層被攔截,那么如何在組件內(nèi)部實(shí)現(xiàn)正常編寫(xiě)異步操作的功能呢?既然 React 會(huì)攔截組件拋出的 Promise 對(duì)象,那么如果把 Promise 對(duì)象包裝一層呢? 于是我們把 Index 內(nèi)容做修改。
- function Index(){
- throw {
- current:new Promise((resolve)=>{
- setTimeout(()=>{
- resolve({ name:'《React進(jìn)階實(shí)踐指南》' })
- },1000)
- })
- }
- }
如上,這回不在直接拋出 Promise,而是在 Promise 的外面在包裹一層對(duì)象。接下來(lái)打印錯(cuò)誤看一下。
可以看到,能夠直接接收到 Promise 啦,接下來(lái)我們執(zhí)行 Promise 對(duì)象,模擬異步請(qǐng)求,用請(qǐng)求之后的數(shù)據(jù)進(jìn)行渲染。于是修改 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 的參數(shù) e 中獲取 Promise ,Promise.resolve 執(zhí)行 Promise 獲取數(shù)據(jù)并渲染。
效果:
可以看到數(shù)據(jù)正常渲染了,但是面臨一個(gè)新的問(wèn)題:目前的 Index 不是一個(gè)真正意義上的組件,而是一個(gè)函數(shù),所以接下來(lái),改造 Index 使其變成正常的組件,通過(guò)獲取異步的數(shù)據(jù)。
- 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進(jìn)階實(shí)踐指南》' })
- },1000)
- })
- }
- }
- }
- Index 中通過(guò) isResolve 判斷組件是否加在完成,第一次的時(shí)候 isResolve = false 所以 throw Promise。
- 父組件 App 中接受 Promise ,得到數(shù)據(jù),改變狀態(tài) isResolve ,二次渲染,那么第二次 Index 就會(huì)正常渲染了??匆幌?App 如何寫(xiě):
- 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!
-
-
- }
- }
通過(guò) componentDidCatch 捕獲錯(cuò)誤,然后進(jìn)行第二次渲染。
效果:
達(dá)到了目的。這里就簡(jiǎn)單介紹了一下異步組件的原理。上述引入了一個(gè) Susponse 的概念,接下來(lái)研究一下 Susponse。
飛翔版——實(shí)現(xiàn)一個(gè)簡(jiǎn)單 Suspense
Susponse 是什么?Susponse 英文翻譯 懸停。在 React 中 Susponse 是什么呢?那么正常情況下組件染是一氣呵成的,在 Susponse 模式下的組件渲染就變成了可以先懸停下來(lái)。
首先解釋為什么懸停?
Susponse 在 React 生態(tài)中的位置,重點(diǎn)體現(xiàn)在以下方面。
- code splitting(代碼分割) :哪個(gè)組件加載,就加載哪個(gè)組件的代碼,聽(tīng)上去挺拗口,可確實(shí)打?qū)嵉慕鉀Q了主文件體積過(guò)大的問(wèn)題,間接優(yōu)化了項(xiàng)目的首屏加載時(shí)間,我們知道過(guò)瀏覽器加載資源也是耗時(shí)的,這些時(shí)間給用戶造成的影響就是白屏效果。
- spinner 解耦:正常情況下,頁(yè)面展示是需要前后端交互的,數(shù)據(jù)加載過(guò)程不期望看到 無(wú)數(shù)據(jù)狀態(tài)->閃現(xiàn)數(shù)據(jù)的場(chǎng)景,更期望的是一種spinner數(shù)據(jù)加載狀態(tài)->加載完成展示頁(yè)面狀態(tài)。比如如下結(jié)構(gòu):
List1 和 List2 都使用服務(wù)端請(qǐng)求數(shù)據(jù),那么在加載數(shù)據(jù)過(guò)程中,需要 Spin 效果去優(yōu)雅的展示 UI,所以需要一個(gè) Spin 組件,但是 Spin 組件需要放入 List1 和 List2 的內(nèi)部,就造成耦合關(guān)系?,F(xiàn)在通過(guò) Susponse 來(lái)接耦 Spin,在業(yè)務(wù)代碼中這么寫(xiě)道:
} >
-
-
-
當(dāng) List1 和 List2 數(shù)據(jù)加載過(guò)程中,用 Spin 來(lái) loading 。把 Spin 解耦出來(lái),就像看電影,如果電影加載視頻流卡住,不期望給用戶展示黑屏幕,取而代之的是用海報(bào)來(lái)填充屏幕,而海報(bào)就是這個(gè) Spin 。
- render data:整個(gè) render 過(guò)程都是同步執(zhí)行一氣呵成的,那樣就會(huì) 組件 Render => 請(qǐng)求數(shù)據(jù) => 組件 reRender ,但是在 Suspense 異步組件情況下允許調(diào)用 Render => 發(fā)現(xiàn)異步請(qǐng)求 => 懸停,等待異步請(qǐng)求完畢 => 再次渲染展示數(shù)據(jù)。這樣無(wú)疑減少了一次渲染。
接下來(lái)解釋如何懸停
上面理解了 Suspense 初衷,接下來(lái)分析一波原理,首先通過(guò)上文中,已經(jīng)交代了 Suspense 原理,如何懸停,很簡(jiǎn)單粗暴,直接拋出一個(gè)異常;
異常是什么,一個(gè) Promise ,這個(gè) Promise 也分為二種情況:
- 第一種就是異步請(qǐng)求數(shù)據(jù),這個(gè) Promise 內(nèi)部封裝了請(qǐng)求方法。請(qǐng)求數(shù)據(jù)用于渲染。
- 第二種就是異步加載組件,配合 webpack 提供的 require() api,實(shí)現(xiàn)代碼分割。
懸停后再次render
在 Suspense 懸停后,如果想要恢復(fù)渲染,那么 rerender 一下就可以了。
如上詳細(xì)介紹了 Suspense 。接下來(lái)到了實(shí)踐環(huán)節(jié),我們?nèi)L試實(shí)現(xiàn)一個(gè) Suspense ,首先聲明一下這個(gè) Suspense 并不是 React 提供的 Suspense ,這里只是模擬了一下它的大致實(shí)現(xiàn)細(xì)節(jié)。
本質(zhì)上 Suspense 落地瓶頸也是對(duì)請(qǐng)求函數(shù)的的封裝,Suspense 主要接受 Promise,并 resolve 它,那么對(duì)于成功的狀態(tài)回傳到異步組件中,對(duì)于開(kāi)發(fā)者來(lái)說(shuō)是未知的,對(duì)于 Promise 和狀態(tài)傳遞的函數(shù) createFetcher,應(yīng)該滿足如下的條件。
- const fetch = createFetcher(function getData(){
- return new Promise((resolve)=>{
- setTimeout(()=>{
- resolve({
- name:'《React進(jìn)階實(shí)踐指南》',
- author:'alien'
- })
- },1000)
- })
- })
- function Text(){
- const data = fetch()
- return
- name: {data.name}
- author:{data.author}
-
- }
- 通過(guò) createFetcher 封裝請(qǐng)求函數(shù)。請(qǐng)求函數(shù) getData 返回一個(gè) Promise ,這個(gè) Promise 的使命就是完成數(shù)據(jù)交互。
- 一個(gè)模擬的異步組件,內(nèi)部使用 createFetcher 創(chuàng)建的請(qǐng)求函數(shù),請(qǐng)求數(shù)據(jù)。
接下來(lái)就是 createFetcher 函數(shù)的編寫(xiě)。
- function createFetcher(fn){
- const fetcher = {
- status:'pedding',
- result:null,
- p:null
- }
- return function (){
- const getDataPromise = fn()
- fetcher.p = getDataPromise
- getDataPromise.then(result=>{ /* 成功獲取數(shù)據(jù) */
- fetcher.result = result
- fetcher.status = 'resolve'
- })
- if(fetcher.status === 'pedding'){ /* 第一次執(zhí)行中斷渲染,第二次 */
- throw fetcher
- }
- /* 第二次執(zhí)行 */
- if(fetcher.status==='resolve')
- return fetcher.result
- }
- }
- 這里要注意的是 fn 就是 getData, getDataPromise 就是 getData返回的 Promise。
- 返回一個(gè)函數(shù) fetch ,在 Text 內(nèi)部執(zhí)行,第一次組件渲染,由于 status = pedding 所以拋出異常 fetcher 給 Susponse,渲染中止。
- Susponse 會(huì)在內(nèi)部 componentDidCatch 處理這個(gè)fetcher,執(zhí)行 getDataPromise.then, 這個(gè)時(shí)候status已經(jīng)是resolve狀態(tài),數(shù)據(jù)也能正常返回了。
- 接下來(lái)Susponse再次渲染組件,此時(shí)就能正常的獲取數(shù)據(jù)了。
既然有了 createFetcher 函數(shù),接下來(lái)就要模擬上游組件 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
- }
- }
我們編寫(xiě)的 Susponse 起名字叫 MySusponse 。
- MySusponse 內(nèi)部 componentDidCatch 通過(guò) Promise.resolve 捕獲 Promise 成功的狀態(tài)。成功后,取締 fallback UI 效果。
大功告成,接下來(lái)就是體驗(yàn)環(huán)節(jié)了。我們嘗試一下 MySusponse 效果。
- export default function Index(){
- return
- hello,world
-
loading... } >
-
-
-
效果:
雖然實(shí)現(xiàn)了效果,但是和真正的 Susponse 還差的很遠(yuǎn),首先暴露出的問(wèn)題就是數(shù)據(jù)可變的問(wèn)題。上述編寫(xiě)的 MySusponse 數(shù)據(jù)只加載一次,但是通常情況下,數(shù)據(jù)交互是存在變數(shù)的,數(shù)據(jù)也是可變的。
衍生版——實(shí)現(xiàn)一個(gè)錯(cuò)誤異常處理組件
言歸正傳,我們不會(huì)在函數(shù)組件中做如上的騷操作,也不會(huì)自己去編寫(xiě) createFetcher 和 Susponse。但是有一個(gè)場(chǎng)景還是蠻實(shí)用的,那就是對(duì)渲染錯(cuò)誤的處理,以及 UI 的降級(jí),這種情況通常出現(xiàn)在服務(wù)端數(shù)據(jù)的不確定的場(chǎng)景下,比如我們通過(guò)服務(wù)端的數(shù)據(jù) data 進(jìn)行渲染,像如下場(chǎng)景:
{ data.name }
如果 data 是一個(gè)對(duì)象,那么會(huì)正常渲染,但是如果 data 是 null,那么就會(huì)報(bào)錯(cuò),如果不加渲染錯(cuò)誤邊界,那么一個(gè)小問(wèn)題會(huì)導(dǎo)致整個(gè)頁(yè)面都渲染不出來(lái)。
那么對(duì)于如上情況,如果每一個(gè)頁(yè)面組件,都加上 componentDidCatch 這樣捕獲錯(cuò)誤,降級(jí) UI 的方式,那么代碼過(guò)于冗余,難以復(fù)用,無(wú)法把降級(jí)的 UI 從業(yè)務(wù)組件中解耦出來(lái)。
所以可以統(tǒng)一寫(xiě)一個(gè) RenderControlError 組件,目的就是在組件的出現(xiàn)異常的情況,統(tǒng)一展示降級(jí)的 UI ,也確保了整個(gè)前端應(yīng)用不會(huì)奔潰,同樣也讓服務(wù)端的數(shù)據(jù)格式容錯(cuò)率大大提升。接下來(lái)看一下具體實(shí)現(xiàn)。
- class RenderControlError extends React.Component{
- state={
- isError:false
- }
- componentDidCatch(){
- this.setState({ isError:true })
- }
- render(){
- return !this.state.isError ?
- this.props.children :
![]()
- style={styles.erroImage}
- />
- 出現(xiàn)錯(cuò)誤
- }
- }
如果 children 出錯(cuò),那么降級(jí) UI。
使用
總結(jié)
本文通過(guò)一些腦洞大開(kāi),奇葩的操作,讓大家明白了 Susponse ,componentDidCatch 等原理。我相信不久之后,隨著 React 18 發(fā)布,Susponse 將嶄露頭角,未來(lái)可期。
網(wǎng)站名稱:「React進(jìn)階」我在函數(shù)組件中可以隨便寫(xiě)-通俗異步組件原理
網(wǎng)頁(yè)地址:http://fisionsoft.com.cn/article/djgcoip.html


咨詢
建站咨詢
