為了實現分離業務邏輯程式碼,實現元件內部相關業務邏輯的複用,在React的迭代中針對類元件中的程式碼複用依次釋出了Mixin、HOC、Render props等幾個方案。此外,針對函式元件,在React v16.7.0-alpha 中提出了hooks的概念,在本身無狀態的函式元件,引入獨立的狀態空間,也就是說在函式元件中,也可以引入類元件中的state和元件生命週期,使得函式元件變得豐富多彩起來,此外,hooks也保證了邏輯程式碼的複用性和獨立性。
本文從針對類元件的複用解決方案開始說起,先後介紹了從Mixin、HOC到Render props的演進,最後介紹了React v16.7.0-alpha 中的 hooks以及自定義一個hooks
- Mixin
- HOC
- Render props
- React hooks的介紹以及如何自定義一個hooks
原文地址在我的部落格中:https://github.com/forthealll…
歡迎star和fork~
一、Mixin
Mixin是最早出現的複用類元件中業務邏輯程式碼的解決方案,首先來介紹以下如何適應Mixin。下面是一個Mixin的例子:
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引入公共業務邏輯:
mixins:[someMixins]
從上面的例子,我們來總結以下mixin的缺點:
- Mixin是可以存在多個的,是一個陣列的形式,且Mixin中的函式是可以呼叫setState方法元件中的state的,因此如果有多處Mixin的模組中修改了相同的state,會無法確定state的更新來源
- ES6 classes支援的是繼承的模式,而不支援Mixins
- Mixin會存在覆蓋,比如說兩個Mixin模組,存在相同生命週期函式或者相同函式名的函式,那麼會存在相同函式的覆蓋問題。
Mixin已經被廢除,具體缺陷可以參考Mixins Considered Harmful
二、HOC
為了解決Mixin的缺陷,第二種解決方案是高階元件(high order component,簡稱HOC)。
1、舉例幾種HOC的形式
HOC簡單理解就是元件工廠,接受原始元件作為引數,新增完功能與業務後,返回新的元件。下面來介紹HOC引數的幾個例子。
(1)引數僅為原始元件
const redApple = withFruit(Apple);
(2)引數為原始元件和一個物件
const redApple = withFruit(Apple,{color:`red`,weight:`200g`});
但是這種情況比較少用,如果物件中僅僅傳遞的是屬性,其實完全可以通過元件的props實現值的傳遞,我們用HOC的主要目的是分離業務,關於UI的展示,以及一些元件中的屬性和狀態,我們一般通過props來指定比較方便
(3)引數為原始元件和一個函式
const redApp=withFruit(App,()=>{console.log(`I am a fruit`)})
(4)柯里化
最常見的是僅以一個原始元件作為引數,但是在外層包裹了業務邏輯,比如react-redux的conect函式中:
class Admin extends React.Component{
}
const mapStateToProps=(state)=>{
return {
};
}
const mapDispatchToProps=(dispatch)=>{
return {
}
}
const connect(mapStateToProps,mapDispatchToProps)(Admin)
2、HOC的缺點
HOC解決了Mixin的一些缺陷,但是HOC本身也有一些缺點:
(1)難以溯源,且存在屬性覆蓋問題
如果原始元件A,先後通過工廠函式1,工廠函式2,工廠函式3….構造,最後生成了元件B,我們知道元件B中有很多與A元件不同的props,但是我們僅僅通過元件B,並不能知道哪個元件來自於哪個工廠函式。同時,如果有2個工廠函式同時修改了元件A的某個同名屬性,那麼會有屬性覆蓋的問題,會使得前一個工廠函式的修改結果失效。
(2)HOC是靜態構建的
所謂靜態構建,也就是說生成的是一個新的元件,並不會馬上render,HOC元件工廠是靜態構建一個元件,這類似於重新宣告一個元件的部分。也就是說,HOC工廠函式裡面的宣告周期函式,也只有在新元件被渲染的時候才會執行。
(3)會產生無用的空元件
三、Render Prop
Render Props從名知義,也是一種剝離重複使用的邏輯程式碼,提升元件複用性的解決方案。在被複用的元件中,通過一個名為“render”(屬性名也可以不是render,只要值是一個函式即可)的屬性,該屬性是一個函式,這個函式接受一個物件並返回一個子元件,會將這個函式引數中的物件作為props傳入給新生成的元件。
這種方法跟直接的在父元件中,將父元件中的state直接傳給子元件的區別是,通過Render Props不用寫死子元件,可以動態的決定父元件需要渲染哪一個子元件。
或者再概括一點:
Render Props就是一個函式,做為一個屬性被賦值給父元件,使得父元件可以根據該屬性去渲染子元件。
(1)標準父子元件通訊方法
首先來看常用的在類元件中常用的父子元件,父元件將自己的狀態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}>
}
}
從上面的例子可以看出通過標準模式的父子元件的通訊方法,雖然能夠傳遞父元件的狀態和函式,但是無法實現複用。
(2)Render Props的引出
我們根據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有以下幾個優點。
- 不用擔心props的命名問題
- 可以溯源,子元件的props一定是來自於直接父元件
- 是動態構建的
Render Props也有一個缺點:
就是無法利用SCU這個生命週期,來實現渲染效能的優化。
四、React hooks的介紹以及如何自定義一個hooks
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,下面分別來一一介紹。
(1)State 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也必須在函式元件的最外層,之後會詳細解釋。
(2)Effect Hooks
通過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函式也必須位於函式元件的最高一級。
(3)Effect Hooks的補充
上述我們知道useEffect其實包含了componentDidMount和componentDidUpdate,如果我們的方法僅僅是想在componentDidMount的時候被執行,那麼必須傳遞一個空陣列作為第二個引數。
useEffect(() => {
//僅在componentDidMount的時候執行
},[]);
上述的方法會僅僅在componentDidMount,也就是函式元件第一次被渲染的時候執行,此後及時狀態更新,也不會執行。
此外,為了減少不必要的狀態更新和渲染,可以如下操作:
useEffect(() => {
//僅在componentDidMount的時候執行
},[stateName]);
在上述的這個例子中,只有stateName的值發生改變,才會去執行useEffect函式。
(4)Custom Hooks自定義hooks
可以將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);
}