簡(jiǎn)介
我們繼續(xù)上一節(jié)的內(nèi)容,開始分析 React 官網(wǎng):https://reactjs.org/docs/accessibility.html 的 “高級(jí)指引” 部分,這一部分會(huì)涉及到異步組件、全局上下文對(duì)象、錯(cuò)誤邊界組件等概念的分析,比前面章節(jié)的難度還是略微大一些的,所以一定要跟上節(jié)奏哦,我們一起出發(fā)吧!
知識(shí)點(diǎn)
- 代碼分割
- 異步組件
- 全局上下文對(duì)象 Context
- 錯(cuò)誤邊界組件
準(zhǔn)備
我們直接用上一節(jié)中的 react-demo-day5 項(xiàng)目來作為我們的 Demo 項(xiàng)目,還沒有創(chuàng)建的小伙伴可以直接執(zhí)行以下命令 clone 一份代碼:
git clone -b dev https://gitee.com/vv_bug/react-demo-day5.git
接著進(jìn)入到項(xiàng)目根目錄 react-demo-day5 ,并執(zhí)行以下命令來安裝依賴與啟動(dòng)項(xiàng)目:
npm install --registry https://registry.npm.taobao.org && npm start

等項(xiàng)目打包編譯成功,瀏覽器會(huì)自動(dòng)打開項(xiàng)目入口,看到上面截圖的效果的時(shí)候,我們的準(zhǔn)備工作就完成了。
代碼分割
因?yàn)槲覀冞@一節(jié)分析的主要是 React 的 “高級(jí)指引” 部分內(nèi)容,所以我們先在 src 目錄下創(chuàng)建一個(gè) advanced-guides 目錄,用來存放 “高級(jí)指引” 的內(nèi)容:
mkdir ./src/advanced-guides
然后在 src/advanced-guides 目錄下創(chuàng)建一個(gè) index.tsx 文件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
function AdvancedGuides() {
return (
<div>
{/* 代碼分割 */}
<CodeSplit/>
</div>
);
};
export default AdvancedGuides;
接著在 src/main.tsx 文件中引入 AdvancedGuides 組件:
import React from "react";
import ReactDOM from "react-dom";
import "./main.scss";
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
// App 組件
const App = (
<div className="root">
{/* 核心概念 */}
<MainConcepts/>
{/* 高級(jí)指引 */}
<AdvancedGuides/>
</div>
);
ReactDOM.render(
App,
document.getElementById("root")
);
ok,我們 “高級(jí)指引” 部分的內(nèi)容就可以在 AdvancedGuides 組件中做測(cè)試了。
我們首先在 src/advanced-guides 目錄下創(chuàng)建一個(gè) code-split 目錄,準(zhǔn)備做 “代碼分割” 的測(cè)試:
mkdir ./src/advanced-guides/code-split
接著在 src/advanced-guides/code-split 目錄下創(chuàng)建一個(gè) index.tsx 文件:
import React from "react";
// 定義一個(gè)異步組件
const LazyComponent = React.lazy(()=>import("./lazy.com"));
function CodeSplit(){
return (
<React.Fragment>
{/* 渲染異步組件 */}
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent/>
</React.Suspense>
</React.Fragment>
);
}
export default CodeSplit;
可以看到,我們用 React.lazy 方法定義了一個(gè)異步組件,然后在 React.Suspense 組件中渲染了這個(gè)異步組件(注意:React.lazy 返回的組件必須配合 Suspense 組件使用,而且 Suspense 組件必須提供 fallback 屬性,Suspense 組件我們后面再詳細(xì)解析)。
然后在 src/advanced-guides/code-split 目錄下創(chuàng)建一個(gè) lazy.com.tsx 文件:
function LazyComponent(){
return (
<div>我是一個(gè)異步組件</div>
);
}
export default LazyComponent;
可以看到,我們定義了一個(gè)簡(jiǎn)單的 “異步組件”。
我們重新運(yùn)行項(xiàng)目看結(jié)果:
npm start

可以看到,我們的 lazy.com.tsx 組件被單獨(dú)分割到了一個(gè) js 文件中,當(dāng)這個(gè) js 文件加載并執(zhí)行完畢后,頁面顯示了這個(gè)異步組件的內(nèi)容。
其實(shí)我們還可以利用 State 單獨(dú)使用異步組件。
我們修改一下 src/advanced-guides/code-split/index.tsx 組件:
import React, {useState, useEffect} from "react";
// 定義一個(gè)異步組件
const LazyComponent = import("./lazy.com");
function CodeSplit() {
let [Com, setCom] = useState(<div>Loading...</div>);
useEffect(() => {
LazyComponent.then((module: any) => {
setCom((React.createElement(module.default, {}, [])) as any);
});
}, []);
return (
<React.Fragment>
{/* 渲染異步組件 */ }
{ Com }
</React.Fragment>
);
}
export default CodeSplit;
可以看到,我們利用 useEffect 定義了一個(gè) Hook,然后通過 LazyComponent.then 獲取到了異步組件 lazy.com.tsx,最后利用 State 把組件渲染到了頁面,效果跟前面一樣,我就不演示了,小伙伴自己跑一下代碼看效果哦。
所以我們大膽猜測(cè)一下,Suspense 組件的是不是也是這樣實(shí)現(xiàn)的呢?這個(gè)答案就留到我們后面源碼解析部分再去解析了。
Context
Context 提供了一個(gè)無需為每層組件手動(dòng)添加 props,就能在組件樹間進(jìn)行數(shù)據(jù)傳遞的方法。
解釋起來可能有點(diǎn)抽象,我們還是利用 Demo 來演示一下。
比如我們的應(yīng)用需要添加一個(gè)換主題的功能,能夠切換 Dark 跟 Light 主題。
我們首先在 src 目錄下創(chuàng)建一個(gè)主題樣式文件 themes.scss:
touch ./src/themes.scss
接著我們?cè)?src/themes.scss 中定義兩種主題 Dark 跟 Light:
/* Light 主題 */
.theme-light {
color: black;
background-color: white;
}
/* Dark 主題 */
.theme-dark {
color: white;
background-color: darkgray;
}
可以看到,我們簡(jiǎn)單的定義了兩個(gè)樣式 theme-light 跟 theme-dark。
接著我們?cè)?src/main.tsx 入口文件中引入這個(gè)主題樣式文件 themes.scss:
import React from "react";
import ReactDOM from "react-dom";
import "./main.scss";
// 引入主題樣式
import "./themes.scss";
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
// App 組件
const App = (
<div className="root">
{/* 核心概念 */}
<MainConcepts/>
{/* 高級(jí)指引 */}
<AdvancedGuides/>
</div>
);
ReactDOM.render(
App,
document.getElementById("root")
);
然后我們對(duì) src/main.tsx 入口進(jìn)行一下改造,把 App 組件單獨(dú)提出到一個(gè)文件中去。
首先在 src 目錄下創(chuàng)建一個(gè) app.tsx 文件作為 App 組件:
touch ./src/app.tsx
然后將 src/main.tsx 中的 App 組件抽離到 src/app.tsx,抽離后的 src/main.tsx 文件:
import React from "react";
import ReactDOM from "react-dom";
import "./main.scss";
// 引入主題樣式
import "./themes.scss";
// App 組件
import App from "./app";
ReactDOM.render(
<App/>,
document.getElementById("root")
);
src/app.tsx 文件內(nèi)容:
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
import React from "react";
function App(){
return (
<div className="root">
{/* 核心概念 */}
<MainConcepts/>
{/* 高級(jí)指引 */}
<AdvancedGuides/>
</div>
)
}
export default App;
React.createContext
創(chuàng)建一個(gè) Context 對(duì)象。
在 src 目錄下創(chuàng)建一個(gè) app-context.tsx 文件:
// 定義主題枚舉類型
import React from "react";
export enum Themes {Light, Dark};
// 定義 AppContext 類型
export type AppContextType = {
theme: Themes,
toggleTheme: () => void
};
// AppContext 的默認(rèn)值
export const defaultAppContext = {
theme: Themes.Light,
toggleTheme: () => {
}
};
// 創(chuàng)建一個(gè) AppContext 對(duì)象
export const AppContext = React.createContext<AppContextType>(defaultAppContext);
可以看到,我們創(chuàng)建并導(dǎo)出了一個(gè) AppContext 對(duì)象。
Context.Provider
每個(gè) Context 對(duì)象都會(huì)返回一個(gè) Provider React 組件,它允許消費(fèi)組件訂閱 context 的變化。Provider 接收一個(gè) value 屬性,傳遞給消費(fèi)組件。一個(gè) Provider 可以和多個(gè)消費(fèi)組件有對(duì)應(yīng)關(guān)系。多個(gè) Provider 也可以嵌套使用,里層的會(huì)覆蓋外層的數(shù)據(jù)。
當(dāng) Provider 的 value 值發(fā)生變化時(shí),它內(nèi)部的所有消費(fèi)組件都會(huì)重新渲染。Provider 及其內(nèi)部 consumer 組件都不受制于 shouldComponentUpdate 函數(shù),因此當(dāng) consumer 組件在其祖先組件退出更新的情況下也能更新。
我們利用 Context.Provider 組件把 AppContext 對(duì)象共享給所有的組件,修改一下 src/app.tsx:
import MainConcepts from "./main-concepts";
import AdvancedGuides from "./advanced-guides";
import React, {useState} from "react";
import {AppContext, Themes, AppContextType} from "./app-context";
function App() {
function toggleTheme() {
setAppContext((preAppContext) => {
return {
theme: Themes.Light === preAppContext.theme ? Themes.Dark : Themes.Light,
toggleTheme
};
});
}
let [appContext, setAppContext] = useState<AppContextType>({
theme: Themes.Light,
toggleTheme
});
return (
<AppContext.Provider value={ appContext }>
<div className={ ["theme-light", "theme-dark"][appContext.theme] }>
{/* 核心概念 */ }
<MainConcepts/>
{/* 高級(jí)指引 */ }
<AdvancedGuides/>
</div>
</AppContext.Provider>
);
}
export default App;
可以看到,我們用 AppContext.Provider 組件把我們的 AppContext 對(duì)象中的 value 屬性共享給了所有組件,并且用 useState 創(chuàng)建了一個(gè) State 去管理這個(gè) value 的狀態(tài)。
那么我們的子組件中怎么才能拿到 AppContext 對(duì)象共享的 value 值呢?
Class.contextType
我們可以利用類組件中的 contextType 聲明來獲取到 AppContext 對(duì)象。
我們?cè)?src/advanced-guides 目錄下創(chuàng)建一個(gè) context 目錄:
mkdir ./src/advanced-guides/context
接著在 src/advanced-guides/context 目錄下創(chuàng)建一個(gè) index.tsx 文件:
import React from "react";
import ContextCom from "./context.com";
function Context() {
return (
<React.Fragment>
{/* 類組件方式 */ }
<ContextCom/>
</React.Fragment>
);
}
export default Context;
然后在 src/advanced-guides/context 目錄下創(chuàng)建一個(gè) context.com.tsx 組件:
import React from "react";
import {AppContext} from "../../app-context";
class ContextCom extends React.Component {
render() {
return (
<div>
<button onClick={ this.context.toggleTheme }>點(diǎn)我切換主題</button>
</div>
);
}
}
// 定義 ContextCom 組件的 contextType 類型
ContextCom.contextType = AppContext;
export default ContextCom;
最后在 src/advanced-guides/index.tsx 文件中引入 src/advanced-guides/context/index.tsx 組件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
function AdvancedGuides() {
return (
<div>
{/* 代碼分割 */ }
<CodeSplit/>
{/* Context */ }
<Context/>
</div>
);
};
export default AdvancedGuides;
重新運(yùn)行項(xiàng)目看結(jié)果:
npm start

