React 中一般使用 類定義 或者 函數定義 建立組件:html
在類定義中,咱們可使用到許多 React 特性,例如 state、 各類組件生命週期鉤子等,可是在函數定義中,咱們卻無能爲力,所以 React 16.8 版本推出了一個新功能 (React Hooks),經過它,能夠更好的在函數定義組件中使用 React 特性。node
好處:react
一、跨組件複用: 其實 render props / HOC 也是爲了複用,相比於它們,Hooks 做爲官方的底層 API,最爲輕量,並且改形成本小,不會影響原來的組件層次結構和傳說中的嵌套地獄;git
二、類定義更爲複雜: 不一樣的生命週期會使邏輯變得分散且混亂,不易維護和管理; 時刻須要關注this的指向問題; 代碼複用代價高,高階組件的使用常常會使整個組件樹變得臃腫;github
三、狀態與UI隔離: 正是因爲 Hooks 的特性,狀態邏輯會變成更小的粒度,而且極容易被抽象成一個自定義 Hooks,組件中的狀態和 UI 變得更爲清晰和隔離。算法
注意:編程
避免在 循環/條件判斷/嵌套函數 中調用 hooks,保證調用順序的穩定; 只有 函數定義組件 和 hooks 能夠調用 hooks,避免在 類組件 或者 普通函數 中調用; 不能在useEffect中使用useState,React 會報錯提示; 類組件不會被替換或廢棄,不須要強制改造類組件,兩種方式能並存;redux
重要鉤子:數組
咱們可使用Array模擬useState的原理,正如文章React hooks: not magic, just arrays所說,可是React底層真實的實現,是利用的鏈表,這裏咱們下面會說到瀏覽器
當調用 useState 的時候,會返回形如 (變量, 函數) 的一個元祖。而且 state 的初始值就是外部調用 useState 的時候,傳入的參數。
理清楚了傳參和返回值,再來看下 useState 還作了些什麼。正以下面代碼所示,當點擊按鈕的時候,執行setNum,狀態 num 被更新,而且 UI 視圖更新。顯然,useState 返回的用於更改狀態的函數,自動調用了render方法來觸發視圖更新。
function App() {
const [num, setNum] = useState(0);
return (
<div> <div>num: {num}</div> <button onClick={() => setNum(num + 1)}>加 1</button> </div>
);
}
複製代碼
初步模擬
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}
let state;
function useState(initialState){
state = state || initialState;
function setState(newState) {
state = newState;
render();
}
return [state, setState];
}
render(); // 首次渲染
複製代碼
初步模擬讓咱們發現了Hooks的第一個核心原理:閉包
,是的Hooks返回的state
和setState
方法,在hooks內部都是利用閉包實現的
可是真實的useXXX
都是能夠屢次聲明使用的,因此咱們這裏的初步實現並不支持對多個變量聲明
首先,利用Array模擬React Hook原理
前面 useState 的簡單實現裏,初始的狀態是保存在一個全局變量中的。以此類推,多個狀態,應該是保存在一個專門的全局容器中。這個容器,就是一個樸實無華的 Array 對象。具體過程以下:
舉例:
function RenderFunctionComponent() {
const [firstName, setFirstName] = useState("Rudi");
const [lastName, setLastName] = useState("Yardley");
return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
複製代碼
上面代碼的建立流程
1)初始化
建立兩個Array, setters
and state
,設置遊標cursor = 0;
2) 首次渲染
遍歷全部的useState
,將setters
push進入數組,將state
push進入狀態數組
3)重渲染
後續的每次重渲染都會重置遊標cursor = 0,並依次從數組中取出以前的state
4)事件觸發
每一個事件都有對應遊標的state值,任何state事件觸發,都會修改state數組中對應的state值
完整模擬useState
import React from "react";
import ReactDOM from "react-dom";
const states = [];
let cursor = 0;
function useState(initialState) {
const currenCursor = cursor;
states[currenCursor] = states[currenCursor] || initialState; // 檢查是否渲染過
function setState(newState) {
states[currenCursor] = newState;
render();
}
cursor+=1; // 更新遊標
return [states[currenCursor], setState];
}
function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(1);
return (
<div> <div>count1: {count1}</div> <div> <button onClick={() => setCount1(count1 + 1)}>add1 1</button> <button onClick={() => setCount1(count1 - 1)}>delete1 1</button> </div> <hr /> <div>num2: {num2}</div> <div> <button onClick={() => setCount2(count2 + 1)}>add2 1</button> <button onClick={() => setCount2(count2 - 1)}>delete2 1</button> </div> </div>
);
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
cursor = 0; // 重置cursor
}
render(); // 首次渲染
複製代碼
若是在循環,判斷中使用Hooks
let firstRender = true;
function RenderFunctionComponent() {
let initName;
if(firstRender){
[initName] = useState("Rudi");
firstRender = false;
}
const [firstName, setFirstName] = useState(initName);
const [lastName, setLastName] = useState("Yardley");
return (
<Button onClick={() => setFirstName("Fred")}>Fred</Button>
);
}
複製代碼
建立流程示意圖
重渲染
能夠看到,由於firstRender
的條件判斷,遊標爲0的state值會被異常設置成useState(initName)
,遊標爲1的state會被異常設置成useState("Yardley")
,而其實Yardley
是遊標爲2的state的值
也就是說初始化組件的時候,hooks會直接維護一套數組,對應相應的state和setState方法,若是在條件渲染中使用,會致使重渲染的時候,異常的遊標對應,異常的遊標對應也會致使調用的setState方法失效
useEffect
多是咱們在使用hooks的時候,使用頻率僅次於useState
的的鉤子方法了,它的做用是反作用
,說直白就是某些state或者props變化的時候,須要監聽並執行相應的操做,那麼咱們就須要使用useEffect
了,對標就是Class組件中的componentDidMount,componentDidUpdate,componentWillUnmount方法的集合
模擬實現(依然是利用Array + Cursor的思路)
const allDeps = [];
let effectCursor = 0;
function useEffect(callback, deps = []) {
if (!allDeps[effectCursor]) {
// 初次渲染:賦值 + 調用回調函數
allDeps[effectCursor] = deps;
effectCursor+=1;
callback();
return;
}
const currenEffectCursor = effectCursor;
const rawDeps = allDeps[currenEffectCursor];
// 檢測依賴項是否發生變化,發生變化須要從新render
const isChanged = rawDeps.some(
(dep,index) => dep !== deps[index]
);
// 依賴變化
if (isChanged) {
// 執行回調
callback();
// 修改新的依賴
allDeps[effectCursor] = deps;
}
// 遊標遞增
effectCursor+=1;
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
effectCursor = 0; // 注意將 effectCursor 重置爲0
}
複製代碼
咱們用數組模擬出了Hooks的實現原理,可是React的真實實現是用單鏈表
的形式代替數組的,經過next
串聯起全部的hook
首先讓咱們看一張圖
dispatcher 是一個包含了 hooks 函數的共享對象。它將基於 ReactDOM 的渲染階段被動態地分配或清理,而且它將確保用戶沒法在React組件外訪問到Hooks,源碼參考
hooks
在啓用時被一個叫作enableHooks
的標誌位變量啓用或禁用,在渲染根組件時,判斷該標誌位並簡單的切換到合適的 dispatcher 上,源碼參考
部分源碼
function renderRoot(root: FiberRoot, isYieldy: boolean): void {
invariant(
!isWorking,
'renderRoot was called recursively. This error is likely caused ' +
'by a bug in React. Please file an issue.',
);
flushPassiveEffects();
isWorking = true;
// 控制hooks的當前Dispatcher
if (enableHooks) {
ReactCurrentOwner.currentDispatcher = Dispatcher;
} else {
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
}
...
複製代碼
當完成渲染後,dispatcher將被置爲null,這是爲了防止在ReactDOM的渲染外被異常訪問,源碼參考
部分源碼
// We're done performing work. Time to clean up.
isWorking = false;
ReactCurrentOwner.currentDispatcher = null;
resetContextDependences();
resetHooks();
複製代碼
在Hooks內部,使用resolveDispatcher
方法解析當前的dispatcher
引用,若是當前的dispatcher
異常,則會報錯
部分源碼
function resolveDispatcher() {
const dispatcher = ReactCurrentOwner.currentDispatcher;
invariant(
dispatcher !== null,
'Hooks can only be called inside the body of a function component.',
);
return dispatcher;
}
複製代碼
能夠說Dispatcher是Hooks機制下的對外統一暴露控制器,渲染過程當中,經過flag標誌控制當前的上下文dispatcher
,核心意義就是嚴格控制hooks的調用渲染,防止hooks在異常的地方被調用了
hooks的表現是:按照調用順序被連接在一塊兒的節點(nodes)。總結一下hooks的一些屬性
因此咱們看Hooks的時候,就不能單純的認爲每一個hook節點是一個對象,而是一個鏈表節點,而整個hooks模型,則是一個隊列
{
memoizedState: 'foo',
next: {
memoizedState: 'bar',
next: {
memoizedState: 'baz',
next: null
}
}
}
複製代碼
咱們能夠看到源碼對於一個Hook和Effect模型的定義,源碼
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any> | null,
queue: UpdateQueue<any> | null,
next: Hook | null,
};
type Effect = {
tag: HookEffectTag,
create: () => mixed,
destroy: (() => mixed) | null,
inputs: Array<mixed>,
next: Effect,
};
...
export function useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initializers
(initialState: any),
);
}
複製代碼
首先,能夠看到useState的實現就是useReducer的某一種狀況的實現,因此在官方文檔上,也說了useReducer是useState的另一種實現方案,結合了Redux的思想,能夠避免過多的傳遞迴調函數,而能夠直接傳遞dispatch到深層次的組件中去 官網關於useReducer的說明
這裏我仍是貼上關於useReducer的用法案例,其實主要是能理解redux
或者dva
的原理和用法,就能夠對標useReducer
的用法
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </>
);
}
複製代碼
回到Hook的定義,咱們如今就能夠對每一個參數進行說明了
memoizedState
:hook更新後的緩存statebaseState
:初始化initialStatebaseUpdate
:最近一次調用更新state方法的actionqueue
:調度操做的隊列,等待進入reducernext
:link到下一個hook,經過next串聯每一個hook對於fiber實現,這裏不作詳細解釋,咱們這裏只須要知道,React在V16中,對組件構建渲染的機制,從棧
模式改成了fiber
模式,變成了具備鏈表和指針的單鏈表樹遍歷算法。經過指針映射,每一個單元都記錄着遍歷當下的上一步和下一步,從而使遍歷變得能夠被暫停或者重啓 這裏的理解就是一種任務分割調度算法,將原先同步更新渲染的任務分割成一個個獨立的小任務單位,根據不一樣的優先級,將小任務分散到瀏覽器的空閒時間執行,充分利用主進程的時間循環機制
fiber簡單說概念,就是組件渲染的一個基礎任務切割單元,裏面包含了當前組件構建的最基礎的一個任務內容單元
其中,要提到一個很重要的概念memoizedState
,這個字段是否是很眼熟,上面關於hook的定義裏面,也有這個字段,是的,fiber數據結構中,也有這個字段,在fiber中,memoizedState
的意義就是指向屬於這個fiber的hooks隊列的首個hook,而hook中的memoizedState
則指的是當前hook緩存的state值(這裏筆者在看一些博客的時候,發現有的博主把兩個數據結構中的同名字段搞混淆了)
咱們能夠看源碼
// There's no existing queue, so this is the initial render.
if (reducer === basicStateReducer) {
// Special case for `useState`.
if (typeof initialState === 'function') {
initialState = initialState();
}
} else if (initialAction !== undefined && initialAction !== null) {
initialState = reducer(initialState, initialAction);
}
// 注意:重點
workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
複製代碼
上面能夠看到,initialState
做爲初始state值,被同時賦值給了baseState
和memoizedState
再看三段段段源碼,源碼連接
// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let firstCurrentHook: Hook | null = null;
let currentHook: Hook | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
複製代碼
export function prepareToUseHooks( current: Fiber | null, workInProgress: Fiber, nextRenderExpirationTime: ExpirationTime, ): void {
if (!enableHooks) {
return;
}
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
firstCurrentHook = current !== null ? current.memoizedState : null;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// remainingExpirationTime = NoWork;
// componentUpdateQueue = null;
// isReRender = false;
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
}
複製代碼
export function finishHooks( Component: any, props: any, children: any, refOrContext: any, ): any {
if (!enableHooks) {
return children;
}
// This must be called after every function component to prevent hooks from
// being used in classes.
while (didScheduleRenderPhaseUpdate) {
// Updates were scheduled during the render phase. They are stored in
// the `renderPhaseUpdates` map. Call the component again, reusing the
// work-in-progress hooks and applying the additional updates on top. Keep
// restarting until no more updates are scheduled.
didScheduleRenderPhaseUpdate = false;
numberOfReRenders += 1;
// Start over from the beginning of the list
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;
children = Component(props, refOrContext);
}
renderPhaseUpdates = null;
numberOfReRenders = 0;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.expirationTime = remainingExpirationTime;
renderedWork.updateQueue = (componentUpdateQueue: any);
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
firstCurrentHook = null;
currentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
...
複製代碼
其中一段源碼有這麼一段註釋:Hooks are stored as a linked list on the fiber's memoizedState field.
,大概意思是Hooks是以鏈表的形式儲存在fiber
的memoizedState
字段中
第二段代碼是fiber中,hook執行前置函數
第三段代碼是fiber中,hook執行後置函數,方法中有這麼一句renderedWork.memoizedState = firstWorkInProgressHook;
1)Hook數據結構中和fiber數據結構中都有memoizedState
字段,可是表達的意義不一樣,Hook中是做爲緩存的state值,可是fiber中是指向的當前fiber下的hooks隊列的首個hook(hook是鏈表結構,指向首個,就意味着能夠訪問整個hooks隊列)
2)fiber中調用hook的時候,會先調用一個前置函數,其中 currentlyRenderingFiber = workInProgress;
firstCurrentHook = current !== null ? current.memoizedState : null;
這兩句代碼分別將當前渲染的fiber
和當前執行的hooks隊列的首個hook賦值給了當前的全局變量currentlyRenderingFiber
和firstCurrentHook
再看下關於currentlyRenderingFiber
變量的源碼說明
// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber | null = null;
複製代碼
currentlyRenderingFiber
就是定義當前正在渲染中的fiber結構
3)fiber調用hooks結束的時候,會調用finishHooks
方法,能夠看到,會將當前fiber的memoizedState
字段存入firstWorkInProgressHook
,也就是將hooks隊列的首個hook存入,而後將currentlyRenderingFiber
字段置爲null
從當下的環境來看,Hooks已經逐漸成爲主流組件方式,好比Ant4.x的組件,已經全面推薦Hooks模式,Hooks的有點主要在於精簡的編碼模式,函數式編程思想,而Calss組件的主要優勢在於【完整】,【精準】的組件流程控制,包括可使用shouldComponentUpdate
等生命週期對渲染作嚴格控制
在業務開發時的思考模式是:【先作什麼,再作什麼】,this.setState
第二個回調的參數,就是這種思想的絕對體現,而後配合【生命週期函數】完成一整個組件的功能,對於組件封裝和複用的角度,HOC模式也必須依賴Class實現
對標Class組件,使用Hooks須要有一個編程思路上的轉變,Hooks的業務開發的思考模式是:【依賴】,【反作用】
全部的狀態維護好後,須要思考的就是圍繞這些狀態產生的【反作用】,個人什麼state或者props變了以後,對應的【反作用】須要幹什麼事,也是這種設計理念下,useEffect
的功能能夠直接對標Class組件的componentDidMount
,componentDidUpdate
,componentWillUnmount
方法的集合
可是Class在目前來講任有不可替代性,由於Class擁有完整的生命週期控制,好比shouldComponentUpdate
等生命週期,而Hooks則沒法作到如此精細化的控制
經過 Hooks 咱們能夠對 state 邏輯進行良好的封裝,輕鬆作到隔離和複用,優勢主要體如今:
【Tips:function組件沒有實例,因此沒法經過ref控制,可是Hooks下可使用React.forwardRef
來將ref傳遞給function組件,而後經過useImperativeHandle
將子組件的實例操做,選擇性的暴露給父組件】
參考: