快來跟我一起學(xué) React(Day6)

簡(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
1-1.png

等項(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
1-2.png

可以看到,我們的 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è)換主題的功能,能夠切換 DarkLight 主題。

我們首先在 src 目錄下創(chuàng)建一個(gè)主題樣式文件 themes.scss

touch ./src/themes.scss

接著我們?cè)?src/themes.scss 中定義兩種主題 DarkLight

/* Light 主題 */
.theme-light {
  color: black;
  background-color: white;
}

/* Dark 主題 */
.theme-dark {
  color: white;
  background-color: darkgray;
}

可以看到,我們簡(jiǎn)單的定義了兩個(gè)樣式 theme-lighttheme-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
1-3.gif

可以看到,我們成功的利用 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ò)誤:

  • 事件處理
  • 異步代碼(例如 setTimeoutrequestAnimationFrame 回調(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
1-4.png

可以看到,直接報(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ù):

  1. error —— 拋出的錯(cuò)誤。
  2. info —— 帶有 componentStack key 的對(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
1-5.png

可以看到,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)贊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容