React Hook簡介
Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性
從官網的這句話中,我們可以明确的知道,Hook增加了函數式元件中state的使用,在之前函數式元件是無法擁有自己的狀态,隻能通過props以及context來渲染自己的UI,而在業務邏輯中,有些場景必須要使用到state,那麼我們就隻能将函數式元件定義為class元件。而現在通過Hook,我們可以輕松的在函數式元件中維護我們的狀态,不需要更改為class元件。
React Hooks要解決的問題是狀态共享,這裡的狀态共享是指隻共享狀态邏輯複用,并不是指資料之間的共享。我們知道在React Hooks之前,解決狀态邏輯複用問題,我們通常使用
higher-order components
和
render-props
,那麼既然已經有了這兩種解決方案,為什麼React開發者還要引入React Hook?對于
higher-order components
和
render-props
,React Hook的優勢在哪?
React Hook例子
我們先來看一下React官方給出的React Hook的demo
import { useState } from 'React';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
我們再來看看不用React Hook的話,如何實作
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
可以看到,在
React Hook
中,
class Example
元件變成了函數式元件,但是這個函數式元件卻擁有的自己的狀态,同時還可以更新自身的狀态。這一切都得益于
useState
這個
Hook
,useState 會傳回一對值:目前狀态和一個讓你更新它的函數,你可以在事件處理函數中或其他一些地方調用這個函數。它類似 class 元件的 this.setState,但是它不會把新的 state 和舊的 state 進行合并
React複用狀态邏輯的解決方案
Hook是另一種複用狀态邏輯的解決方案,React開發者一直以來對狀态邏輯的複用方案不斷提出以及改進,從Mixin到高階元件到Render Propss 到現在的Hook,我們先來簡單了解一下以前的解決方案
Mixin模式
在React最早期,提出了根據Mixin模式來複用元件之間的邏輯。在Javascript中,我們可以将Mixin繼承看作是通過擴充收集功能的一種途徑.我們定義的每一個新的對象都有一個原型,從中它可以繼承更多的屬性.原型可以從其他對象繼承而來,但是更重要的是,能夠為任意數量的對象定義屬性.我們可以利用這一事實來促進功能重用。
React中的mixin主要是用于在完全不相關的兩個元件中,有一套基本相似的功能,我們就可以将其提取出來,通過mixin的方式注入,進而實作代碼的複用。例如,在不同的元件中,元件需要每隔一段時間更新一次,我們可以通過建立setInterval()函數來實作這個功能,同時在元件銷毀的時候,我們需要解除安裝此函數。是以可以建立一個簡單的 mixin,提供一個簡單的 setInterval() 函數,它會在元件被銷毀時被自動清理。
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.forEach(clearInterval);
}
};
var createReactClass = require('create-React-class');
var TickTock = createReactClass({
mixins: [SetIntervalMixin], // 使用 mixin
getInitialState: function() {
return {seconds: 0};
},
componentDidMount: function() {
this.setInterval(this.tick, 1000); // 調用 mixin 上的方法
},
tick: function() {
this.setState({seconds: this.state.seconds + 1});
},
render: function() {
return (
<p>
React has been running for {this.state.seconds} seconds.
</p>
);
}
});
ReactDOM.render(
<TickTock />,
document.getElementById('example')
);
mixin的缺點
- 不同mixin可能會互相依賴,耦合性太強,導緻後期維護成本過高
- mixin中的命名可能會沖突,無法使用同一命名的mixin
- mixin即使開始很簡單,它們會随着業務場景增多,時間的推移産生滾雪球式的複雜化
具體缺點可以看此連結Mixins是一種禍害
因為mixin的這些缺點存在,在React中已經不建議使用mixin模式來複用代碼,React全面推薦使用高階元件來替代mixin模式,同時ES6 本身是不包含任何 mixin 支援。是以,當你在 React 中使用 ES6 class 時,将不支援 mixins 。
高階元件
高階元件(HOC)是 React 中用于複用元件邏輯的一種進階技巧。HOC 自身不是 React API 的一部分,它是一種基于 React 的組合特性而形成的設計模式
進階元件并不是React提供的API,而是React的一種運用技巧,高階元件可以看做是裝飾者模式(Decorator Pattern)在React的實作。裝飾者模式: 動态将職責附加到對象上,若要擴充功能,裝飾者提供了比繼承更具彈性的代替方案.
具體而言,高階元件是參數為元件,傳回值為新元件的函數。
元件是将 props 轉換為 UI,而高階元件是将元件轉換為另一個元件
我們可以通過高階元件動态給其他元件增加日志列印功能,而不影響原先元件的功能
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
}
Render Propss
術語 “Render Props” 是指一種在 React 元件之間使用一個值為函數的 prop 共享代碼的簡單技術
具有 Render Props 的元件接受一個函數,該函數傳回一個 React 元素并調用它而不是實作自己的渲染邏輯
以下我們提供了一個帶有prop的元件,它能夠動态決定什麼需要渲染,這樣就能對元件的邏輯以及狀态複用,而不用改變它的渲染結構。
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移動滑鼠!</h1>
<Mouse render={mouse => (
)}/>
</div>
);
}
}
然而通常我們說的Render Props 是因為模式才被稱為 Render Props ,又不是因為一定要用render對prop進行命名。我們也可以這樣來表示
<Mouse>
{mouse => (
<Cat mouse={mouse} />
)}
</Mouse>
React Hook動機
React Hook是官網提出的又一種全新的解決方案,在了解React Hook之前,我們先看一下React Hook提出的動機 1. 在元件之間複用狀态邏輯很難 2. 複雜元件變得難以了解 3. 難以了解的 class
下面說說我對這三個動機的了解:
在元件之間複用狀态邏輯很難,在之前,我們通過高階元件(Higher-Order Components)和渲染屬性(Render Propss)來解決狀态邏輯複用困難的問題。很多庫都使用這些模式來複用狀态邏輯,比如我們常用redux、React Router。高階元件、渲染屬性都是通過組合來一層層的嵌套共用元件,這會大大增加我們代碼的層級關系,導緻層級的嵌套過于誇張。從React的devtool我們可以清楚的看到,使用這兩種模式導緻的層級嵌套程度
複雜元件變得難以了解,在不斷變化的業務需求中,元件逐漸會被狀态邏輯以及副作用充斥,每個生命周期常常會包含一些不相關的邏輯。我們寫代碼通常都依據函數的單一原則,一個函數一般隻處理一件事,但在生命周期鈎子函數中通常會同時做很多事情。比如,在我們需要在
componentDidMount
中發起ajax請求擷取資料,同時有時候也會把事件綁定寫在此生命周期中,甚至有時候需要在
componentWillReceiveProps
中對資料進行跟
componentDidMount
一樣的處理。
互相關聯且需要對照修改的代碼被進行了拆分,而完全不相關的代碼卻在同一個方法中組合在一起。如此很容易産生 bug,并且導緻邏輯不一緻。難以了解的class
,個人覺得使用class元件這種還是可以的,隻要了解了class的this指向綁定問題,其實上手的難度不大。大家要了解,這并不是 React 特有的行為;這其實與 JavaScript 函數工作原理有關。是以隻要了解好JS函數工作原理,其實this綁定都不是事。隻是有時候為了保證this的指向正确,我們通常會寫很多代碼來綁定this,如果忘記綁定的話,就有會各種bug。綁定this方法:
1.this.handleClick = this.handleClick.bind(this);
2.<button onClick={(e) => this.handleClick(e)}>
Click me
</button>
于是為了解決以上問題,React Hook就被提出來了
state Hook使用
我們回到剛剛的代碼中,看一下如何在函數式元件中定義state
import React, { useState } from 'React';
const [count, setCount] = useState(0);
- useState做了啥
我們可以看到,在此函數中,我們通過useState定義了一個'state變量',它與 class 裡面的
this.state
提供的功能完全相同.相當于以下代碼
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
}
- useState參數
在代碼中,我們傳入了0作為useState的參數,這個參數的數值會被當成count初始值。當然此參數不限于傳遞數字以及字元串,可以傳入一個對象當成初始的state。如果state需要儲存多個變量的值,那麼調用多次useState即可
- useState傳回值
傳回值為:目前 state 以及更新 state 的函數,這與 class 裡面
this.state.count
和
this.setState
類似,唯一差別就是你需要成對的擷取它們。看到
[count, setCount]
很容易就能明白這是ES6的解構數組的寫法。相當于以下代碼
let _useState = useState(0);// 傳回一個有兩個元素的數組
let count = _useState[0];// 數組裡的第一個值
let setCount = _useState[1];// 數組裡的第二個值
讀取狀态值
隻需要使用變量即可
以前寫法
<p>You clicked {this.state.count} times</p>
現在寫法
<p>You clicked {count} times</p>
更新狀态
通過setCount函數更新
以前寫法
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
現在寫法
<button onClick={() => setCount(count + 1)}>
Click me
</button>
這裡setCount接收的參數是修改過的新狀态值
聲明多個state變量
我們可以在一個元件中多次使用state Hook來聲明多個state變量
function ExampleWithManyStates() {
// 聲明多個 state 變量!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
// ...
}
React 假設當你多次調用 useState
的時候,你能保證每次渲染時它們的調用順序是不變的 為什麼React要規定每次渲染它們時的調用順序不變呢,這個是一個了解Hook至關重要的問題
Hook 規則
Hook 本質就是 JavaScript 函數,但是在使用它時需要遵循兩條規則。并且React要求強制執行這兩條規則,不然就會出現異常的bug
- 隻在最頂層使用 Hook
確定總是在你的 React 函數的最頂層調用他們
- 隻在 React 函數中調用 Hook
這兩條規則出現的原因是,我們可以在單個元件中使用多個 State Hook 或 Effect Hook, React 靠的是 Hook 調用的順序來知道哪個 state 對應哪個
useState
function Form() {
const [name1, setName1] = useState('Arzh1');
const [name2, setName2] = useState('Arzh2');
const [name3, setName3] = useState('Arzh3');
// ...
}
// ------------
// 首次渲染
// ------------
useState('Arzh1') // 1. 使用 'Arzh1' 初始化變量名為 name1 的 state
useState('Arzh2') // 2. 使用 'Arzh2' 初始化變量名為 name2 的 state
useEffect('Arzh3') // 3. 使用 'Arzh3' 初始化變量名為 name3 的 state
// -------------
// 二次渲染
// -------------
useState('Arzh1') // 1. 讀取變量名為 name1 的 state(參數被忽略)
useState('Arzh2') // 2. 讀取變量名為 name2 的 state(參數被忽略)
useEffect('Arzh3') // 3. 讀取變量名為 name3 的 state(參數被忽略)
如果我們違反React的規則,使用條件渲染
if (name !== '') {
const [name2, setName2] = useState('Arzh2');
}
假設第一次
(name !== '')
為true的時候,執行此Hook,第二次渲染
(name !== '')
為false時,不執行此Hook,那麼Hook的調用順序就會發生變化,産生bug
useState('Arzh1') // 1. 讀取變量名為 name1 的 state
//useState('Arzh2') // 2. Hook被忽略
useEffect('Arzh3') // 3. 讀取變量名為 name2(之前為name3) 的 state
React 不知道第二個
useState
的 Hook 應該傳回什麼。React 會以為在該元件中第二個 Hook 的調用像上次的渲染一樣,對應的是
arzh2
的 useState,但并非如此。是以這就是為什麼React強制要求Hook使用必須遵循這兩個規則,同時我們可以使用
eslint-plugin-React-Hooks
來強制限制
Effect Hook使用
我們在上面的代碼中增加Effect Hook的使用,在函數式元件中增加副作用,修改網頁的标題
useEffect(() => {
document.title = `You clicked ${count} times`;
});
如果你熟悉 React class 的生命周期函數,你可以把Hook 看做
useEffect
,
componentDidMount
和
componentDidUpdate
這三個函數的組合。
componentWillUnmount
也就是我們完全可以通過useEffect來替代這三個生命鈎子函數
我們來了解一下通常需要副作用的場景,比如發送請求,手動變更dom,記錄日志等。通常我們都會在第一次dom渲染完成以及後續dom重新更新時,去調用我們的副作用操作。我們可以看一下以前生命周期的實作
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
這也就是我們上面提到的React Hook動機的第二個問題來源之一,需要在第一次渲染以及後續的渲染中調用相同的代碼
Effect在預設情況下,會在第一次渲染之後和每次更新之後都會執行,這也就讓我們不需要再去考慮是
componentDidMount
還是
componentDidUpdate
時執行,
隻需要明白Effect在元件渲染後執行即可清除副作用
有時候對于一些副作用,我們是需要去清除的,比如我們有個需求需要輪詢向伺服器請求最新狀态,那麼我們就需要在解除安裝的時候,清理掉輪詢的操作。
componentDidMount() {
this.pollingNewStatus()
}
componentWillUnmount() {
this.unPollingNewStatus()
}
我們可以使用Effect來清除這些副作用,隻需要在Effect中傳回一個函數即可
useEffect(() => {
pollingNewStatus()
//告訴React在每次渲染之前都先執行cleanup()
return function cleanup() {
unPollingNewStatus()
};
});
有個明顯的差別在于useEffect其實是每次渲染之前都會去執行
cleanup()
,而
componentWillUnmount
隻會執行一次。
Effect性能優化
useEffect其實是每次更新都會執行,在某些情況下會導緻性能問題。那麼我們可以通過跳過 Effect 進行性能優化。在class元件中,我們可以通過在
componentDidUpdate
中添加對
prevProps
或
prevState
的比較邏輯解決
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
在Effect中,我們可以通過增加Effect的第二個參數即可,如果沒有變化,則跳過更新
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新
其他Hooks
由于篇幅原因,就不再此展開了,有興趣可以自行官網檢視
微信公衆号
希望大家多多支援,如有幫助,掃碼關注
參考文章
- 30分鐘精通React Hooks
- React hooks實踐
- 從Mixin到HOC再到Hook