React Hooks
Hook是React v16.8的新特性,可以用函數(shù)的形式代替原來的繼承類的形式,可以在不編寫
class的情況下使用state以及其他React特性
React 設(shè)計原理
- React認為,UI視圖是數(shù)據(jù)的一種視覺映射,
UI = F(Data),這里的F主要負責對輸入數(shù)據(jù)進行加工,對數(shù)據(jù)變更做出相應 - 公式里的
F在React里抽象成組件,React是以組件為粒度編排應用的,組件是代碼復用的最小單元 - 在設(shè)計上,React采用
props屬性來接收外部數(shù)據(jù),使用state屬性來管理組件自身產(chǎn)生的數(shù)據(jù),而為了實現(xiàn)(運行時)對數(shù)據(jù)變更做出相應需要,React采用基于類的組件設(shè)計 - 除此之外,React認為組件是有生命周期的,因此提供了一系列API供開發(fā)者使用
我們所熟悉的React組件長這樣
import React, { Component } from "react";
// React基于Class設(shè)計組件
export default class Button extends Component {
constructor() {
super();
// 組件自身數(shù)據(jù)
this.state = { buttonText: "Click me, please" };
this.handleClick = this.handleClick.bind(this);
}
// 響應數(shù)據(jù)變更
handleClick() {
this.setState({ buttonText: "Thanks, been clicked!" });
}
// 編排數(shù)據(jù)呈現(xiàn)UI
render() {
const { buttonText } = this.state;
return <button onClick={this.handleClick}>{buttonText}</button>;
}
}
組件類的缺點
上面實例代碼只是一個按鈕組件,但是可以看到,它的代碼已經(jīng)很重了。真實的React App由多個類按照層級,一層層構(gòu)成,復雜度成倍增長。再加入 Redux + React Router,就變得更復雜
很可能隨便一個組件最后export出去就是醬紫的:
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
一個4層嵌套HOC,嵌套地獄
同時,如果你的組件內(nèi)事件多,那么你的constructor就是醬紫的
class MyComponent extends React.Component {
constructor() {
// initiallize
this.handler1 = this.handler1.bind(this)
this.handler2 = this.handler2.bind(this)
this.handler3 = this.handler3.bind(this)
this.handler4 = this.handler4.bind(this)
this.handler5 = this.handler5.bind(this)
// ...more
}
}
而Function Component編譯后就是一個普通的function,function對js引擎是友好的,而Class Component在React內(nèi)部是當做Javascript Function類來處理的,代碼很難被壓縮,比如方法名稱
還有this啦,稍微不注意就會出現(xiàn)因this指向報錯的問題等。。。
總結(jié)一下就是:
- 很難復用邏輯,會導致組件樹層級很深
- 會產(chǎn)生巨大的組件(很多代碼必須寫在類里面)
- 類組件很難理解,比如方法需要
bind,this的指向不明確 - 編譯size,性能問題
Hooks
State Hook
Hook是什么?
可以先通過一個例子來看看,在class中,我們通過在構(gòu)造函數(shù)中設(shè)置this.state初始化組件的state:
this.state = {
n: 0
}
而在函數(shù)組件中,我們沒有this,所以我們不能分配或讀取this.state,但是可以在組件中調(diào)用useStateHook
import React, {useState} from 'react';
function xxx() {
const [n, setN] = useState(0);
}
在上面代碼中,useState就是Hook
Hook是一個特殊的函數(shù),它可以讓你“鉤入”React的特性。例如
useState是允許你在React函數(shù)組件中添加state的Hook。
如果你在編寫函數(shù)組件并意識到需要向其添加一些state,以前的做法是必須將其轉(zhuǎn)化為Class。現(xiàn)在你可以在現(xiàn)有的函數(shù)組件中使用Hook
讓函數(shù)組件自身具備狀態(tài)處理能力,且自身能夠通過某種機制觸發(fā)狀態(tài)的變更并引起re-render,這種機制就是Hooks
走進useState
示例代碼:
import React, { useState } from 'react';
function App() {
// 聲明一個叫 "n" 的 state 變量
// useState接收一個參數(shù)作為初始值
// useState返回一個數(shù)組,[state, setState]
const [n, setN] = useState(0);
return (
<div>
{/* 讀取n,等同于this.state.n */}
<p>{n}</p>
{/* 通過setN更新n,等同于this.setN(n: this.state.n + 1) */}
<button onClick={() => setN(n + 1)}>
+1
</button>
</div>
);
}
運行一下(代碼1)
- 首次渲染 render
<App /> - 調(diào)用
App函數(shù),得到虛擬DOM對象,創(chuàng)建真實DOM - 點擊buttno調(diào)用
setN(n + 1),因為要更新頁面的n,所以再次render<App /> - 重復第二步,從控制臺打印看出每次執(zhí)行
setN都會觸發(fā)App函數(shù)運行,得到一個新的虛擬DOM,DOM Diff更新真實DOM
那么問題來了,首次運行App函數(shù)和setN時都調(diào)用了App,兩次運行useState是一樣的嗎?setN改變n的值了嗎?為什么得到了不一樣的n,useState的時候做了什么?
分析:
- setN
- setN一定會修改數(shù)據(jù)x,將n+1存入x
- setN一定會觸發(fā)
<App />重新渲染(re-render)
- useState
- useState肯定會從x讀取n的最新值
- x
- 每個組件都有自己的數(shù)據(jù)x,我們將其命名為state
嘗試實現(xiàn)React.useState(代碼2)
// 和useState一樣,myUseState接收一個初始值,返回state和setState方法
const myUseState = initialValue => {
let state = initialValue
const setState = newValue => {
state = newValue
// 重新渲染
render()
}
return [state, setState]
}
const render = () => {
// 雞賊暴力渲染法
ReactDOM.render(<App />, rootElement)
}
function App() {
const [n, setN] = myUseState(0)
...
}
點擊button,n沒有任何變化
原來每次state都變成了初始值0,因為myUseState會將state重置
我們需要一個不會被myUseState重置的變量,那么這個變量只要聲明在myUseState外面即可
let _state;
const myUseState = initialValue => {
// 如果state是undefined,則賦給初始值,否則就賦值為保存在外面的_state
_state = _state === undefined ? initialValue : _state;
const setState = newValue => {
_state = newValue;
render();
};
return [_state, setState];
};
還有問題,如果一個組件有倆state咋整?由于所有數(shù)據(jù)都放在_state,產(chǎn)生沖突:
function App() {
const [n, setN] = myUseState(0)
const [m, setM] = myUseState(0)
...
}
解決:
- 把_state做成對象
- 不可行,沒有key,
useState(0)只傳入了一個參數(shù)0,并不知道是n還是m
- 不可行,沒有key,
- 把_state做成數(shù)組
- 可行,
_state = [0, 0]
- 可行,
let _state = [];
let index = 0;
const myUseState = (initialValue) => {
const currentIndex = index;
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
render();
};
index += 1;
return [_state[currentIndex], setState];
};
const render = () => {
// 重新渲染要重置index
index = 0;
ReactDOM.render(<App />, rootElement);
};
解決了存在多個state的情況,但是還有問題,就是useState調(diào)用順序必須一致!
- 如果第一次渲染時n是第一個,m是第二個,k是第三個
- 則第二次渲染時必須保證順序一致,因為數(shù)組根據(jù)調(diào)用順序存儲值
- re-render時會從第一行代碼開始重新執(zhí)行整個組件
- 所以React不允許出現(xiàn)如下代碼
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
最后一個問題:
App用了_state和index,那其他組件用什么?放在全局作用域重名怎么解決?
運行App后,React會維護一個虛擬DOM樹,每個節(jié)點都有一個虛擬DOM對象(Fiber),將_state,index存儲在對象上
額外擴展一下Fiber對象,它的數(shù)據(jù)結(jié)構(gòu)如下:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance 實例
this.tag = tag;
this.key = key;
// JSX翻譯過來之后是React.createElement,他最終返回的是一個ReactElement對象
// 就是ReactElement的`?typeof`
this.elementType = null;
// 就是ReactElement的type,他的值就是<MyClassComponent />這個class,不是實例,實例是在render過程中創(chuàng)建
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
// 用來存儲state
// 記錄useState應該返回的結(jié)果
this.memoizedState = null;
this.firstContextDependency = null;
// ...others
}
總結(jié):
- 每個函數(shù)組件對應一個React節(jié)點(FiberNode)
- 每個節(jié)點保存著
_state(memorizedState)和index(實際是鏈表) -
useState會讀取對應節(jié)點的state[index] -
index是由useState的調(diào)用順序決定 -
setState會修改_state,并觸發(fā)更新
搞清楚useState干了啥以后,回過頭再看setN改變n了嗎,為什么得到了不一樣的n(代碼3)
- 先+1,后log => 1
- 先log,后+1 => 0
- 為什么log出了舊數(shù)據(jù)
分析:
- 先點擊log,
log(0)三秒后執(zhí)行,此時n是0,n不會變 - 再點擊+1,此時調(diào)用的是一個新的函數(shù),生成了新的
n,re-render -
n=0和n=1同時存在內(nèi)存中
結(jié)論:因為有多個n,setN并不會改變n,React函數(shù)式編程決定了n的值不會被改變,只會被回收
注意事項:
- 不可局部更新(代碼4)
- 地址要變:
setState(obj)如果obj地址不變,那么React就認為數(shù)據(jù)沒有變化 - useState接受函數(shù):函數(shù)返回初始state,且只執(zhí)行一次
- setState接收函數(shù):setN(i => i + 1),優(yōu)先使用這種形式
useReducer
React本身不提供狀態(tài)管理功能,通常需要使用外部庫,最常用的庫是
Redux
Redux的核心概念是,將需要修改的state都存入到store里,發(fā)起一個action用來描述發(fā)生了什么,用reducers描述action如何改變state,真正能改變store中數(shù)據(jù)的是store.dispatch API
Reducer是一個純函數(shù),只承擔計算 State 的功能,函數(shù)的形式是(state, action) => newState
Action是消息的載體,只能被別人操作,自己不能進行任何操作
useReducer()鉤子用來引入Reducer功能(代碼5)
const [state, dispatch] = useReducer(reducer, initial)
上面是useReducer基本用法
- 接受Reducer函數(shù)和一個初始值作為參數(shù)
- 返回一個數(shù)組,數(shù)組
[0]位是狀態(tài)當前值,第[1]位是dispatch函數(shù),用來發(fā)送action
似曾相識的感覺
const [n, setN] = useState(0)
// n:讀
// setN:寫
總的來說useReducer就是復雜版本的useState,那么什么時候使用useReducer,什么時候又使用useState呢?
看一個代碼6
當你需要維護多個state,那么為什么不用一個對象來維護呢,對象是可以合并的
需要注意的是,由于Hooks可以提供狀態(tài)管理和Reducer函數(shù),所以在這方面可以取代Redux。但是,它沒法兒提供中間件(midddleware)和時間旅行(time travel),如果你需要這兩個功能,還是要用Redux。
中間件原理:封裝改造store.dispatch,將其指向中間件,以實現(xiàn)在dispatch和reducer之間處理action數(shù)據(jù)的邏輯,也可以將中間件看成是dispatch方法的封裝器
有沒有代替Redux的方法呢?
Reducer + Context
useContext
什么是上下文?
- 全局變量是全局的上下文
- 上下文是局部的全局變量
使用方法:
// 創(chuàng)建上下文
const c = createContext(null)
function App() {
const [n, setN] = useState(0)
return (
// 使用<c.Provider>圈定作用域
<c.Provider value={n, setN}>
<Father />
</ c.Provider>
)
}
function Father() {
return (
<div>我是爸爸
<Son />
</div>
)
}
function Son() {
// 在作用域中使用useContext(c)來獲取并使用上下文
// 要注意這里useContext返回的是對象,不是數(shù)組
const {n, setN} = useContext(c)
const onClick = () => {
setN( i => i + 1)
}
return (
<div>我是兒子,我可以拿到n:{n}
<button onClick={onClick}>我也可以更新n</button>
</div>
)
}
注意事項:
- 使用useContext時,在一個模塊改變數(shù)據(jù),另一個模塊是感知不到的
-
setN會重新渲染<App />,自上而下逐級通知更新,并不是響應式,因為響應式是監(jiān)聽數(shù)據(jù)變化通知對應組件進行更新
useEffect
useEffect鉤子會在每次render后運行
React保證了每次運行useEffect的同時,DOM 都已經(jīng)更新完畢
應用:
- 作為
componentDidMount使用,[]作第二個參數(shù) - 作為
componentDidUpdate使用,可指定依賴 - 作為
componentWillUnmount使用,通過return - 以上三種可同時存在
function App() {
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
const afterRender = useEffect;
// componentDidMount
useEffect(() => {
console.log('第一次渲染之后執(zhí)行這句話')
}, [])
// componentDidUpdate
useEffect(() => {
console.log('每次次都會執(zhí)行這句話')
})
useEffect(() => {
console.log('n變化就會執(zhí)行這句話,包含第一次')
}, [n])
// componentWillUnmount
useEffect(() => {
const id = setInterval(() => {
console.log('每一秒都打印這句話')
}, 1000)
return () =>{
// 如果組件多次渲染,則在執(zhí)行下一個 effect 之前,上一個 effect 就已被清除
console.log('當組件要掛掉了,打印這句話')
window.clearInterval(id)
}
}, [])
return (
<div>
n: {n}
<button onClick={onClick}>+1</button>
</div>
)
}
Hook 允許我們按照代碼的用途分離他們,而不是像生命周期函數(shù)那樣
React將按照effect聲明的順序依次調(diào)用組件中的每一個effect
對應的,另一個effect鉤子,useLayoutEffect
- useEffect在瀏覽器渲染之后執(zhí)行,useLayoutEffect在渲染前執(zhí)行(代碼7)
- useLayoutEffect在渲染前執(zhí)行,使用它來讀取 DOM 布局并同步觸發(fā)重渲染
// 偽代碼
App() -> 執(zhí)行 -> VDOM -> DOM -> useLayoutEffect -> render -> useEffect
特點:
- useLayoutEffect性能更好,但是會影響用戶看到頁面變化的時間(代碼7)
- useLayoutEffect總是比useEffect先執(zhí)行
- useLayoutEffect里的任務最好是影響了layout
- 還是推薦優(yōu)先使用useEffect(如果不涉及操作dom的操作)
為什么建議將修改DOM的操作放到useLayoutEffect里,而不是useEffect呢,是因為當DOM被修改時,瀏覽器的線程處于被阻塞階段(js線程和瀏覽器線程互斥),所以還沒有發(fā)生回流、重繪。由于內(nèi)存中的DOM已經(jīng)被修改,通過useLayoutEffect可以拿到最新的DOM節(jié)點,并且在此時對DOM進行樣式上的修改。這樣修改一次性渲染到屏幕,依舊只有一次回流、重繪的代價。
注意:
由于useEffect是在render之后執(zhí)行,瀏覽器完成布局和繪制后,不應在函數(shù)中執(zhí)行阻塞瀏覽器更新屏幕的操作
useMemo
React默認有多余的render(修改n,但是依賴m的組件卻自動刷新了),如果props不變就沒有必要再執(zhí)行一次函數(shù)組件,先從一個例子來理解memo(代碼8)
這里有一個問題,如果給子組件一個方法,即使prop沒有變化,子組件還是會每一次都執(zhí)行
const onClickChild = () => {}
<Child data={m} onClick={onClickChild} />
這是因為在App重新渲染時,生成了新的函數(shù),就像一開始講的多個n的道理一樣,新舊函數(shù)雖然功能一樣,但是地址不一樣,這就導致props還是變化了
那么對于子組件的方法,如何重用?
使用useMemo鉤子(代碼9)
const onClickChild = useMemo(() => {
return () => {
console.log(m)
}
}, [m])
特點:
- useMemo第一個參數(shù)是
() => value(value可以是函數(shù)、對象之類的),第二個參數(shù)是依賴數(shù)組[m] - 只有當依賴變化時,才會重新計算新的value
- 如果依賴沒有變化,就重用之前的value
- 這不就是vue中的
computed嗎?
注意:
- 如果你的value是個函數(shù),那么你要寫成
useMemo(() => x => console.log(x)) - 這是一個返回函數(shù)的函數(shù)
- 這么難用的話,用用useCallback
// useMemo
const onClickChild = useMemo(() => {
return () => {
console.log(m)
}
}, [m])
// useCallback
const onClickChild = useCallback(() => {
console.log(m)
})
// 偽代碼
useCallback(x => log(x), [m]) 等價于 useMemo(() => x => log(x), [m])
useMemo和useCallback作用完全一樣,語法糖而已
useRef
一直用到的這個例子,每點擊一下就會重新渲染一下App
function App() {
console.log('App 執(zhí)行');
const [n, setN] = useState(0)
const onClick = () => {
setN(i => i + 1)
}
return (
<div>
<button onClick={onClick}>update n {n}</button>
</div>
)
}
假如我要知道這個App執(zhí)行了多少次,我怎么記錄?
如果我需要一個值,在組件不斷render的時候也能夠保持不變怎么做?
function App() {
// count的值通過useRef記錄了下來
// 初始化
const count = useRef(0)
useEffect(() => {
// 讀取 count.current
count.current += 1
})
}
同樣的,useRef也是通過它所對應的fiberNode對象來保存
為什么需要current?
- 為了保證兩次useRef是同一個值,只有引用才能做到
- useRef存儲的實際上是一個對象
{currnt: 0},對象對應的是同一個地址(內(nèi)存) - 每次改變只是改變對象中的值,而不是改變對象,新舊組件必須引用同一個對象
講了useRef就不得不講講forwardRef了
在函數(shù)組件中怎么使用ref,嘗試一下(代碼10)
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
說明,props無法傳遞ref屬性
所以,函數(shù)組件用ref的話,需要用forwardRef包裝做一下轉(zhuǎn)發(fā),才能拿到ref
自定義Hook
通過自定義Hook,可以將組件邏輯提取到可重用的函數(shù)中
自定義Hook是一個函數(shù),其名稱以 “use” 開頭(符合 Hook 的規(guī)則),函數(shù)內(nèi)部可以調(diào)用其他的Hook
每次使用自定義 Hook 時,其中的所有 state 和副作用都是完全隔離的(每次調(diào)用 Hook,它都會獲取獨立的 state)
代碼