React Hooks是React 16.8的新增特性,它可讓你在不編寫類的狀況下使用state和其餘React功能。hook翻譯過來就是「鉤子」,即在函數式組件中「鉤入」React state以及生命週期等特性的函數。一個簡單的hook相似於javascript
function Count(props) {
const [count, setCount] = useState(0); // 鉤入state
useEffect(() => { // 鉤入組件生命週期
console.log("component did mount");
}, []);
return (
<div onClick={() => setCount(v => v - 1)}> count: {count} </div>
)
}
複製代碼
那麼,爲何會有React Hooks呢,簡而言之四個字:邏輯複用。React沒用提供將可複用代碼附加到組件中去的途徑,對於這種問題通常都使用render props和高階組件來解決。然而這類方案須要從新組織結構,並且很麻煩,寫出來的代碼像下面的那樣,難以理解html
// redux connect高階組件
export default connect(
({ reducer: { isLogin } }) => ({ isLogin })
)(Avatar);
複製代碼
並且當咱們在使用Class在寫組件時會把UI與邏輯混在Class裏,致使邏輯複用困難。相信不少讀者都碰到過邏輯一致而UI不一致的問題,至少在字節跳動實習期間,大部分的重複代碼都是這種問題形成的。好比下面的UIjava
兩個部分都有PK邏輯,使用Class來編寫每個組件,每一個組件都得從新寫一遍PK邏輯。因爲後端提供的接口比較噁心,爲了儘量複用代碼,對PK接口也進行了儘量的封裝:react
API.match: 設置定時器,每3s給後端push一個發起匹配的消息,並在匹配成功後resolve
結果,匹配失敗後reject
。redux
API.cancelMatch: 清除定時器,並給後端push一個取消匹配的消息,取消成功resolve
,取消失敗(即在取消前一刻匹配成功了)reject
。後端
const PK_STATUS = {
NO_MATCH: 0, // 不匹配
MATCHING: 1, // 匹配中
PK: 2 // 正在PK
};
// banner * 2
class PKBanner extends React.Component {
constructor (props) {
super(props);
this.state = {
status: PK_STATUS.NO_MATCH
};
this.handleMatch = this.handleMatch.bind(this);
this.handleCancelMatch = this.handleCancelMatch.bind(this);
this.autoCancelMatch = this.autoCancelMatch.bind(this);
}
handleMatch () { // 發起匹配
this.setState({ status: PK_STATUS.MATCHING });
API.match().then(data => this.setState({ status: PK_STATUS.PK })).catch(() => { // 出錯狀況
toast("當前不能發起PK");
this.setState({ status: PK_STATUS.NO_MATCH });
API.cancelMatch();
});
}
handleCancelMatch () { // 取消匹配
API.cancelMatch().then(() => this.setState({ status: PK_STATUS.NO_MATCH }), data => { // 在取消時又匹配成功了
this.setState({ status: PK_STATUS.PK });
});
}
autoCancelMatch () { // 自動取消匹配
if (this.state.status === PK_STATUS.MATCHING) {
API.cancelMatch().then(() => {
toast('匹配超時');
this.setState({ status: PK_STATUS.NO_MATCH })
}, data => { // 在取消匹配時匹配成功了
this.setState({ status: PK_STATUS.PK });
})
}
}
render () {
// do something
}
}
複製代碼
這仍是對接口進行了比較好的封裝,若是把每3s給後端push匹配消息以及push取消匹配消息等邏輯直接往Class裏面寫可能會更多的重複代碼。因此能夠考慮把這種重複邏輯抽象成一個useMatch hook數組
function useMatch () {
const [status, setStatus] = useState(PK_STATUS.NO_MATCH);
function match () {
API.match().then(data => setStatus( PK_STATUS.PK)).catch(() => {
toast("當前不能發起PK");
setStatus(PK_STATUS.NO_MATCH );
API.cancelMatch();
});
}
function cancelMatch () {
API.cancelMatch().then(() => setStatus( PK_STATUS.NO_MATCH ), data => {
setStatus(PK_STATUS.PK);
});
}
function autoCancelMatch () {
if (status === PK_STATUS.MATCHING) {
API.cancelMatch().then(() => {
toast('匹配超時');
setStatus(PK_STATUS.NO_MATCH)
}, data => {
setStatus(PK_STATUS.PK);
})
}
}
return [status, match, cancelMatch, autoCancelMatch];
}
複製代碼
而後在每個須要使用PK邏輯的組件都可以將邏輯「鉤入」組件中(也許這就是hooks的精髓)閉包
function PKBanner (props) {
const [status, match, cancelMatch, autoCancelMatch] = useMatch();
// do something
}
複製代碼
其實React Hooks的原理和redux的實現有些類似(redux原理淺析),都是手動render
,好比useState
hook實現dom
import ReactDOM from "react-dom";
import React from "react";
const MyReact = function () {
function isFunc (fn) {
return typeof fn === "function";
}
let state = null; // 全局變量保存state
function useState (initState) {
state = state || initState;
function setState(newState) {
// 保存新的state,注意setState裏面可接個函數setState(v => v + 1);
state = isFunc(newState) ? newState(state) : newState;
render(); // 手動調用render => ReactDOM.render => diff => 重構UI
}
return [state, setState];
}
return { useState }
}();
function App(props) {
const [count, setCount] = MyReact.useState(0);
return (
<div onClick={() => setCount(count => count + 1)}> count: { count } </div>
)
}
function render () {
ReactDOM.render(<App/>, document.getElementById("root"));
}
render();
複製代碼
上面實現了一個簡單的useState
hook,其基本原理並不複雜(理解App
會一直調用)。當App
執行時,調用MyReact.useState(0)
將state初始爲0(state = state || initState
),並將state
和setState
返回,也就是count
和setCount
,當點擊事件發生時,state = newState
而且手動調用render
,而後執行App
,調用MyReact.useState(0)
,上一個步驟state = newState
將state賦值,所以state = state || initState
獲得的就是上一個的state
,在UI上的變化就是count + 1
了。函數
同理咱們能夠實現useEffect
hook,useEffect
有幾個特色:接受callback
和depArr
兩個參數;當depArr
不存在時callback
會一直執行;depArr
存在且和上一個depArr
不相等時執行callback
;
let deps = null;
function useEffect (callback, depArr) {
const hasDepsChange = deps ? deps.some((v, i) => depArr[i] !== v) : true; //檢查依賴數組是否變化
if (!depArr || hasDepsChange) { // depArr = undefined 或者 依賴數組變化時才調用callback
deps = depArr; // 記錄下上一個依賴數組
isFunc(callback) && callback();
}
}
複製代碼
每一次調用useEffect
時都對依賴數組作一個判斷,來判斷callback
是否須要執行,因此像下面的代碼,依賴數組一直沒變化,因此callback
也就只會執行一次
MyReact.useEffect(() => {
console.log("count: ", count);
}, []);
複製代碼
上面的實現存在問題,即state
只有一個,因此寫兩個hooks前一個會被覆蓋,好比
const [count, setCount] = MyReact.useState(0);
const [text, setText] = MyReact.useState("");
複製代碼
咱們可使用一個hooks數組來記錄第幾個hook,修改後的useState
的實現
let hooksOfState = []; // 記錄state的數組
let hooksOfStateIndex = 0; // 當前索引
function useState (initState) {
const currentIndex = hooksOfStateIndex; // 記錄這是第幾個useState
hooksOfState[currentIndex] = hooksOfState[currentIndex] || initState;
function setState(newState) { // 利用閉包的特性,這裏的currentIndex就是對應編號的useState
hooksOfState[currentIndex] = isFunc(newState) ? newState(hooksOfState[currentIndex]) : newState;
render();
}
return [hooksOfState[hooksOfStateIndex++], setState]; // 記得索引++
}
複製代碼
而useEffect
也是相似
let hooksOfEffect = []; // 記錄依賴數組
let hooksOfEffectIndex = 0; // 記錄索引
function useEffect (callback, depArr) {
const currentIndex = hooksOfEffectIndex;
const hasDepsChange = !hooksOfEffect[currentIndex] || hooksOfEffect[currentIndex].some((v, i) => depArr[i] !== v);
if (!depArr || hasDepsChange) {
hooksOfEffect[currentIndex] = depArr; // 這兩行順序很重要,若是callback是一個setState的操做,致使render,能夠避免出現棧溢出
isFunc(callback) && callback();
}
hooksOfEffectIndex ++; // 索引++
}
複製代碼
須要注意在每一次render
時都須要將索引設爲0,保證render
後還能按順序找到對應的hooks
function resetIndex () {
hooksOfStateIndex = 0;
hooksOfEffectIndex = 0;
}
function render () {
MyReact.resetIndex(); // render時將索引設置爲初始值
ReactDOM.render(<App/>, document.getElementById("root"));
}
複製代碼
這裏能夠思考,爲何hooks不讓寫在條件分支或循環內,答案也很明顯,若是寫在條件分支或者循環內將沒法保證下一次render
時hooks的順序。
function App(props) {
const [a, setA] = useState('a'); // 索引0
if (condition) {
const [b, setB] = useState('b'); // 假設條件執行,索引爲1,若是在下一次render時條件不成立,則跳過這個hook
}
const [c, setC] = useState('c'); // 索引 2,假設render時上一個條件不成立,這裏的索引將會變成1,致使發生意想不到的錯誤
return <div> </div>;
}
複製代碼
其餘hooks好比useMemo
,useCallback
的實現也是相似
let hooksOfMemo = []; // {deps: , result: }
let hooksOfMemoIndex = 0;
function useMemo (callback, depArr) {
const currentIndex = hooksOfMemoIndex;
const hasDepsChange = !hooksOfMemo[currentIndex] || hooksOfMemo[currentIndex].deps.some((v, i) => depArr[i] !== v); //
if (!depArr || hasDepsChange) {
hooksOfMemo[currentIndex] = {
deps: depArr,
result: isFunc(callback) && callback()
};
}
hooksOfMemoIndex ++;
return hooksOfMemo[currentIndex].result;
}
let hooksOfCallback = []; // {deps: , callback}
let hooksOfCallbackIndex = 0;
function useCallback (callback, depArr) {
const currentIndex = hooksOfCallbackIndex;
const hasDepsChange = !hooksOfCallback[currentIndex] || hooksOfCallback[currentIndex].deps.some((v, i) => depArr[i] !== v);
if (!depArr || hasDepsChange) {
hooksOfCallback[currentIndex] = {
deps: depArr,
callback: callback
};
}
hooksOfCallbackIndex ++;
return hooksOfCallback[currentIndex].callback;
}
複製代碼
固然上面的實現並非React中Hooks的真正實現,它的實現比這要複雜不少,好比React中是經過相似單鏈表的形式來代替數組的,經過next按順序串聯全部的hook。而且hooks的數據做爲組件的一個信息,存儲在這些節點上,伴隨組件整個生命週期。
import ReactDOM from "react-dom";
import React from "react";
const MyReact = function () {
function isFunc (fn) {
return typeof fn === "function";
}
let hooksOfState = [];
let hooksOfStateIndex = 0;
function useState (initState) {
const currentIndex = hooksOfStateIndex;
hooksOfState[currentIndex] = hooksOfState[currentIndex] || initState;
function setState (newState) {
hooksOfState[currentIndex] = isFunc(newState) ? newState(hooksOfState[currentIndex]) : newState;
render();
}
return [hooksOfState[hooksOfStateIndex ++], setState];
}
let hooksOfEffect = [];
let hooksOfEffectIndex = 0;
function useEffect (callback, depArr) {
const currentIndex = hooksOfEffectIndex;
const hasDepsChange = !hooksOfEffect[currentIndex] || hooksOfEffect[currentIndex].some((v, i) => depArr[i] !== v);
if (!depArr || hasDepsChange) {
hooksOfEffect[currentIndex] = depArr;
isFunc(callback) && callback();
}
hooksOfEffectIndex ++;
}
let hooksOfMemo = []; // { deps, result }
let hooksOfMemoIndex = 0;
function useMemo (callback, depArr) {
const currentIndex = hooksOfMemoIndex;
const hasDepsChange = !hooksOfMemo[currentIndex] || hooksOfMemo[currentIndex].deps.some((v, i) => depArr[i] !== v);
if (!depArr || hasDepsChange) {
hooksOfMemo[currentIndex] = {
deps: depArr,
result: isFunc(callback) && callback()
};
}
hooksOfMemoIndex ++;
return hooksOfMemo[currentIndex].result;
}
let hooksOfCallback = []; // {deps, callback}
let hooksOfCallbackIndex = 0;
function useCallback (callback, depArr) {
const currentIndex = hooksOfCallbackIndex;
const hasDepsChange = !hooksOfCallback[currentIndex] || hooksOfCallback[currentIndex].deps.some((v, i) => depArr[i] !== v);
if (!depArr || hasDepsChange) {
hooksOfCallback[currentIndex] = {
deps: depArr,
callback: callback
};
}
hooksOfCallbackIndex ++;
return hooksOfCallback[currentIndex].callback;
}
function resetIndex () {
hooksOfStateIndex = 0;
hooksOfEffectIndex = 0;
hooksOfMemoIndex = 0;
hooksOfCallbackIndex = 0;
}
return { resetIndex, useEffect, useState, useMemo, useCallback }
}();
function App (props) {
const [count, setCount] = MyReact.useState(0);
const [text, setText] = MyReact.useState("count");
MyReact.useEffect(() => {
console.log("count: ", count);
}, [count]);
MyReact.useEffect(() => {
console.log("changed");
// setText("change start");
}, []);
const sum = MyReact.useMemo(function () {
let result = 0;
for (let i = 0; i < count; i ++) result += i;
return result;
}, [count]);
const fn = MyReact.useCallback(() => 20 + count, [count]);
return (
<div onClick={ () => setCount(count => count + 1) }> <div>{ text }: { count } </div> <div>sum: { sum }, const: { fn() }</div> </div>
)
}
function render () {
MyReact.resetIndex();
ReactDOM.render(<App/>, document.getElementById("root"));
}
render();
複製代碼