React將組件作為參數(shù)進行傳遞的3種方法實例
前言
在日常的開發(fā)中,開發(fā)通用組件的機會其實并不多,尤其是在各種組件庫已經(jīng)遍地都是的情況下。而作為一個通用組件庫的使用者,經(jīng)常會看到把 React 組件作為參數(shù)傳遞下去的場景,每當(dāng)這個時候,其實或多或少都會有一些疑問,比如:有些組件傳遞下去的是組件名,而有些組件傳遞下去的是一個箭頭函數(shù)返回一個組件,而有些直接傳遞一個 jsx 創(chuàng)建好的元素,這些傳遞方案的適用場景如何,有什么不同,是否會導(dǎo)致組件的 memo 失效,是否會引發(fā)組件的不必要渲染?
本文是筆者在閱讀了 antd、mui, react-select 的 api 之后,結(jié)合自己日常業(yè)務(wù)中使用的組件 api 格式,對傳遞一個組件作為 React 組件參數(shù)的方式的思考和總結(jié),如果有寫的不到位的,歡迎補充和指點。
大體來講,傳遞組件的方式,分為三種:
- 傳遞 jsx 創(chuàng)建好的元素
- 傳遞組件本身
- 傳遞返回 jsx 創(chuàng)建好的元素的函數(shù)
下文也主要展開介紹這三種方式并結(jié)合實際場景對比這三種方案。
方式一:直接傳遞 jsx 創(chuàng)建好的元素
在 antd 的組件 api 中,最常見的方式便是這個方法,以 button 為例,有一個 icon 參數(shù)便是允許使用者傳遞一個經(jīng)過 jsx 創(chuàng)建好的元素。簡化后的示例如下:
function DownloadOutlined() { return /* icon 的實現(xiàn)*/; } function Button({ icon, children }) { return <button> {icon} {children} </button> } function App() { return <Button icon={<DownloadOutlined />}>test</Button> }
可以看出來,icon 直接傳遞了一個 jsx 創(chuàng)建好的組件,從而滿足了用戶自定義 icon 的需求。
相比于通過字符串枚舉內(nèi)置 icon, 給了用戶更大的定制空間。
方式二:直接傳遞組件本身
這一用法在 antd 中很少出現(xiàn),在 react-select 中比較常見。
這里為了方便還是以 Button 為例,修改下上文的 Button 組件,將其參數(shù)改為傳遞 DownloadOutlined
而非經(jīng)過 jsx 創(chuàng)建好的元素 <DownloadOutlined />
function DownloadOutlined() { return /* icon 的實現(xiàn)*/; } function Button({ icon: Icon, children }) { return <button> // 渲染方式進行了改變 <Icon /> {children} </Button> } function App() { return <Button icon={DownloadOutlined}>test</Button> }
通過直接傳遞組件本身的方式,也可將其傳遞給子組件進行渲染,當(dāng)然,子組件渲染的地方也改成了 <Icon />
而非上文的 {icon}
。ps: 上文中由于 jsx 語法要求,將 icon 變量名改成了首字母大寫的 Icon。
方式三:傳遞一個返回組件的函數(shù)
這一用法用 Button 示例改寫如下:
function DownloadOutlined() { return /* icon 的實現(xiàn)*/; } function Button({ icon, children }) { return <button> // 渲染方式進行了改變 {icon()} {children} </Button> } function App() { return <Button icon={() => <DownloadOutlined />}>test</Button> }
在這一例子中,由于傳遞的是個函數(shù),那么返回值在渲染時,改成執(zhí)行函數(shù)即可。
三種方案的對比
上文中分別介紹了這三種方案的實現(xiàn)方法,從結(jié)果來看,三種方案都能滿足傳遞組件作為組件參數(shù)的場景。
但是在實際的場景中,往往不會這么簡單,往往有更多需要考慮的情況。
情況一: 考慮是否存在不必要的渲染?
三種方案下,當(dāng)父組件發(fā)生渲染時,Button 組件是否會發(fā)生不必要的渲染。示例如下:
import React, { useState } from 'react'; function DownloadOutlined() { return <span>icon</span>; } const Button1 = React.memo(({ icon, children }) => { console.log('button1 render'); return ( <button> {icon} {children} </button> ); }); const Button2 = React.memo(({ icon: Icon, children }) => { console.log('button2 render'); return ( <button> <Icon /> {children} </button> ); }); const Button3 = React.memo(({ icon, children }) => { console.log('button3 render'); return ( <button> {icon()} {children} </button> ); }); export default function App() { const [count, setCount] = useState(0); console.log('App render'); return ( <> <Button1 icon={<DownloadOutlined />}>button1</Button1> <Button2 icon={DownloadOutlined}>button2</Button2> <Button3 icon={() => <DownloadOutlined />}>button3</Button3> <button onClick={() => setCount((pre) => pre + 1)}>render</button> </> ); }
在該示例中,點擊 render button,此時,期望的最小渲染應(yīng)該是僅僅渲染 app 組件即可,Button1 - Button3 由于并未依賴 count 的變化,同時 Button1 - Button3 都通過 React.memo 進行包裹,期望的是組件不進行渲染。
實際輸出如下:
可以看出,Button1 和 Button3 均進行了渲染,這是由于這兩種方案下,icon的參數(shù)發(fā)生了變化,對于 Button1, <DownloadOutlined />
, 本質(zhì)是 React.createElement(DownloadOutlined)
, 此時將會返回一個新的引用,就導(dǎo)致了 Button1 參數(shù)的改變,從而使得其會重新渲染。而對于 Button3,就更加明顯,每次渲染后返回的箭頭函數(shù)都是新的,自然也會引發(fā)渲染。而只有方案二,由于返回的始終是組件的引用,故不會重新渲染。
要避免(雖然實際中,99%的場景都不需要避免,也不會有性能問題)這種情況,可以通過加 memo 解決。改動點如下:
export default function App() { const [count, setCount] = useState(0); console.log('App render'); const button1Icon = useMemo(() => { return <DownloadOutlined />; }, []); const button3Icon = useCallback(() => { return () => <DownloadOutlined />; }, []); return ( <> <Button1 icon={butto1Icon}>button1</Button1> <Button2 icon={DownloadOutlined}>button2</Button2> <Button3 icon={button3Icon}>button3</Button3> <button onClick={() => setCount((pre) => pre + 1)}>render</button> </> ); }
通過 useMemo, useCallback包裹后,即可實現(xiàn) Button1, Button3 組件參數(shù)的不變,從而避免了多余的渲染。相比之下,目前看,直接傳遞組件本身的方案寫法似乎更為簡單。
實際的場景中,Icon 組件往往不會如此簡單,往往會有一些參數(shù)來控制其比如顏色、點擊行為以及大小等等,此時,要將這些參數(shù)傳遞給 Icon 組件,這也是筆者想要討論的:
情況二:需要傳遞來自父組件(App)的參數(shù)的情況。
在現(xiàn)有的基礎(chǔ)上, 以傳遞 size 到 Icon 組件為例,改造如下:
import React, { useState, useMemo, useCallback } from 'react'; // 增加 size 參數(shù), 控制 icon 大小 function DownloadOutlined({ size }) { return <span style={{ fontSize: `${size}px` }}>icon</span>; } // 無需修改 const Button1 = React.memo(({ icon, children }) => { console.log('button1 render'); return ( <button> {icon} {children} </button> ); }); // 增加 iconProps,來傳遞給 Icon 組件 const Button2 = React.memo(({ icon: Icon, children, iconProps = {} }) => { console.log('button2 render'); return ( <button> <Icon {...iconProps} /> {children} </button> ); }); // 無需修改 const Button3 = React.memo(({ icon, children }) => { console.log('button3 render'); return ( <button> {icon()} {children} </button> ); }); export default function App() { const [count, setCount] = useState(0); const [size, setSize] = useState(12); console.log('App render'); // 增加size依賴 const button1Icon = useMemo(() => { return <DownloadOutlined size={size} />; }, [size]); // 增加size依賴 const button3Icon = useCallback(() => { return <DownloadOutlined size={size} />; }, [size]); return ( <> <Button1 icon={button1Icon}>button1</Button1> <Button2 icon={DownloadOutlined} iconProps={{ size }}> button2 </Button2> <Button3 icon={button3Icon}>button3</Button3> <button onClick={() => setCount((pre) => pre + 1)}>render</button> <button onClick={() => setSize((pre) => pre + 1)}>addSize</button> </> ); }
通過上述改動,可以發(fā)現(xiàn),當(dāng)需要從 App 組件中,向 Icon 傳遞參數(shù)時,Button1 和 Button3 組件本身不需要做任何改動,僅僅需要修改 Icon jsx創(chuàng)建時的參數(shù)即可,而 Button2 的 Icon 由于渲染發(fā)生在內(nèi)部,故需要額外傳遞 iconProps 作為參數(shù)傳遞給 Icon。與此同時,render按鈕點擊時,由于 iconProps 是個引用類型,導(dǎo)致觸發(fā)了 Button2 的額外渲染,當(dāng)然可以通過 useMemo 來控制,此處不再贅述。
接下來看情況三,當(dāng)子組件(Button1 - button3)需要傳遞它自身內(nèi)部的狀態(tài)到 Icon 組件中時,需要做什么改動。
設(shè)想一個虛構(gòu)的需求, Button1 - Button3 組件內(nèi)部維護了一個狀態(tài),count,也就是每個組件點擊的次數(shù),而 DownloadOutlined
也接收一個參數(shù),count, 隨著 count 的變化,他的顏色會從 rbg(0, 0, 0)
變化為 rgb(count, 0, 0)
。
DownloadOutlined 改動如下:
// 增加 count 參數(shù),控制 icon 顏色 function DownloadOutlined({ size = 12, count = 0 }) { console.log(count); return ( <span style={{ fontSize: `${size}px`, color: `rgb(${count}, 0, 0)` }}> icon </span> ); }
Button2 的改造(Button1放在最后)如下:
const Button2 = React.memo(({ icon: Icon, children, iconProps = {} }) => { console.log('button2 render'); const [count, setCount] = useState(0); return ( <button onClick={() => setCount(pre => pre + 40)}> {/* 將count參數(shù)注入即可 */} <Icon {...iconProps} count={count} /> {children} </button> ); });
Button3的改造如下:
const Button3 = React.memo(({ icon, children }) => { console.log('button3 render'); const [count, setCount] = useState(0); return ( // 此處為了放大顏色的改變,點擊一次加 40 <button onClick={() => setCount(pre => pre + 40)}> {/* 將 count 作為參數(shù)傳遞給 icon 函數(shù) */} {icon({count})} {children} </button> ); });
相應(yīng)的,App 組件傳入也需要做改動
export default function App() { /* 省略 */ const button3Icon = useCallback((props) => { // 接收參數(shù)并將其傳遞給icon組件 return <DownloadOutlined size={size} {...props} />; }, [size]); /* 省略 */ }
而對于 button1, 由于 icon 渲染的時機,是在 App 組件中,而在 App 組件中,獲取 Button1 組件內(nèi)部的狀態(tài)并不方便(可以通過 ref, 但是略顯麻煩)。此時可以借助 React.cloneElement
api來新建一個 Icon 組件并將子組件參數(shù)注入,改造如下:
const Button1 = React.memo(({ icon, children }) => { console.log('button1 render'); const [count, setCount] = useState(0); // 借助 cloneElement 向icon 注入?yún)?shù) const newIcon = React.cloneElement(icon, { count, }); return ( <button onClick={() => setCount((pre) => pre + 40)}> {newIcon} {children} </button> ); });
從這個例子可以看出,如果傳入的組件(icon),需要獲取即將傳入組件(Button1, Button2, Button3)內(nèi)部的狀態(tài),那么直接傳遞 jsx 創(chuàng)建好的元素,并不方便,因為在父組件(App)中獲取子組件(Button1)內(nèi)部的狀態(tài)并不方便,而直接傳遞組件本身,和傳遞返回 jsx 創(chuàng)建元素的函數(shù),前者由于元素真正的創(chuàng)建,就是發(fā)生在子組件內(nèi)部,故可以方便的獲取子組件狀態(tài),而后者由于是函數(shù)式的創(chuàng)建,通過簡單的參數(shù)傳遞,即可將內(nèi)部參數(shù)傳入 icon 中,從而方便的實現(xiàn)響應(yīng)的需求。
總結(jié)
本文先簡單介紹了三種將組件作為參數(shù)傳遞的方案:
- 傳遞 jsx 創(chuàng)建好的元素:
icon = {<Icon />}
- 傳遞組件本身:
icon={Icon}
- 傳遞返回 jsx 創(chuàng)建好的元素的函數(shù):
icon={() => <Icon />}
接下來,從三個角度對其進行分析:
- 是否存在不必要的渲染
- Icon 組件需要接收來自父組件的參數(shù)
- Icon 組件需要接收來自子組件的參數(shù)
其中,三種方案,在不做 useMemo, useCallback 這樣的緩存情況下,直接傳遞組件本身,由于引用不變,可以直接避免非必要渲染,但是當(dāng)需要接收來自父組件的參數(shù)時,需要開辟額外的字段 iconProps 來接收父組件的參數(shù),在不做緩存的情況下,由于參數(shù)的對象引用每次都會更新從而也存在不必要渲染的情況。當(dāng)然,這種不必要的渲染,在絕大部分場景下,并不會存在性能問題。
考慮了來自父組件的傳參后,除了方案二直接傳遞組件本身的方案需要對子組件增加 iconProps 之外,其余兩個方案由于 jsx 創(chuàng)建組件元素的寫法本身就在父組件中,只需稍作改動即可將參數(shù)攜帶入 Icon 組件中。
而當(dāng)需要接收來自子組件的參數(shù)場景下,方案一顯得略有不足,jsx 的創(chuàng)建在父組件已經(jīng)創(chuàng)建好,子組件中需要注入額外的參數(shù)相對麻煩(使用 cloneElement 實現(xiàn)參數(shù)注入)。而方案三由于函數(shù)的執(zhí)行時機是在子組件內(nèi)部,可以很方便的將參數(shù)通過函數(shù)傳參帶入 Icon 組件,可以很方便的滿足需求。
從實際開發(fā)組件的場景來看,被作為參數(shù)傳遞的組件需要使用子組件內(nèi)部參數(shù)的,一般通過方案三傳遞函數(shù)的方案來設(shè)計,而不需要子組件內(nèi)部參數(shù)的,方案一二三均可,實際的開銷幾乎沒有差異,只能說方案一寫法較為簡單,也是 antd 的 api 中最常見的用法。而方案三,多見于需要子組件內(nèi)部狀態(tài)的情況,比如 antd 的面包屑 itemRender,F(xiàn)orm.list的 children 的渲染,通過函數(shù)注入?yún)?shù)給被作為參數(shù)傳遞的組件方便靈活的進行渲染。
最后,由于筆者之前寫過一段時間vue,不免還是想到了 vue 中 slot 的寫法,說實話,還是回去翻了下文檔,其實就是方案一和方案三的合集,由于slot本身是在父組件渲染的,所以直接具備父組件的作用域,能夠訪問父組件的狀態(tài),需要注入父組件參數(shù)的,直接在插槽的組件中使用即可,而作用域插槽便是提供子組件的作用域,使插槽中的組件可以獲取到子組件的參數(shù)。
到此這篇關(guān)于React將組件作為參數(shù)進行傳遞的3種方法的文章就介紹到這了,更多相關(guān)React組件作參數(shù)傳遞內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React為什么需要Scheduler調(diào)度器原理詳解
這篇文章主要為大家介紹了React為什么需要Scheduler調(diào)度器原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10react-router-dom6(對比?router5)快速入門指南
這篇文章主要介紹了快速上手react-router-dom6(對比?router5),通過本文學(xué)習(xí)最新的react-router-dom?v6版本的路由知識,并且會與v5老版本進行一些對比,需要的朋友可以參考下2022-08-08React利用插件和不用插件實現(xiàn)雙向綁定的方法詳解
我們知道在 angular 中數(shù)據(jù)時雙向綁定的;而在 react 中,數(shù)據(jù)是向一個方向傳遞:從擁有者到子節(jié)點。也就是我們說的單向數(shù)據(jù)綁定。那如何實現(xiàn)雙向綁定呢?下面這篇文章主要給大家介紹了關(guān)于React利用插件和不用插件實現(xiàn)雙向綁定的方法,需要的朋友可以參考下。2017-07-07React實現(xiàn)數(shù)字滾動組件numbers-scroll的示例詳解
數(shù)字滾動組件,也可以叫數(shù)字輪播組件,這個名字一聽就是非常普通常見的組件。本文將利用React實現(xiàn)這一組件,感興趣的小伙伴可以了解一下2023-03-03