<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在日常的開發中,開發通用元件的機會其實並不多,尤其是在各種元件庫已經遍地都是的情況下。而作為一個通用元件庫的使用者,經常會看到把 React 元件作為引數傳遞下去的場景,每當這個時候,其實或多或少都會有一些疑問,比如:有些元件傳遞下去的是元件名,而有些元件傳遞下去的是一個箭頭函數返回一個元件,而有些直接傳遞一個 jsx 建立好的元素,這些傳遞方案的適用場景如何,有什麼不同,是否會導致元件的 memo 失效,是否會引發元件的不必要渲染?
本文是筆者在閱讀了 antd、mui, react-select 的 api 之後,結合自己日常業務中使用的元件 api 格式,對傳遞一個元件作為 React 元件引數的方式的思考和總結,如果有寫的不到位的,歡迎補充和指點。
大體來講,傳遞元件的方式,分為三種:
下文也主要展開介紹這三種方式並結合實際場景對比這三種方案。
在 antd 的元件 api 中,最常見的方式便是這個方法,以 button 為例,有一個 icon 引數便是允許使用者傳遞一個經過 jsx 建立好的元素。簡化後的範例如下:
function DownloadOutlined() { return /* icon 的實現*/; } function Button({ icon, children }) { return <button> {icon} {children} </button> } function App() { return <Button icon={<DownloadOutlined />}>test</Button> }
可以看出來,icon 直接傳遞了一個 jsx 建立好的元件,從而滿足了使用者自定義 icon 的需求。
相比於通過字串列舉內建 icon, 給了使用者更大的客製化空間。
這一用法在 antd 中很少出現,在 react-select 中比較常見。
這裡為了方便還是以 Button 為例,修改下上文的 Button 元件,將其引數改為傳遞 DownloadOutlined
而非經過 jsx 建立好的元素 <DownloadOutlined />
function DownloadOutlined() { return /* icon 的實現*/; } function Button({ icon: Icon, children }) { return <button> // 渲染方式進行了改變 <Icon /> {children} </Button> } function App() { return <Button icon={DownloadOutlined}>test</Button> }
通過直接傳遞元件本身的方式,也可將其傳遞給子元件進行渲染,當然,子元件渲染的地方也改成了 <Icon />
而非上文的 {icon}
。ps: 上文中由於 jsx 語法要求,將 icon 變數名改成了首字母大寫的 Icon。
這一用法用 Button 範例改寫如下:
function DownloadOutlined() { return /* icon 的實現*/; } function Button({ icon, children }) { return <button> // 渲染方式進行了改變 {icon()} {children} </Button> } function App() { return <Button icon={() => <DownloadOutlined />}>test</Button> }
在這一例子中,由於傳遞的是個函數,那麼返回值在渲染時,改成執行函數即可。
上文中分別介紹了這三種方案的實現方法,從結果來看,三種方案都能滿足傳遞元件作為元件引數的場景。
但是在實際的場景中,往往不會這麼簡單,往往有更多需要考慮的情況。
情況一: 考慮是否存在不必要的渲染?
三種方案下,當父元件發生渲染時,Button 元件是否會發生不必要的渲染。範例如下:
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,此時,期望的最小渲染應該是僅僅渲染 app 元件即可,Button1 - Button3 由於並未依賴 count 的變化,同時 Button1 - Button3 都通過 React.memo 進行包裹,期望的是元件不進行渲染。
實際輸出如下:
可以看出,Button1 和 Button3 均進行了渲染,這是由於這兩種方案下,icon的引數發生了變化,對於 Button1, <DownloadOutlined />
, 本質是 React.createElement(DownloadOutlined)
, 此時將會返回一個新的參照,就導致了 Button1 引數的改變,從而使得其會重新渲染。而對於 Button3,就更加明顯,每次渲染後返回的箭頭函數都是新的,自然也會引發渲染。而只有方案二,由於返回的始終是元件的參照,故不會重新渲染。
要避免(雖然實際中,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包裹後,即可實現 Button1, Button3 元件引數的不變,從而避免了多餘的渲染。相比之下,目前看,直接傳遞元件本身的方案寫法似乎更為簡單。
實際的場景中,Icon 元件往往不會如此簡單,往往會有一些引數來控制其比如顏色、點選行為以及大小等等,此時,要將這些引數傳遞給 Icon 元件,這也是筆者想要討論的:
情況二:需要傳遞來自父元件(App)的引數的情況。
在現有的基礎上, 以傳遞 size 到 Icon 元件為例,改造如下:
import React, { useState, useMemo, useCallback } from 'react'; // 增加 size 引數, 控制 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> </> ); }
通過上述改動,可以發現,當需要從 App 元件中,向 Icon 傳遞引數時,Button1 和 Button3 元件本身不需要做任何改動,僅僅需要修改 Icon jsx建立時的引數即可,而 Button2 的 Icon 由於渲染髮生在內部,故需要額外傳遞 iconProps 作為引數傳遞給 Icon。與此同時,render按鈕點選時,由於 iconProps 是個參照型別,導致觸發了 Button2 的額外渲染,當然可以通過 useMemo 來控制,此處不再贅述。
接下來看情況三,當子元件(Button1 - button3)需要傳遞它自身內部的狀態到 Icon 元件中時,需要做什麼改動。
設想一個虛構的需求, Button1 - Button3 元件內部維護了一個狀態,count,也就是每個元件點選的次數,而 DownloadOutlined
也接收一個引數,count, 隨著 count 的變化,他的顏色會從 rbg(0, 0, 0)
變化為 rgb(count, 0, 0)
。
DownloadOutlined 改動如下:
// 增加 count 引數,控制 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引數注入即可 */} <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 作為引數傳遞給 icon 函數 */} {icon({count})} {children} </button> ); });
相應的,App 元件傳入也需要做改動
export default function App() { /* 省略 */ const button3Icon = useCallback((props) => { // 接收引數並將其傳遞給icon元件 return <DownloadOutlined size={size} {...props} />; }, [size]); /* 省略 */ }
而對於 button1, 由於 icon 渲染的時機,是在 App 元件中,而在 App 元件中,獲取 Button1 元件內部的狀態並不方便(可以通過 ref, 但是略顯麻煩)。此時可以藉助 React.cloneElement
api來新建一個 Icon 元件並將子元件引數注入,改造如下:
const Button1 = React.memo(({ icon, children }) => { console.log('button1 render'); const [count, setCount] = useState(0); // 藉助 cloneElement 向icon 注入引數 const newIcon = React.cloneElement(icon, { count, }); return ( <button onClick={() => setCount((pre) => pre + 40)}> {newIcon} {children} </button> ); });
從這個例子可以看出,如果傳入的元件(icon),需要獲取即將傳入元件(Button1, Button2, Button3)內部的狀態,那麼直接傳遞 jsx 建立好的元素,並不方便,因為在父元件(App)中獲取子元件(Button1)內部的狀態並不方便,而直接傳遞元件本身,和傳遞返回 jsx 建立元素的函數,前者由於元素真正的建立,就是發生在子元件內部,故可以方便的獲取子元件狀態,而後者由於是函數式的建立,通過簡單的引數傳遞,即可將內部引數傳入 icon 中,從而方便的實現響應的需求。
本文先簡單介紹了三種將元件作為引數傳遞的方案:
icon = {<Icon />}
icon={Icon}
icon={() => <Icon />}
接下來,從三個角度對其進行分析:
其中,三種方案,在不做 useMemo, useCallback 這樣的快取情況下,直接傳遞元件本身,由於參照不變,可以直接避免非必要渲染,但是當需要接收來自父元件的引數時,需要開闢額外的欄位 iconProps 來接收父元件的引數,在不做快取的情況下,由於引數的物件參照每次都會更新從而也存在不必要渲染的情況。當然,這種不必要的渲染,在絕大部分場景下,並不會存在效能問題。
考慮了來自父元件的傳參後,除了方案二直接傳遞元件本身的方案需要對子元件增加 iconProps 之外,其餘兩個方案由於 jsx 建立元件元素的寫法本身就在父元件中,只需稍作改動即可將引數攜帶入 Icon 元件中。
而當需要接收來自子元件的引數場景下,方案一顯得略有不足,jsx 的建立在父元件已經建立好,子元件中需要注入額外的引數相對麻煩(使用 cloneElement 實現引數注入)。而方案三由於函數的執行時機是在子元件內部,可以很方便的將引數通過函數傳參帶入 Icon 元件,可以很方便的滿足需求。
從實際開發元件的場景來看,被作為引數傳遞的元件需要使用子元件內部引數的,一般通過方案三傳遞函數的方案來設計,而不需要子元件內部引數的,方案一二三均可,實際的開銷幾乎沒有差異,只能說方案一寫法較為簡單,也是 antd 的 api 中最常見的用法。而方案三,多見於需要子元件內部狀態的情況,比如 antd 的麵包屑 itemRender,Form.list的 children 的渲染,通過函數注入引數給被作為引數傳遞的元件方便靈活的進行渲染。
最後,由於筆者之前寫過一段時間vue,不免還是想到了 vue 中 slot 的寫法,說實話,還是回去翻了下檔案,其實就是方案一和方案三的合集,由於slot本身是在父元件渲染的,所以直接具備父元件的作用域,能夠存取父元件的狀態,需要注入父元件引數的,直接在插槽的元件中使用即可,而作用域插槽便是提供子元件的作用域,使插槽中的元件可以獲取到子元件的引數。
到此這篇關於React將元件作為引數進行傳遞的3種方法的文章就介紹到這了,更多相關React元件作引數傳遞內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45