可以看到,我們成功的利用 Context 實(shí)現(xiàn)了 “換主題” 的效果。
Context.Consumer
此組件可以讓你在 函數(shù)式組件 中可以訂閱 context。
接下來我們用函數(shù)式組件來實(shí)現(xiàn)一下 src/advanced-guides/context/context.com.tsx 組件。
首先在 src/advanced-guides/context 目錄下創(chuàng)建一個(gè) context.func.tsx 組件:
import {AppContext} from "../../app-context";
import React from "react";
function ContextFunc() {
return (
<div>
<AppContext.Consumer>
{ ({toggleTheme}) => <button onClick={ toggleTheme }>點(diǎn)我切換主題</button> }
</AppContext.Consumer>
</div>
);
}
export default ContextFunc;
然后在 src/advanced-guides/context/index.tsx 組件中引入 context.func.tsx 組件:
import React from "react";
import ContextCom from "./context.com";
import ContextFunc from "./context.func";
function Context() {
return (
<React.Fragment>
{/* 類組件方式 */ }
<ContextCom/>
{/* 函數(shù)組件方式 */ }
<ContextFunc/>
</React.Fragment>
);
}
export default Context;
效果跟前面一樣,我就不演示了,小伙伴自己跑跑項(xiàng)目看效果哦。
其實(shí)在 React 中,像這種全局共享數(shù)據(jù)方案有很多,像 Redux、Mobox 等第三方狀態(tài)管理庫,我們后面講 React 全家桶的時(shí)候會(huì)詳細(xì)介紹,當(dāng)然,一些簡(jiǎn)單的全局?jǐn)?shù)據(jù)共享,我們直接用 Context 方案就可以了,沒必要引入那些重量級(jí)的全局狀態(tài)管理框架了。
錯(cuò)誤邊界
錯(cuò)誤邊界是一種 React 組件,這種組件可以捕獲并打印發(fā)生在其子組件樹任何位置的 JavaScript 錯(cuò)誤,并且,它會(huì)渲染出備用 UI,而不是渲染那些崩潰了的子組件樹。錯(cuò)誤邊界在渲染期間、生命周期方法和整個(gè)組件樹的構(gòu)造函數(shù)中捕獲錯(cuò)誤。
注意
錯(cuò)誤邊界無法捕獲以下場(chǎng)景中產(chǎn)生的錯(cuò)誤:
- 事件處理
- 異步代碼(例如
setTimeout或requestAnimationFrame回調(diào)函數(shù)) - 服務(wù)端渲染
- 它自身拋出來的錯(cuò)誤(并非它的子組件)
我們還是來演示一下效果吧。
首先在 src/advanced-guides 目錄下創(chuàng)建一個(gè) error.tsx 組件:
touch ./src/advanced-guides/error.tsx
src/advanced-guides/error.tsx:
function ErrorCom(): null{
throw new Error("報(bào)錯(cuò)啦!");
}
export default ErrorCom;
可以看到,我們創(chuàng)建了一個(gè)函數(shù)式組件 ErrorCom,然后直接通過 throw 拋出了一個(gè) Error。
我們?cè)?src/advanced-guides/index.tsx 文件中引入 error.tsx 組件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorCom from "./error";
function AdvancedGuides() {
return (
<div>
{/* 代碼分割 */ }
<CodeSplit/>
{/* Context */ }
<Context/>
{/* 報(bào)錯(cuò)的組件 */ }
<ErrorCom/>
</div>
);
};
export default AdvancedGuides;
然后我們重新運(yùn)行項(xiàng)目看結(jié)果:
npm start

