讓你 React 組件水平暴增的 5 個(gè)技巧
最近看了一些 Ant Design 的組件源碼,學(xué)到一些很實(shí)用的技巧,這篇文章來分享一下。
首先,我們用 create-react-app 創(chuàng)建個(gè) React 項(xiàng)目(選擇 typescript 模版):
npx?create-react-app?--template=typescript?component-test

進(jìn)入項(xiàng)目目錄,把開發(fā)服務(wù)跑起來:
npm?run?start

然后引入 antd:
npm?install?--save?antd
在 App.tsx 里引入幾個(gè) antd 組件:

頁面上可以看到這倆組件都成功渲染了:

然后我們來看一下 Ant Design 組件里的一些技巧:
透?jìng)?className、style
我們可以給組件設(shè)置 className 和 style:
import?'./App.css';
import?{?Button?}?from?'antd';
function?App()?{
??return?(
????<div?className="App">
????????<Button?className="aaa?bbb"?style={{
??????????width:?'100px',
??????????height:?'50px'
????????}}?type="primary">測(cè)試</Button>
????</div>
??);
}
export?default?App;
在頁面里打開 DevTools 可以看到 className 和 style 都被設(shè)置到了 button 上。

這種功能的實(shí)現(xiàn)就是透?jìng)?className 和 style 的 props。
基本 antd 所有的組件都會(huì)做這個(gè)。
比如 VisualList 組件的源碼:

它取了傳入的 className、style 的 props,還有剩余的所有 props。
對(duì) className 做了一些處理,添加了兩個(gè) className:

對(duì) style 也做了擴(kuò)展,添加了個(gè) position: relative 的樣式。

然后把 style、className,額外的 props 都設(shè)置給最外層的 div。
這樣,使用這個(gè)組件的時(shí)候,就可以自己定義一些樣式,設(shè)置一些 props。
其中,classnames 是用來動(dòng)態(tài)產(chǎn)生 className 的一個(gè)包,用起來很簡(jiǎn)單。
比如這樣調(diào)用:
classNames('aaa',?{?bbb:?true,?ccc:?false?},?false,?{?eee:?true?});?
那么最終的 className 就是 'aaa bbb eee'。
這樣,組件用起來體驗(yàn)就和 html 標(biāo)簽差不多,可以自己控制一些樣式。
這樣寫 props 的類型的時(shí)候,也是直接用了 html 標(biāo)簽的類型。
比如這個(gè) List 的參數(shù)就繼承了 React.HTMLArrtibutes<any>,也就是任意 html 標(biāo)簽的屬性:

當(dāng)然,children 屬性是不可以設(shè)置的。因?yàn)?React 用 children 參數(shù)來傳遞子組件。
比如 form 組件:
它的參數(shù)是繼承了 React.FormHTMLAttributes<HTMLFormElement>:

去掉了 children 和 onSubmit 這倆屬性,因?yàn)檫@倆是 From 組件的參數(shù)。
也就是說:antd 的組件基本都支持傳入 className、style 或者任何 html 標(biāo)簽的 props,會(huì)透?jìng)?props 到組件內(nèi)的容器標(biāo)簽,所以用起來體驗(yàn)和原生標(biāo)簽很類似。但這也要求 props 實(shí)現(xiàn) React.FormHTMLAttributes 的 type。
通過 forwardRef 暴露一些方法
外界控制組件的方式就是通過傳 props,但有時(shí)候想調(diào)用組件的一些方法呢?
這時(shí)候就需要 ref 了。
我們先來試一下 ref:

通過 useRef 創(chuàng)建個(gè) ref 對(duì)象,然后把 input 標(biāo)簽設(shè)置到 ref。
在 useEffect 里就可以調(diào)用 input 的方法了:

但這是原生標(biāo)簽,如果是組件呢?
這時(shí)候就需要 forwardRef 了,也就是把組件內(nèi)的 ref 轉(zhuǎn)發(fā)一下。
比如這樣:
import?'./App.css';
import?{?useRef?}?from?'react';
import?{?useEffect?}?from?'react';
import?React?from?'react';
const?Guang:?React.ForwardRefRenderFunction<HTMLInputElement>?=?(props,?ref)?=>?{
??return?<div>
????<input?ref={ref}></input>
??</div>
}
const?WrapedGuang?=?React.forwardRef(Guang);
function?App()?{
??const?ref?=?useRef<HTMLInputElement>(null);
?
??useEffect(()=>?{
????console.log('ref',?ref.current)
????ref.current?.focus()
??},?[]);
??return?(
????<div?className="App">
??????<WrapedGuang?ref={ref}/>
????</div>
??);
}
export?default?App;
其實(shí) forwardRef 這個(gè) api 做的事情也很容易懂。
就是把 ref 轉(zhuǎn)發(fā)到組件內(nèi)部來設(shè)置:

這樣就把組件內(nèi)的 input 通過 ref 的方式傳遞到了組件外。
效果和之前一樣:

