前言
HOC(高階組件)是React中的一種組織代碼的手段,而不是一個API.
這種設計模式可以復用在React組件中的代碼與邏輯,因為一般來講React組件比較容易復用渲染函數, 也就是主要負責HTML的輸出.
高階組件實際上是經過一個包裝函數返回的組件,這類函數接收React組件處理傳入的組件,然后返回一個新的組件.
注意:前提是建立在不修改原有組件的基礎上.
文字描述太模糊,借助于官方文檔稍稍修改,我們可以更加輕松的理解高階組件.
具體的實施流程如下:
找出組件中復用的邏輯
創建適用于上方邏輯的函數
利用這個函數來創建一個組件
enjoy it
找出組件中復用的邏輯在實際開發中, 這種邏輯的組件非常常見:
組件創建
向服務器拉取數據
利用數據渲染組件
監聽數據的變化
數據變化或者觸發修改的事件
利用變化后的數據再次渲染
組件銷毀移除監聽的數據源
首先我們來創建一個生產假數據的對象來模擬數據源:
const fakeDataGenerator = ()=>({ timer: undefined, getData(){ return ["hello", "world"]; }, addChangeListener(handleChangeFun){ // 監聽數據產生鉤子 if(this.timer){ return; } this.timer = setInterval(()=> { handleChangeFun(); },2000) }, removeChangeListener(){ // 停止數據監聽 clearInterval(this.timer); } });
然后來編寫我們的組件A:
const FakeDataForA = fakeDataGenerator(); class A extends React.Component { constructor(props) {// 1 組件創建 super(props); this.state = { someData: fakeData.getData() // 1.1 向服務器拉取數據 } } handleFakeDataChange = ()=>{ this.setState({ someData:fakeData.getData() // 4. 數據變化或者觸發修改的事件 }); } componentDidMount(){ // 3. 監聽數據的變化 // 4. 數據變化或者觸發修改的事件 fakeData.addChangeListener(this.handleFakeDataChange); } componentWillUnmount(){ fakeData.removeChangeListener(); // 6. 組件銷毀移除監聽的數據源 } render() { return ( {/* 2. 利用數據渲染組件 5. 利用變化后的數據再次渲染 */} this.state.someData.map(name => ({name})) ) } } ReactDOM.render(, document.getElementById("root"));
然后我們再來創建一個組件B這個雖然渲染方式不同,但是數據獲取的邏輯是一致的.
在一般的開發過程中實際上也是遵循這個請求模式的,然后創建一個組件B:
const FakeDataForB = fakeDataGenerator(); class B extends React.Component { constructor(props) {// 1 組件創建 super(props); this.state = { someData: fakeData.getData() // 1.1 向服務器拉取數據 } } handleFakeDataChange = ()=>{ this.setState({ someData:fakeData.getData() // 4. 數據變化或者觸發修改的事件 }); } componentDidMount(){ // 3. 監聽數據的變化 // 4. 數據變化或者觸發修改的事件 fakeData.addChangeListener(this.handleFakeDataChange); } componentWillUnmount(){ fakeData.removeChangeListener(); // 6. 組件銷毀移除監聽的數據源 } render() { return ( {/* 2. 利用數據渲染組件 5. 利用變化后的數據再次渲染 */} this.state.someData.map(name => ({name})) ) } } ReactDOM.render(, document.getElementById("root"));
這里我把redner中原來渲染的span標簽改為了div標簽,雖然這是一個小小的變化但是請你腦補這是兩個渲染結果完全不同的組件好了.
這時候問題以及十分明顯了組件A和B明顯有大量的重復邏輯但是借助于React組件卻無法將這公用的邏輯來抽離.
在一般的開發中沒有這么完美重復的邏輯代碼,例如在生命周期函數中B組件可能多了幾個操作或者A組件數據源獲取的地址不同.
但是這里依然存在大量的可以被復用的邏輯.
這種函數的第一個參數接收一個React組件,然后返回這個組件:
function MyHoc(Wrap) { return class extends React.Component{ render(){} } }
就目前來說這個函數沒有任何實際功能只是將原有的組件包裝返回而已.
但是如果我們將組件A和B傳入到這個函數中,而使用返回的函數,我們可以得到了什么.
我們獲取了在原有的組件上的一層包裝,利用這層包裝我們可以把組件A和B的共同邏輯提取到這層包裝上.
我們來刪除組件A和B有關數據獲取以及修改的操作:
class A extends React.Component { componentDidMount(){ // 這里執行某些操作 假設和另外一個組件不同 } componentWillUnmount(){ // 這里執行某些操作 假設和另外一個組件不同 } render() { return ( this.state.data.map(name => ({name})) ) } } class B extends React.Component { componentDidMount(){ // 這里執行某些操作 假設和另外一個組件不同 } componentWillUnmount(){ // 這里執行某些操作 假設和另外一個組件不同 } render() { return ( this.state.data.map(name => ({name})) ) } }
然后將在這層包裝上的獲取到的外部數據使用props來傳遞到原有的組件中:
function MyHoc(Wrap) { return class extends React.Component{ constructor(props){ super(props); this.state = { data:fakeData // 假設這樣就獲取到了數據, 先不考慮其他情況 } } render(){ return{/* 通過 props 把獲取到的數據傳入 */} } } }
在這里我們在 HOC 返回的組件中獲取數據, 然后把數據傳入到內部的組件中, 那么數據獲取的這種功能就被多帶帶的拿了出來.
這樣組件A和B只要關注自己的 props.data 就可以了完全不需要考慮數據獲取和自身的狀態修改.
但是我們注意到了組件A和B原有獲取數據源不同,我們如何在包裝函數中處理?
這點好解決,利用函數的參數差異來抹消掉返回的高階組件的差異.
既然A組件和B組件的數據源不同那么這個函數就另外接收一個數據源作為參數好了.
并且我們將之前通用的邏輯放到了這個內部的組件上:
function MyHoc(Wrap,fakeData) { // 這次我們接收一個數據源 return class extends React.Component{ constructor(props){ super(props); this.state = { data: fakeData.getData() // 模擬數據獲取 } } handleDataChange = ()=>{ this.setState({ data:fakeData.getData() }); } componentDidMount() { fakeData.addChangeListener(this.handleDataChange); } componentWillUnmount(){ fakeData.removeChangeListener(); } render(){利用高階組件來創建組件} } }
經過上面的思考,實際上已經完成了99%的工作了,接下來就是完成剩下的1%,把它們組合起來.
偽代碼:
const FakeDataForA = FakeDataForAGenerator(), FakeDataForB = FakeDataForAGenerator(); // 兩個不同的數據源 function(Wrap,fakdData){ // 一個 HOC 函數 return class extends React.Components{}; } class A {}; // 兩個不同的組件 class B {}; // 兩個不同的組件 const AFromHoc = MyHoc(A,FakeDataForA), BFromHoc = MyHoc(B,FakeDataForB); // 分別把不同的數據源傳入, 模擬者兩個組件需要不同的數據源, 但是獲取數據邏輯一致
這個時候你就可以渲染自己的高階組件AFromHoc和BFromHoc了.
這兩個組件使用不同的數據源來獲取數據,通用的部分已經被抽離.
HOC函數需要像透明的一樣,經過他的包裝產生的新的組件和傳入前沒有什么區別.
這樣做的目的在于,我們不需要考慮經過HOC函數后的組件會產生什么變化而帶來額外的心智負擔.
如果你的HOC函數對傳入的組件進行了修改,那么套用這種HOC函數多次后返回的組件在使用的時候.
你不得不考慮這個組件帶來的一些非預期行為.
所以請不要將原本組件不需要的props傳入:
render() { // 過濾掉非此 HOC 額外的 props,且不要進行透傳 const { extraProp, ...passThroughProps } = this.props; // 將 props 注入到被包裝的組件中。 // 通常為 state 的值或者實例方法。 const injectedProp = someStateOrInstanceMethod; // 將 props 傳遞給被包裝組件 return (HOC是函數!利用函數來最大化組合性); }
因為HOC是一個返回組件的函數,只要是函數可以做的事情HOC同樣可以做到.
利用這一點,我們可以借用在使用React之前我們就已經學會的一些東西.
例如定義一個高階函數用于返回一個高階組件:
function HighLevelHoc(content) { return function (Wrap, className) { return class extends React.Component { render() { return ({content} ) } } } } class Test extends React.Component { render() { return ({this.props.children || "hello world"}
) } } const H1Test = HighLevelHoc("foobar")(Test, 1); ReactDOM.render(, document.getElementById("root"));
或者干脆是一個不接收任何參數的函數:
function DemoHoc(Wrap) { // 用于向 Wrap 傳入一個固定的字符串 return class extends React.Component{ render(){ return (注意 不要在 render 方法中使用 HOC{"hello world"} ) } } } function Demo(props) { return ({props.children}) } const App = DemoHoc(Demo); ReactDOM.render(, document.getElementById("root"));
我們都知道 React 會調用 render 方法來渲染組件, 當然 React 也會做一些額外的工作例如性能優化.
在組件重新渲染的時候 React 會判斷當前 render 返回的組件和未之前的組件是否相等 === 如果相等 React 會遞歸更新組件, 反之他會徹底的卸載之前的舊的版本來渲染當前的組件.
HOC每次返回的內容都是一個新的內容:
function Hoc(){ return {} } console.log( Hoc()===Hoc() ) // false
如果在 render 方法中使用:
render() { const DemoHoc = Hoc(MyComponent); // 每次調用 render 都會返回一個新的對象 // 這將導致子樹每次渲染都會進行卸載,和重新掛載的操作! return記得復制靜態方法; }
React 的組件一般是繼承 React.Component 的子類.
不要忘記了一個類上除了實例方法外還有靜態方法, 使用 HOC 我們對組件進行了一層包裝會覆蓋掉原來的靜態方法:
class Demo extends React.Component{ render(){ return ({this.props.children}) } } Demo.echo = function () { console.log("hello world"); } Demo.echo();// 是可以調用的 // -------- 定一個類提供一個靜態方法 function DemoHoc(Wrap) { return class extends React.Component{ render(){ return ({"hello world"} ) } } } const App = DemoHoc(Demo); // ----- HOC包裝這個類 App.echo(); // error 這個靜態方法不見了
解決方式
在 HOC 內部直接將原來組件的靜態方法復制就可以了:
function DemoHoc(Wrap) { const myClass = class extends React.Component{ render(){ return ({"hello world"} ) } } myClass.echo = Wrap.echo; return myClass; }
不過這樣一來 HOC 中就需要知道被復制的靜態方法名是什么, 結合之前提到的靈活使用 HOC 我們可以讓 HOC 接收靜態方法參數名稱:
function DemoHoc(Wrap,staticMethods=[]) { // 默認空數組 const myClass = class extends React.Component{ render(){ return ({"hello world"} ) } } for (const methodName of staticMethods) { // 循環復制 myClass[methodName] = Wrap[methodName]; } return myClass; } // ----- const App = DemoHoc(Demo,["echo"]);
此外一般我們編寫組件的時候都是一個文件對應一個組件, 這時候我們可以把靜態方法導出.
HOC 不拷貝靜態方法, 而是需要這些靜態方法的組件直接引入就好了:
來自官方文檔
// 使用這種方式代替... MyComponent.someFunction = someFunction; export default MyComponent; // ...多帶帶導出該方法... export { someFunction }; // ...并在要使用的組件中,import 它們 import MyComponent, { someFunction } from "./MyComponent.js";透傳 ref
ref 作為組件上的特殊屬性, 無法像普通的 props 那樣被向下傳遞.
例如我們有一個組件, 我們想使用 ref 來引用這個組件并且試圖調用它的 echo 方法:
class Wraped extends React.Component{ constructor(props){ super(props); this.state = { message:"" } } echo(){ this.setState({ message:"hello world" }); } render(){ return{this.state.message}} }
我們使用一個 HOC 包裹它:
function ExampleHoc(Wrap) { return class extends React.Component{ render(){ return} } } const Example = ExampleHoc(Wraped); // 得到了一個高階組件
現在我們把這個組件放入到 APP 組件中進行渲染, 并且使用 ref 來引用這個返回的組件, 并且試圖調用它的 echo 方法:
const ref = React.createRef(); class App extends React.Component { handleEcho = () => { ref.current.echo(); } render() { return () } }{/* 點擊按鈕相當于執行echo */}
但是當你點擊按鈕試圖觸發子組件的事件的時候它不會起作用, 系統報錯沒有 echo 方法.
實際上 ref 被綁定到了 HOC 返回的那個匿名類上, 想要綁定到內部的組件中我們可以進行 ref 透傳.
默認的情況下 ref 是無法被進行向下傳遞的因為 ref 是特殊的屬性就和 key 一樣不會被添加到 props 中, 因此 React 提供了一個 API 來實現透傳 ref 的這種需求.
這個 API 就是 React.forwardRef.
這個方法接收一個函數返回一個組件, 在這個含中它可以讀取到組件傳入的 ref , 某種意義上 React.forwardRef 也相當于一個高階組件:
const ReturnedCompoent = React.forwardRef((props, ref) => { // 我們可以獲取到在props中無法獲取的 ref 屬性了 return // 返回這個需要使用 ref 屬性的組件 });
我們把這個 API 用在之前的 HOC 中:
function ExampleHoc(Wrap) { class Inner extends React.Component { render() { const { forwardedRef,...rest} = this.props; return// 2. 我們接收到 props 中被改名的 ref 然后綁定到 ref 上 } } return React.forwardRef((props,ref)=>{ // 1. 我們接收到 ref 然后給他改名成 forwardedRef 傳入到props中 return }) }
這個時候在調用 echo 就沒有問題了:
handleEcho = () => { ref.current.echo(); }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/109694.html
摘要:作用是給組件增減屬性。如果你的高階組件不需要帶參數,這樣寫也是很的。那么需要建立一個引用可以對被裝飾的組件做羞羞的事情了,注意在多個高階組件裝飾同一個組件的情況下,此法并不奏效。你拿到的是上一個高階組件的函數中臨時生成的組件。 是什么 簡稱HOC,全稱 High Order Component。作用是給react組件增減props屬性。 怎么用 為什么不先說怎么寫?恩,因為你其實已經用...
摘要:原文鏈接高階組件在中是組件復用的一個強大工具。在本文中,高階組件將會被分為兩種基本模式,我們將其命名為和用附加的功能來包裹組件。這里我們使用泛型表示傳遞到的組件的。在這里,我們定義從返回的組件,并指定該組件將包括傳入組件的和的。 原文鏈接:https://medium.com/@jrwebdev/... 高階組件(HOCs)在React中是組件復用的一個強大工具。但是,經常有開發者在...
摘要:與繼承相比,裝飾者是一種更輕便靈活的做法。它只是一種模式,這種模式是由自身的組合性質必然產生的。對比原生組件增強的項可操作所有傳入的可操作組件的生命周期可操作組件的方法獲取反向繼承返回一個組件,繼承原組件,在中調用原組件的。 導讀 前端發展速度非常之快,頁面和組件變得越來越復雜,如何更好的實現狀態邏輯復用一直都是應用程序中重要的一部分,這直接關系著應用程序的質量以及維護的難易程度。 本...
摘要:高階函數我們都用過,就是接受一個函數然后返回一個經過封裝的函數而高階組件就是高階函數的概念應用到高階組件上使用接受一個組件返回一個經過包裝的新組件。靈活性在組合階段相對更為靈活,他并不規定被增強組件如何使用它傳遞下去的屬性。 在接觸過React項目后,大多數人都應該已經了解過或則用過了HOC(High-Order-Components)和FaCC(Functions as Child ...
摘要:總結其實,這個和的思想有很大的淵源,不推薦繼承,而是推薦組合,而就是其中的典范。比如我們寫了兩個個高階組件,一個是,一個是,組件就可以隨意的在和之間隨意切換,而不需要改動組件原有代碼。 0x000 概述 高階函數組件...還是一個函數,和函數組件不同的是他返回了一個完整的組件...他返回了一個class!!! 0x001 直接上栗子 照常,先寫個App組件,外部傳入一個theme ...
閱讀 2818·2023-04-25 15:01
閱讀 3049·2021-11-23 10:07
閱讀 3364·2021-10-12 10:12
閱讀 3455·2021-08-30 09:45
閱讀 2194·2021-08-20 09:36
閱讀 3586·2019-08-30 12:59
閱讀 2431·2019-08-26 13:52
閱讀 934·2019-08-26 13:24