爲了實現分離業務邏輯代碼,實現組件內部相關業務邏輯的複用,在React的迭代中針對類組件中的代碼複用依次發佈了Mixin、HOC、Render props等幾個方案。此外,針對函數組件,在React v16.7.0-alpha 中提出了hooks的概念,在自己無狀態的函數組件,引入獨立的狀態空間,也就是說在函數組件中,也能夠引入類組件中的state和組件生命週期,使得函數組件變得豐富多彩起來,此外,hooks也保證了邏輯代碼的複用性和獨立性。html
本文從針對類組件的複用解決方案開始提及,前後介紹了從Mixin、HOC到Render props的演進,最後介紹了React v16.7.0-alpha 中的 hooks以及自定義一個hooksreact
- Mixin
- HOC
- Render props
- React hooks的介紹以及如何自定義一個hooks
原文地址在個人博客中:https://github.com/forthealll...git
歡迎star和fork~github
Mixin是最先出現的複用類組件中業務邏輯代碼的解決方案,首先來介紹如下如何適應Mixin。下面是一個Mixin的例子:npm
const someMixins={ printColor(){ console.log(this.state.color); } setColor(newColor){ this.setState({color:newColor}) } componentDidMount(){ .. } }
下面是一個使用Mixin的組件:編程
class Apple extends React.Component{ //僅僅做爲演示,mixins通常是經過React.createClass建立,而且ES6中沒有這種寫法 mixins:[someMixins] constructor(props){ super(props); this.state={ color:'red' } this.printColor=this.printColor.bind(this); } render(){ return <div className="m-box" onClick={this.printColor}> 這是一個蘋果 </div> } }
在類中mixin引入公共業務邏輯:redux
mixins:[someMixins]
從上面的例子,咱們來總結如下mixin的缺點:數組
Mixin已經被廢除,具體缺陷能夠參考Mixins Considered Harmfuldom
爲了解決Mixin的缺陷,第二種解決方案是高階組件(high order component,簡稱HOC)。ide
HOC簡單理解就是組件工廠,接受原始組件做爲參數,添加完功能與業務後,返回新的組件。下面來介紹HOC參數的幾個例子。
const redApple = withFruit(Apple);
const redApple = withFruit(Apple,{color:'red',weight:'200g'});
可是這種狀況比較少用,若是對象中僅僅傳遞的是屬性,其實徹底能夠經過組件的props實現值的傳遞,咱們用HOC的主要目的是分離業務,關於UI的展現,以及一些組件中的屬性和狀態,咱們通常經過props來指定比較方便
const redApp=withFruit(App,()=>{console.log('I am a fruit')})
最多見的是僅以一個原始組件做爲參數,可是在外層包裹了業務邏輯,好比react-redux的conect函數中:
class Admin extends React.Component{ } const mapStateToProps=(state)=>{ return { }; } const mapDispatchToProps=(dispatch)=>{ return { } } const connect(mapStateToProps,mapDispatchToProps)(Admin)
HOC解決了Mixin的一些缺陷,可是HOC自己也有一些缺點:
若是原始組件A,前後經過工廠函數1,工廠函數2,工廠函數3….構造,最後生成了組件B,咱們知道組件B中有不少與A組件不一樣的props,可是咱們僅僅經過組件B,並不能知道哪一個組件來自於哪一個工廠函數。同時,若是有2個工廠函數同時修改了組件A的某個同名屬性,那麼會有屬性覆蓋的問題,會使得前一個工廠函數的修改結果失效。
所謂靜態構建,也就是說生成的是一個新的組件,並不會立刻render,HOC組件工廠是靜態構建一個組件,這相似於從新聲明一個組件的部分。也就是說,HOC工廠函數裏面的聲明周期函數,也只有在新組件被渲染的時候纔會執行。
Render Props從名知義,也是一種剝離重複使用的邏輯代碼,提高組件複用性的解決方案。在被複用的組件中,經過一個名爲「render」(屬性名也能夠不是render,只要值是一個函數便可)的屬性,該屬性是一個函數,這個函數接受一個對象並返回一個子組件,會將這個函數參數中的對象做爲props傳入給新生成的組件。
這種方法跟直接的在父組件中,將父組件中的state直接傳給子組件的區別是,經過Render Props不用寫死子組件,能夠動態的決定父組件須要渲染哪個子組件。
或者再歸納一點:
Render Props就是一個函數,作爲一個屬性被賦值給父組件,使得父組件能夠根據該屬性去渲染子組件。
首先來看經常使用的在類組件中經常使用的父子組件,父組件將本身的狀態state,經過props傳遞給子組件。
class Son extends React.Component{ render(){ const {feature} = this.props; return <div> <span>My hair is {feature.hair}</span> <span>My nose is {feature.nose}</span> </div> } } class FatherToSon extends React.Component{ constructor(){ this.state = { hair:'black', nose:'high' } } render(){ return <Son feature = {this.state}> } }
咱們定義了父組件FatherToSon,存在自身的state,而且將自身的state經過props的方式傳遞給了子組件。
這種就是常見的利用組件的props父子間傳值的方式,這個值能夠是變量,對象,也能夠是方法,可是僅僅使用只能一次性的給特定的子組件使用。若是如今有個Daughter組件也想複用父組件中的方法或者狀態,那麼必須新構建一個新組件:
class FatherToDaughter extends React.Component{ constructor(){ this.state = { hair:'black', nose:'high' } } render(){ return <Daughter feature = {this.state}> } }
從上面的例子能夠看出經過標準模式的父子組件的通訊方法,雖然可以傳遞父組件的狀態和函數,可是沒法實現複用。
咱們根據Render Props的特色:
Render Props就是一個函數,作爲一個屬性被賦值給父組件,使得父組件能夠根據該屬性去渲染子組件。
從新去實現上述的(1)中的例子。
class FatherChild extends React.Component{ constructor(){ this.state = { hair:'black', nose:'high' } } render(){ <React.Fragment> {this.props.render} </React.Fragment> } }
此時若是子組件要複用父組件中的屬性或者函數,則能夠直接使用,好比子組件Son如今能夠直接調用:
<FatherChild render={(obj)=>(<Son feature={obj}>)} />
若是子組件Daughter要複用父組件的方法,能夠直接調用:
<FatherChild render={(obj)=>(<Daughter feature={obj}>)} />
從這個例子中能夠看出,經過Render Props咱們實現一樣實現了一個組件工廠,能夠實現業務邏輯代碼的複用,相比與HOC,Render Props有如下幾個優勢。
Render Props也有一個缺點:
就是沒法利用SCU這個生命週期,來實現渲染性能的優化。
hooks概念在React Conf 2018被提出來,並將在將來的版本中被引入,hooks遵循函數式編程的理念,主旨是在函數組件中引入類組件中的狀態和生命週期,而且這些狀態和生命週期函數也能夠被抽離,實現複用的同時,減小函數組件的複雜性和易用性。
hooks相關的定義還在beta中,能夠在React v16.7.0-alpha中體驗,爲了渲染hooks定義的函數組件,必須執行React-dom的版本也爲v16.7.0-alpha,引入hooks必須先安裝:
npm i -s React@16.7.0-alpha npm i -s React-dom@16.7.0-alpha
hooks主要有三部分組成,State Hooks、Effect Hooks和Custom Hooks,下面分別來一一介紹。
跟類組件同樣,這裏的state就是狀態的含義,將state引入到函數組件中,同時類組件中更新state的方法爲setState,在State Hooks中也有相應的更新狀態的方法。
function ExampleWithManyStates() { // 聲明各類state以及更新相應的state的方法 const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); // ... }
上述就聲明瞭3個State hooks,相應的方法爲useState,該方法建立一個傳入初始值,建立一個state。返回一個標識該state的變量,以及更新該state的方法。
從上述例子咱們來看,一個函數組件是能夠經過useState建立多個state的。此外State Hooks的定義必須在函數組件的最高一級,不能在嵌套,循環等語句中使用。
function ExampleWithManyStates() { // 聲明各類state以及更新相應的state的方法 if(Math.random()>1){ const [age, setAge] = useState(42); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); }else{ const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); } // ... }
上述的方式是不被容許的,由於一個函數組件能夠存在多個State Hooks,而且useState返回的是一個數組,數組的每個元素是沒有標識信息的,徹底依靠調用useState的順序來肯定哪一個狀態對應於哪一個變量,因此必須保證使用useState在函數組件的最外層,此外後面要介紹的Effect Hooks的函數useEffect也必須在函數組件的最外層,以後會詳細解釋。
經過State Hooks來定義組件的狀態,一樣經過Effect Hooks來引入生命週期,Effect hooks經過一個useEffect的方法,以一種極爲簡化的方式來引入生命週期。
來看一個更新的例子:
import { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
上述就是一個經過useEffect來實現組件中生命週期的例子,useEffect整合了componentDidMount和componentDidUpdate,也就是說在componentDidMount和componentDidUpdate的時候都會執行一遍useEffect的函數,此外爲了實現componentWillUnmount這個生命週期函數,useEffect函數若是返回值是一個函數,這個函數就被定義成在componentWillUnmount這個週期內執行的函數。
useEffect(() => { //componentDidMount和componentDidUpdate週期的函數體 return ()=>{ //componentWillUnmount週期的函數體 } });
若是存在多個useState和useEffect時,必須按順序書寫,定義一個useState後,緊接着就使用一個useEffect函數。
useState('Mary') useEffect(persistForm) useState('Poppins') useEffect(updateTitle)
所以通useState同樣,useEffect函數也必須位於函數組件的最高一級。
上述咱們知道useEffect其實包含了componentDidMount和componentDidUpdate,若是咱們的方法僅僅是想在componentDidMount的時候被執行,那麼必須傳遞一個空數組做爲第二個參數。
useEffect(() => { //僅在componentDidMount的時候執行 },[]);
上述的方法會僅僅在componentDidMount,也就是函數組件第一次被渲染的時候執行,此後及時狀態更新,也不會執行。
此外,爲了減小沒必要要的狀態更新和渲染,能夠以下操做:
useEffect(() => { //僅在componentDidMount的時候執行 },[stateName]);
在上述的這個例子中,只有stateName的值發生改變,纔會去執行useEffect函數。
能夠將useState和useEffect的狀態和生命週期函數抽離,組成一個新的函數,該函數就是一個自定義的封裝完畢的hooks。
這是我寫的一個hooks ---> dom-location,
能夠這樣引入:
npm i -s dom-location
而且能夠在函數組件中使用。這個自定義的hooks也很簡單,就是封裝了狀態和生命週期函數。
import { useState, useEffect } from 'react' const useDomLocation = (element) => { let [elementlocation,setElementlocation] = useState(getlocation(element)); useEffect(()=>{ element.addEventListener('resize',handleResize); return ()=>{ element.removeEventListener('resize', handleResize); } },[]); function handleResize(){ setElementlocation(getlocation(element)); } function getlocation(E){ let rect = E.getBoundingClientRect() let top = document.documentElement.clientTop let left= document.documentElement.clientLeft return{ top : rect.top - top, bottom : rect.bottom - top, left : rect.left - left, right : rect.right - left }; } return elementlocation }
而後直接在函數中使用:
import useDomLocation from 'dom-location'; function App() { .... let obj = useDomLocation(element); }