可以看到,直接報(bào)錯(cuò)了,整個(gè)頁面都掛了。
但是在我們正常的項(xiàng)目開發(fā)中,我們并不希望因?yàn)槟骋粋€(gè)組件出錯(cuò)整個(gè)應(yīng)用都掛掉的情況。
接下來我們就用 "錯(cuò)誤邊界" 組件來處理一下這種情況。
我們?cè)?src/advanced-guides 目錄下創(chuàng)建一個(gè) error-boundaries.tsx 組件:
import React from "react";
class ErrorBoundaries extends React.Component {
state = {
hasError: false
};
static getDerivedStateFromError() {
// 更新 state 使下一次渲染能夠顯示降級(jí)后的 UI
return {hasError: true};
}
componentDidCatch(error: any, errorInfo: any) {
// eslint-disable-next-line no-console
console.log("error", error);
// eslint-disable-next-line no-console
console.log("errorInfo", errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定義降級(jí)后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundaries;
可以看到,ErrorBoundaries 組件中聲明了一個(gè)靜態(tài)的方法 getDerivedStateFromError 跟一個(gè) componentDidCatch 方法。
static getDerivedStateFromError
此生命周期會(huì)在后代組件拋出錯(cuò)誤后被調(diào)用。 它將拋出的錯(cuò)誤作為參數(shù),并返回一個(gè)值以更新 state。
componentDidCatch
此生命周期在后代組件拋出錯(cuò)誤后被調(diào)用。 它接收兩個(gè)參數(shù):
-
error—— 拋出的錯(cuò)誤。 -
info—— 帶有componentStackkey 的對(duì)象。
接著我們?cè)?src/advanced-guides/index.tsx 組件中引用 ErrorBoundaries 組件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorBoundaries from "./error-boundaries";
import ErrorCom from "./error";
function AdvancedGuides() {
return (
<ErrorBoundaries>
<div>
{/* 代碼分割 */ }
<CodeSplit/>
{/* Context */ }
<Context/>
{/* 報(bào)錯(cuò)的組件 */ }
<ErrorCom/>
</div>
</ErrorBoundaries>
);
};
export default AdvancedGuides;
我們重新運(yùn)行項(xiàng)目看結(jié)果:
npm start

可以看到,src/advanced-guides/error-boundaries.tsx 組件中成功捕捉到了錯(cuò)誤,應(yīng)用也沒有全部掛掉,只是 src/advanced-guides/index.tsx 組件中的內(nèi)容:
<ErrorBoundaries>
<div>
{/* 代碼分割 */ }
<CodeSplit/>
{/* Context */ }
<Context/>
{/* 報(bào)錯(cuò)的組件 */ }
<ErrorCom/>
</div>
</ErrorBoundaries>
由于錯(cuò)誤的原因,直接替換成了:
if (this.state.hasError) {
// 你可以自定義降級(jí)后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
邊界處理組件在錯(cuò)誤的捕獲與收集上很有用處,可以結(jié)合一些錯(cuò)誤收集框架做線上錯(cuò)誤統(tǒng)計(jì),快速分析出一些 bug 問題原因。
總結(jié)
我們通過 Demo 演示了什么是異步組件、Context 對(duì)象、錯(cuò)誤邊界組件,有些小伙伴要說了 ”我們何不把所有的組件都做成異步組件?所有的全局?jǐn)?shù)據(jù)共享都用 Context?給所有的模塊都加上錯(cuò)誤邊界組件?“,小伙伴一定要結(jié)合具體項(xiàng)目場(chǎng)景來使用這些高級(jí)特性,比如你項(xiàng)目本來就不大,你還把所有的組件都做成異步組件,這樣做不但沒有加快應(yīng)用渲染速度,反而會(huì)引起服務(wù)器壓力過大,然后把所有的全局狀態(tài)共享都用 Context 處理,這樣做雖然可以達(dá)到效果,但是當(dāng) Context 對(duì)象中邏輯過于龐大,這樣做反而不利于全局狀態(tài)的管理,而且管理不好還會(huì)造成狀態(tài)更新頻繁而引起性能問題,最后你會(huì)得不償失的。
好啦,這節(jié)到這就結(jié)束啦。
Demo 項(xiàng)目代碼下載:https://gitee.com/vv_bug/react-demo-day5/tree/dev
歡迎志同道合的小伙伴一起交流,一起學(xué)習(xí)。
覺得寫得不錯(cuò)的可以點(diǎn)點(diǎn)關(guān)注,幫忙轉(zhuǎn)發(fā)跟點(diǎn)贊。