不過被 forwardRef 包裹的組件的類型就要用 React.forwardRefRenderFunction 了:

第一個(gè)類型參數(shù)是 ref 的 content 的類型。
但有的時(shí)候,我不是想把原生標(biāo)簽暴露出去,而是暴露一些自定義方法。
這時(shí)候就需要 useImperativeHandle 的 hook 了。
這樣寫:
import?'./App.css';
import?{?useRef?}?from?'react';
import?{?useEffect?}?from?'react';
import?React?from?'react';
import?{?useImperativeHandle?}?from?'react';
interface?RefProps?{
??aaa:?()?=>?void;
}
const?Guang:?React.ForwardRefRenderFunction<RefProps>?=?(props,?ref)?=>?{
??const?inputRef?=?useRef<HTMLInputElement>(null);
??useImperativeHandle(ref,?()?=>?{
????return?{
??????aaa()?{
????????inputRef.current?.focus();
??????}
????}
??});
??return?<div>
????<input?ref={inputRef}></input>
??</div>
}
const?WrapedGuang?=?React.forwardRef(Guang);
function?App()?{
??const?ref?=?useRef<RefProps>(null);
?
??useEffect(()=>?{
????console.log('ref',?ref.current)
????ref.current?.aaa();
??},?[]);
??return?(
????<div?className="App">
??????<WrapedGuang?ref={ref}/>
????</div>
??);
}
export?default?App;
也就是用 useImperativeHanlde 自定義了 ref 對(duì)象:

小結(jié)一下:
React 可以用 ref 保存原生標(biāo)簽,通過 ref.current 調(diào)用這個(gè)對(duì)象的屬性、方法。跨組件傳遞 ref 需要用 forwardRef 方法,如果你要進(jìn)一步自定義 ref,那就要用 useImperativeHandle 的 hook。
然后看看 antd 組件是怎么用 ref 的。
就如說 VisualList 組件:

它也是包了一層 React.forwardRef,內(nèi)部用 useImperativeHandle 自定義了 ref:

這樣外部就可以調(diào)用這個(gè) ref 的方法了:

再比如 Form 組件:
它也是被 forwarRef 包裹的函數(shù)組件:

內(nèi)部用 useImperativeHandle 返回了自定義的對(duì)象:

所以你才可以這樣調(diào)用 form 組件的方法:

這就是說:antd 的組件都會(huì)用 forwardRef 包裹一層,用來轉(zhuǎn)發(fā) ref,或者是轉(zhuǎn)發(fā)內(nèi)部的 html 標(biāo)簽的引用,或者是用 useImperativeHandle 自定義 ref 對(duì)象,來暴露一些方法。
useCallback、useMemo
useMemo 和 useCallback 是性能優(yōu)化相關(guān)的 hook。
很多人不知道啥時(shí)候用,其實(shí)看下 antd 怎么用的就知道了:

比如 VisualList 組件里計(jì)算 start、end、scrollHeight 這些值需要大量的計(jì)算。
這些計(jì)算需要每次 render 都跑一遍么?
不需要,只有在某些值變化的時(shí)候才需要重新計(jì)算。
這時(shí)候用 React.useMemo 包裹就可以減少計(jì)算量,它只會(huì)在 deps 數(shù)組變化的時(shí)候執(zhí)行第一個(gè)參數(shù)的函數(shù)。
useMemo 是 deps 變化之后重新執(zhí)行函數(shù)創(chuàng)建值,而 useCallback 并不會(huì)執(zhí)行函數(shù),它只是在 deps 變化的時(shí)候返回第一個(gè)參數(shù)的函數(shù):

這樣有什么用呢?
react 重新渲染的依據(jù)是 props 是否有變化,如果每次都創(chuàng)建新的函數(shù),那是不是每次都會(huì)重新渲染?
所以用 useCallback 包裹的函數(shù)參數(shù),就可以在 deps 沒變的時(shí)候,始終返回同一個(gè)函數(shù),這樣避免了沒必要的渲染。
當(dāng)然,useMemo 也有這個(gè)作用:
比如說 Form 組件源碼里的這個(gè) useMemo:

你說它是為了減少計(jì)算量么?
并不是,它沒有做任何計(jì)算,只是把參數(shù)原封不動(dòng)返回了。
這也同樣是為了避免 props 變化。
也就是說:antd 里很多地方都用了 useMemo 和 useCallback 來進(jìn)行渲染性能優(yōu)化。useMemo 只有在 deps 數(shù)組變化的時(shí)候才會(huì)執(zhí)行第一個(gè)函數(shù),返回新的值,可以用來減少不必要的計(jì)算,也可以保證 props 不變來避免不要的渲染。useCallback 是只有 deps 數(shù)組變化的時(shí)候才返回第一個(gè)函數(shù)的值,可以保證 props 不變來用來避免不必要的渲染
用 Context 來跨組件傳遞值
antd 里很多配置的傳遞都是通過 Context。
比如 disabled 的設(shè)置:

通過 React.createContext 創(chuàng)建 context 對(duì)象,通過 Provider 修改 context 的值。
在最外層包裹這個(gè) Provider 組件來修改 context 值:

然后你可以在任意的組件把 context 值取出來用:


像什么主題、大小等配置,都是通過 Context 傳遞的。
除了用來傳遞配置外,很多組件也依賴 Context 來傳遞一些值,比如 Form:

在 Form 組件里設(shè)置 form 對(duì)象,然后 setFieldValue 設(shè)置字段值。
為什么 Form.Item 里加個(gè) name 就可以取出來了呢?
我并沒有傳遞 form 參數(shù)過去呀?
很明顯,這里也是用 Context 來傳遞的:
antd 會(huì)創(chuàng)建這樣一個(gè) context 對(duì)象:

然后在外層用 Provider 設(shè)置 context 值:

也就是我們這里傳的 form:

那 Form.Item 里自然可以拿到 context 的值,從而取到具體字段信息了:


也就是說:antd 里大量用到了 Context,除了用來傳遞 config、theme、size 等全局配置信息外,還用來跨組件傳遞數(shù)據(jù),比如 Form、Form.Item 組件,就是通過 Provider、useContext 來存取值的。
React.Children、React.cloneElement
React 組件可以設(shè)置內(nèi)容,在組件內(nèi)通過 props.children 來取。
import?React?from?'react';
interface?GuangProps?{
??children:?React.ReactNode[];
}
const?Guang:?React.FunctionComponent<GuangProps>?=?(props)?=>?{
??console.log(props);
??return?<div?className="guang">
????{props.children}
??</div>
}
function?App()?{
??return?(
????<div?className="App">
??????<Guang>
????????<p>111</p>
????????<p>222</p>
??????</Guang>
????</div>
??);
}
export?default?App;
比如我在組件里把 props.children 取出來,放到 className 為 guang 的 div 下:


如果想對(duì)這些 children 做一些操作,就需要用 React.Children 的 api 了,比如 React.Children.toArray、React.Children.forEach、React.Children.map
有同學(xué)說,props.children 本來就是數(shù)組啊,直接操作不就行了?
不行的,直接操作有一些問題,比如我 sort 一下:

會(huì)報(bào)錯(cuò):

所以 props.children 不能直接當(dāng)做數(shù)組用,需要 toArray 一下:

這樣就沒有報(bào)錯(cuò)了:

同理,React.Children 的 forEach 和 map 也很容易理解。
而且還可以用 React.cloneElement 復(fù)制下傳入的 ReactElement。
比如這樣:

用 React.Children.map 遍歷 children,對(duì)每個(gè) child 復(fù)制一份出來,修改下 props ,并且添加一個(gè) children。
效果是這樣的:

React.cloneElement 的第二個(gè)參數(shù)是修改的 props,后面的參數(shù)是 children:

結(jié)合 React.Children 的 api 和 React.cloneElement 的 api 就可以任意修改 children 渲染的結(jié)果。
在 antd 里也有大量運(yùn)用:
比如 button 組件里,通過 map + cloneElement 來處理中文字符的問題:

或者用 map + cloneElement 給 child 的 children 外包一層組件:

更巧妙的是 VirtualList 里的應(yīng)用:

你不需要給傳入的 children 設(shè)置 ref,antd 會(huì)通過 map + cloneElement 給你加上 ref 的 props,然后在回調(diào)函數(shù)里把這個(gè) ref 保存下來。
這樣就拿到了你傳入的每一個(gè) children 的 ref。
比如根據(jù) key 來保存每個(gè) Item 的 ref:

也就是說:antd 組件里大量用到了 React.Children + React.cloneElement 的 api 對(duì) props.children 做一些修改,比如包一層組件、添加 ref 等參數(shù)、添加一些 children 等。
總結(jié)
這篇文章總結(jié)了 ant design 組件源碼里的 5 個(gè)技巧:
- 透?jìng)?className、style,還有其他 html 標(biāo)簽的 props,讓你的組件用起來體驗(yàn)和原生 html 標(biāo)簽一樣
- 通過 forwardRef + useImperativeHandle 暴露一些方法,每個(gè)組件都可以通過 ref 暴露一些 api 出來
- useCallback、useMemo 緩存計(jì)算結(jié)果,通過讓 props 不變來減少?zèng)]必要的渲染
- 用 Context 的 Provider + useContext 來跨組件傳遞值,可以用來傳遞全局配置,也可以用來做業(yè)務(wù)組件的跨層傳遞數(shù)據(jù)
- 通過 React.Children + React.cloneElement 的 api 對(duì) props.children 做各種修改
這些都是在 antd 里隨處可見的技巧,可以說任何一個(gè)組件里都有這些東西。
這些寫 React 組件的技巧你都用過么?沒用過的話不妨從今天開始用起來吧。
