以前的兩篇文章,分別介紹了react-hooks
如何使用,以及自定義hooks
設計模式及其實戰,本篇文章主要從react-hooks
起源,原理,源碼角度,開始剖析react-hooks
運行機制和內部原理,相信這篇文章事後,對於面試的時候那些hooks
問題,也就迎刃而解了。實際react-hooks
也並無那麼難以理解,聽起來很cool
,實際就是函數組件解決沒有state
,生命週期,邏輯不能複用的一種技術方案。前端
Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。react
老規矩,🤔️🤔️🤔️咱們帶着疑問開始今天的探討(能回答上幾個,本身能夠嘗試一下,掌握程度):git
react
用什麼方式記錄了hooks
的狀態?react-hooks
用什麼來記錄每個hooks
的順序的 ? 換個問法!爲何不能條件語句中,聲明hooks
? hooks
聲明爲何在組件的最頂部?function
函數組件中的useState
,和 class
類組件 setState
有什麼區別?react
是怎麼捕獲到hooks
的執行上下文,是在函數組件內部的?useEffect
,useMemo
中,爲何useRef
不須要依賴注入,就能訪問到最新的改變值?useMemo
是怎麼對值作緩存的?如何應用它優化性能?useState
的值相同,函數組件不更新?若是你認真讀完這篇文章,這些問題全會迎刃而解。github
在解釋react-hooks
原理的以前,咱們要加深理解一下, 函數組件和類組件到底有什麼區別,廢話很少說,咱們先看 兩個代碼片斷。面試
class Index extends React.Component<any,any>{
constructor(props){
super(props)
this.state={
number:0
}
}
handerClick=()=>{
for(let i = 0 ;i<5;i++){
setTimeout(()=>{
this.setState({ number:this.state.number+1 })
console.log(this.state.number)
},1000)
}
}
render(){
return <div> <button onClick={ this.handerClick } >num++</button> </div>
}
}
複製代碼
打印結果?算法
再來看看函數組件中:redux
function Index(){
const [ num ,setNumber ] = React.useState(0)
const handerClick=()=>{
for(let i=0; i<5;i++ ){
setTimeout(() => {
setNumber(num+1)
console.log(num)
}, 1000)
}
}
return <button onClick={ handerClick } >{ num }</button>
}
複製代碼
打印結果?設計模式
------------公佈答案-------------
前端工程化
在第一個例子🌰打印結果: 1 2 3 4 5數組
在第二個例子🌰打印結果: 0 0 0 0 0
這個問題實際很蒙人,咱們來一塊兒分析一下,第一個類組件中,因爲執行上setState
沒有在react
正常的函數執行上下文上執行,而是setTimeout
中執行的,批量更新條件被破壞。原理這裏我就不講了,因此能夠直接獲取到變化後的state
。
可是在無狀態組件中,彷佛沒有生效。緣由很簡單,在class
狀態中,經過一個實例化的class
,去維護組件中的各類狀態;可是在function
組件中,沒有一個狀態去保存這些信息,每一次函數上下文執行,全部變量,常量都從新聲明,執行完畢,再被垃圾機制回收。因此如上,不管setTimeout
執行多少次,都是在當前函數上下文執行,此時num = 0
不會變,以後setNumber
執行,函數組件從新執行以後,num
才變化。
因此, 對於class
組件,咱們只須要實例化一次,實例中保存了組件的state
等狀態。對於每一次更新只須要調用render
方法就能夠。可是在function
組件中,每一次更新都是一次新的函數執行,爲了保存一些狀態,執行一些反作用鉤子,react-hooks
應運而生,去幫助記錄組件的狀態,處理一些額外的反作用。
咱們從引入 hooks
開始,以useState
爲例子,當咱們從項目中這麼寫:
import { useState } from 'react'
複製代碼
因而乎咱們去找useState
,看看它究竟是哪路神仙?
react/src/ReactHooks.js
useState
export function useState(initialState){
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
複製代碼
useState()
的執行等於 dispatcher.useState(initialState)
這裏面引入了一個dispatcher
,咱們看一下resolveDispatcher
作了些什麼?
resolveDispatcher
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current
return dispatcher
}
複製代碼
ReactCurrentDispatcher
react/src/ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
current: null,
};
複製代碼
咱們看到ReactCurrentDispatcher.current
初始化的時候爲null
,而後就沒任何下文了。咱們暫且只能把**ReactCurrentDispatcher
**記下來。看看ReactCurrentDispatcher
何時用到的 ?
想要完全弄明白hooks
,就要從其根源開始,上述咱們在引入hooks
的時候,最後以一個ReactCurrentDispatcher
草草收尾,線索所有斷了,因此接下來咱們只能從函數組件執行開始。
對於function
組件是何時執行的呢?
react-reconciler/src/ReactFiberBeginWork.js
function
組件初始化:
renderWithHooks(
null, // current Fiber
workInProgress, // workInProgress Fiber
Component, // 函數組件自己
props, // props
context, // 上下文
renderExpirationTime,// 渲染 ExpirationTime
);
複製代碼
對於初始化是沒有current
樹的,以後完成一次組件更新後,會把當前workInProgress
樹賦值給current
樹。
function
組件更新:
renderWithHooks(
current,
workInProgress,
render,
nextProps,
context,
renderExpirationTime,
);
複製代碼
咱們從上邊能夠看出來,renderWithHooks
函數做用是調用function
組件函數的主要函數。咱們重點看看renderWithHooks
作了些什麼?
renderWithHooks react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks( current, workInProgress, Component, props, secondArg, nextRenderExpirationTime, ) {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
if (workInProgress.expirationTime === renderExpirationTime) {
// ....這裏的邏輯咱們先放一放
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
return children;
}
複製代碼
全部的函數組件執行,都是在這裏方法中,首先咱們應該明白幾個感念,這對於後續咱們理解useState
是頗有幫助的。
current fiber樹
: 當完成一次渲染以後,會產生一個current
樹,current
會在commit
階段替換成真實的Dom
樹。
workInProgress fiber樹
: 即將調和渲染的 fiber
樹。再一次新的組件更新過程當中,會從current
複製一份做爲workInProgress
,更新完畢後,將當前的workInProgress
樹賦值給current
樹。
workInProgress.memoizedState
: 在class
組件中,memoizedState
存放state
信息,在function
組件中,這裏能夠提早透漏一下,memoizedState
在一次調和渲染過程當中,以鏈表的形式存放hooks
信息。
workInProgress.expirationTime
: react
用不一樣的expirationTime
,來肯定更新的優先級。
currentHook
: 能夠理解 current
樹上的指向的當前調度的 hooks
節點。
workInProgressHook
: 能夠理解 workInProgress
樹上指向的當前調度的 hooks
節點。
renderWithHooks
函數主要做用:
首先先置空即將調和渲染的workInProgress
樹的memoizedState
和updateQueue
,爲何這麼作,由於在接下來的函數組件執行過程當中,要把新的hooks
信息掛載到這兩個屬性上,而後在組件commit
階段,將workInProgress
樹替換成current
樹,替換真實的DOM
元素節點。並在current
樹保存hooks
信息。
而後根據當前函數組件是不是第一次渲染,賦予ReactCurrentDispatcher.current
不一樣的hooks
,終於和上面講到的ReactCurrentDispatcher
聯繫到一塊兒。對於第一次渲染組件,那麼用的是HooksDispatcherOnMount
hooks對象。 對於渲染後,須要更新的函數組件,則是HooksDispatcherOnUpdate
對象,那麼兩個不一樣就是經過current
樹上是否memoizedState
(hook信息)來判斷的。若是current
不存在,證實是第一次渲染函數組件。
接下來,調用Component(props, secondArg);
執行咱們的函數組件,咱們的函數組件在這裏真正的被執行了,而後,咱們寫的hooks
被依次執行,把hooks
信息依次保存到workInProgress
樹上。 至於它是怎麼保存的,咱們立刻會講到。
接下來,也很重要,將ContextOnlyDispatcher
賦值給 ReactCurrentDispatcher.current
,因爲js
是單線程的,也就是說咱們沒有在函數組件中,調用的hooks
,都是ContextOnlyDispatcher
對象上hooks
,咱們看看ContextOnlyDispatcher
hooks,究竟是什麼。
const ContextOnlyDispatcher = {
useState:throwInvalidHookError
}
function throwInvalidHookError() {
invariant(
false,
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
}
複製代碼
原來如此,react-hooks
就是經過這種函數組件執行賦值不一樣的hooks
對象方式,判斷在hooks
執行是否在函數組件內部,捕獲並拋出異常的。
最後,從新置空一些變量好比currentHook
,currentlyRenderingFiber
,workInProgressHook
等。
hooks
對象上述講到在函數第一次渲染組件和更新組件分別調用不一樣的hooks
對象,咱們如今就來看看HooksDispatcherOnMount
和 HooksDispatcherOnUpdate
。
第一次渲染(我這裏只展現了經常使用的hooks
):
const HooksDispatcherOnMount = {
useCallback: mountCallback,
useEffect: mountEffect,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
};
複製代碼
更新組件:
const HooksDispatcherOnUpdate = {
useCallback: updateCallback,
useEffect: updateEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState
};
複製代碼
看來對於第一次渲染組件,和更新組件,react-hooks
採用了兩套Api
,本文的第二部分和第三部分,將重點二者的聯繫。
咱們用流程圖來描述整個過程:
本文將重點圍繞四個中重點hooks
展開,分別是負責組件更新的useState
,負責執行反作用useEffect
,負責保存數據的useRef
,負責緩存優化的useMemo
, 至於useCallback
,useReducer
,useLayoutEffect
原理和那四個重點hooks
比較相近,就不一一解釋了。
咱們先寫一個組件,而且用到上述四個主要hooks
:
請記住以下代碼片斷,後面講解將以以下代碼段展開
import React , { useEffect , useState , useRef , useMemo } from 'react'
function Index(){
const [ number , setNumber ] = useState(0)
const DivDemo = useMemo(() => <div> hello , i am useMemo </div>,[])
const curRef = useRef(null)
useEffect(()=>{
console.log(curRef.current)
},[])
return <div ref={ curRef } > hello,world { number } { DivDemo } <button onClick={() => setNumber(number+1) } >number++</button> </div>
}
複製代碼
接下來咱們一塊兒研究一下咱們上述寫的四個hooks
最終會變成什麼?
在組件初始化的時候,每一次hooks
執行,如useState()
,useRef()
,都會調用mountWorkInProgressHook
,mountWorkInProgressHook
到底作了寫什麼,讓咱們一塊兒來分析一下:
react-reconciler/src/ReactFiberHooks.js -> mountWorkInProgressHook
function mountWorkInProgressHook() {
const hook: Hook = {
memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 對象 | useMemo 中 保存的是緩存的值和deps | useRef中保存的是ref 對象
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) { // 例子中的第一個`hooks`-> useState(0) 走的就是這樣。
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
複製代碼
mountWorkInProgressHook
這個函數作的事情很簡單,首先每次執行一個hooks
函數,都產生一個hook
對象,裏面保存了當前hook
信息,而後將每一個hooks
以鏈表形式串聯起來,並賦值給workInProgress
的memoizedState
。也就證明了上述所說的,函數組件用memoizedState
存放hooks
鏈表。
至於hook
對象中都保留了那些信息?我這裏先分別介紹一下 :
memoizedState: useState中
保存 state
信息 | useEffect
中 保存着 effect
對象 | useMemo
中 保存的是緩存的值和 deps
| useRef
中保存的是 ref
對象。
baseQueue : usestate
和useReducer
中 保存最新的更新隊列。
baseState : usestate
和useReducer
中,一次更新中 ,產生的最新state
值。
queue : 保存待更新隊列 pendingQueue
,更新函數 dispatch
等信息。
next: 指向下一個 hooks
對象。
那麼當咱們函數組件執行以後,四個hooks
和workInProgress
將是如圖的關係。
知道每一個hooks
關係以後,咱們應該理解了,爲何不能條件語句中,聲明hooks
。
咱們用一幅圖表示若是在條件語句中聲明會出現什麼狀況發生。
若是咱們將上述demo
其中的一個 useRef
放入條件語句中,
let curRef = null
if(isFisrt){
curRef = useRef(null)
}
複製代碼
由於一旦在條件語句中聲明hooks
,在下一次函數組件更新,hooks
鏈表結構,將會被破壞,current
樹的memoizedState
緩存hooks
信息,和當前workInProgress
不一致,若是涉及到讀取state
等操做,就會發生異常。
上述介紹了 hooks
經過什麼來證實惟一性的,答案 ,經過hooks
鏈表順序。和爲何不能在條件語句中,聲明hooks
,接下來咱們按照四個方向,分別介紹初始化的時候發生了什麼?
mountState
function mountState( initialState ){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// 若是 useState 第一個參數爲函數,執行函數獲得state
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null, // 帶更新的
dispatch: null, // 負責更新函數
lastRenderedReducer: basicStateReducer, //用於獲得最新的 state ,
lastRenderedState: initialState, // 最後一次獲得的 state
});
const dispatch = (queue.dispatch = (dispatchAction.bind( // 負責更新的函數
null,
currentlyRenderingFiber,
queue,
)))
return [hook.memoizedState, dispatch];
}
複製代碼
mountState
到底作了些什麼,首先會獲得初始化的state
,將它賦值給mountWorkInProgressHook
產生的hook
對象的 memoizedState
和baseState
屬性,而後建立一個queue
對象,裏面保存了負責更新的信息。
這裏先說一下,在無狀態組件中,useState
和useReducer
觸發函數更新的方法都是dispatchAction
,useState
,能夠當作一個簡化版的useReducer
,至於dispatchAction
怎麼更新state
,更新組件的,咱們接着往下研究dispatchAction
。
在研究以前 咱們先要弄明白dispatchAction
是什麼?
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) 複製代碼
const [ number , setNumber ] = useState(0)
複製代碼
dispatchAction
就是 setNumber
, dispatchAction
第一個參數和第二個參數,已經被bind
給改爲currentlyRenderingFiber
和 queue
,咱們傳入的參數是第三個參數action
做爲更新的主要函數,咱們一下來研究一下,我把 dispatchAction
精簡,精簡,再精簡,
function dispatchAction(fiber, queue, action) {
// 計算 expirationTime 過程略過。
/* 建立一個update */
const update= {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
}
/* 把建立的update */
const pending = queue.pending;
if (pending === null) { // 證實第一次更新
update.next = update;
} else { // 不是第一次更新
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
/* 判斷當前是否在渲染階段 */
if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
didScheduleRenderPhaseUpdate = true;
update.expirationTime = renderExpirationTime;
currentlyRenderingFiber.expirationTime = renderExpirationTime;
} else { /* 當前函數組件對應fiber沒有處於調和渲染階段 ,那麼獲取最新state , 執行更新 */
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState = queue.lastRenderedState; /* 上一次的state */
const eagerState = lastRenderedReducer(currentState, action); /**/
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
return
}
}
}
}
scheduleUpdateOnFiber(fiber, expirationTime);
}
}
複製代碼
不管是類組件調用setState
,仍是函數組件的dispatchAction
,都會產生一個 update
對象,裏面記錄了這次更新的信息,而後將此update
放入待更新的pending
隊列中,dispatchAction
第二步就是判斷當前函數組件的fiber
對象是否處於渲染階段,若是處於渲染階段,那麼不須要咱們在更新當前函數組件,只須要更新一下當前update
的expirationTime
便可。
若是當前fiber
沒有處於更新階段。那麼經過調用lastRenderedReducer
獲取最新的state
,和上一次的currentState
,進行淺比較,若是相等,那麼就退出,這就證明了爲何useState
,兩次值相等的時候,組件不渲染的緣由了,這個機制和Component
模式下的setState
有必定的區別。
若是兩次state
不相等,那麼調用scheduleUpdateOnFiber
調度渲染當前fiber
,scheduleUpdateOnFiber
是react
渲染更新的主要函數。
咱們把初始化mountState
和無狀態組件更新機制講明白了,接下來看一下其餘的hooks初始化作了些什麼操做?
上述講到了無狀態組件中fiber
對象memoizedState
保存當前的hooks
造成的鏈表。那麼updateQueue
保存了什麼信息呢,咱們會在接下來探索useEffect
過程當中找到答案。 當咱們調用useEffect
的時候,在組件第一次渲染的時候會調用mountEffect
方法,這個方法到底作了些什麼?
function mountEffect( create, deps, ) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create, // useEffect 第一次參數,就是反作用函數
undefined,
nextDeps, // useEffect 第二次參數,deps
);
}
複製代碼
每一個hooks
初始化都會建立一個hook
對象,而後將hook的memoizedState
保存當前effect hook
信息。
有兩個memoizedState
你們千萬別混淆了,我這裏再友情提示一遍
workInProgress / current
樹上的 memoizedState
保存的是當前函數組件每一個hooks
造成的鏈表。
每一個hooks
上的memoizedState
保存了當前hooks
信息,不一樣種類的hooks
的memoizedState
內容不一樣。上述的方法最後執行了一個pushEffect
,咱們一塊兒看看pushEffect
作了些什麼?
function pushEffect(tag, create, destroy, deps) {
const effect = {
tag,
create,
destroy,
deps,
next: null,
};
let componentUpdateQueue = currentlyRenderingFiber.updateQueue
if (componentUpdateQueue === null) { // 若是是第一個 useEffect
componentUpdateQueue = { lastEffect: null }
currentlyRenderingFiber.updateQueue = componentUpdateQueue
componentUpdateQueue.lastEffect = effect.next = effect;
} else { // 存在多個effect
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
複製代碼
這一段實際很簡單,首先建立一個 effect
,判斷組件若是第一次渲染,那麼建立 componentUpdateQueue
,就是workInProgress
的updateQueue
。而後將effect
放入updateQueue
中。
假設咱們在一個函數組件中這麼寫:
useEffect(()=>{
console.log(1)
},[ props.a ])
useEffect(()=>{
console.log(2)
},[])
useEffect(()=>{
console.log(3)
},[])
複製代碼
最後workInProgress.updateQueue
會以這樣的形式保存:
effect list
能夠理解爲是一個存儲 effectTag
反作用列表容器。它是由 fiber
節點和指針 nextEffect
構成的單鏈表結構,這其中還包括第一個節點 firstEffect
,和最後一個節點 lastEffect
。 React
採用深度優先搜索算法,在 render
階段遍歷 fiber
樹時,把每個有反作用的 fiber
篩選出來,最後構建生成一個只帶反作用的 effect list
鏈表。 在 commit
階段,React
拿到 effect list
數據後,經過遍歷 effect list
,並根據每個 effect
節點的 effectTag
類型,執行每一個effect
,從而對相應的 DOM
樹執行更改。
不知道你們是否把 useMemo
想象的過於複雜了,實際相比其餘 useState
, useEffect
等,它的邏輯實際簡單的很。
function mountMemo(nextCreate,deps){
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
複製代碼
初始化useMemo
,就是建立一個hook
,而後執行useMemo
的第一個參數,獲得須要緩存的值,而後將值和deps
記錄下來,賦值給當前hook
的memoizedState
。總體上並無複雜的邏輯。
對於useRef
初始化處理,彷佛更是簡單,咱們一塊兒來看一下:
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
複製代碼
mountRef
初始化很簡單, 建立一個ref對象, 對象的current
屬性來保存初始化的值,最後用memoizedState
保存ref
,完成整個操做。
咱們來總結一下初始化階段,react-hooks
作的事情,在一個函數組件第一次渲染執行上下文過程當中,每一個react-hooks
執行,都會產生一個hook
對象,並造成鏈表結構,綁定在workInProgress
的memoizedState
屬性上,而後react-hooks
上的狀態,綁定在當前hooks
對象的memoizedState
屬性上。對於effect
反作用鉤子,會綁定在workInProgress.updateQueue
上,等到commit
階段,dom
樹構建完成,在執行每一個 effect
反作用鉤子。
上述介紹了第一次渲染函數組件,react-hooks
初始化都作些什麼,接下來,咱們分析一下,
對於更新階段,說明上一次 workInProgress
樹已經賦值給了 current
樹。存放hooks
信息的memoizedState
,此時已經存在current
樹上,react
對於hooks
的處理邏輯和fiber
樹邏輯相似。
對於一次函數組件更新,當再次執行hooks
函數的時候,好比 useState(0)
,首先要從current
的hooks
中找到與當前workInProgressHook
,對應的currentHooks
,而後複製一份currentHooks
給workInProgressHook
,接下來hooks
函數執行的時候,把最新的狀態更新到workInProgressHook
,保證hooks
狀態不丟失。
因此函數組件每次更新,每一次react-hooks
函數執行,都須要有一個函數去作上面的操做,這個函數就是updateWorkInProgressHook
,咱們接下來一塊兒看這個updateWorkInProgressHook
。
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) { /* 若是 currentHook = null 證實它是第一個hooks */
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else { /* 不是第一個hooks,那麼指向下一個 hooks */
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook
if (workInProgressHook === null) { //第一次執行hooks
// 這裏應該注意一下,當函數組件更新也是調用 renderWithHooks ,memoizedState屬性是置空的
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
/* 這個狀況說明 renderWithHooks 執行 過程發生屢次函數組件的執行 ,咱們暫時先不考慮 */
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook = { //建立一個新的hook
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) { // 若是是第一個hooks
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else { // 從新更新 hook
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
複製代碼
這一段的邏輯大體是這樣的:
hooks
函數,那麼從current
樹上取出memoizedState
,也就是舊的hooks
。nextWorkInProgressHook
,這裏應該值得注意,正常狀況下,一次renderWithHooks
執行,workInProgress
上的memoizedState
會被置空,hooks
函數順序執行,nextWorkInProgressHook
應該一直爲null
,那麼什麼狀況下nextWorkInProgressHook
不爲null
,也就是當一次renderWithHooks
執行過程當中,執行了屢次函數組件,也就是在renderWithHooks
中這段邏輯。if (workInProgress.expirationTime === renderExpirationTime) {
// ....這裏的邏輯咱們先放一放
}
複製代碼
這裏面的邏輯,實際就是斷定,若是當前函數組件執行後,當前函數組件的仍是處於渲染優先級,說明函數組件又有了新的更新任務,那麼循壞執行函數組件。這就形成了上述的,nextWorkInProgressHook
不爲 null
的狀況。
current
的hooks
,把它賦值給workInProgressHook
,用於更新新的一輪hooks
狀態。接下來咱們看一下四個種類的hooks
,在一次組件更新中,分別作了那些操做。
useState
function updateReducer( reducer, initialArg, init, ){
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 這裏省略... 第一步:將 pending queue 合併到 basequeue
}
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) { //優先級不足
const clone = {
expirationTime: update.expirationTime,
...
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
} else { //此更新確實具備足夠的優先級。
if (newBaseQueueLast !== null) {
const clone= {
expirationTime: Sync,
...
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
/* 獲得新的 state */
newState = reducer(newState, action);
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch = queue.dispatch
return [hook.memoizedState, dispatch];
}
複製代碼
這一段看起來很複雜,讓咱們慢慢吃透,首先將上一次更新的pending queue
合併到 basequeue
,爲何要這麼作,好比咱們再一次點擊事件中這麼寫,
function Index(){
const [ number ,setNumber ] = useState(0)
const handerClick = ()=>{
// setNumber(1)
// setNumber(2)
// setNumber(3)
setNumber(state=>state+1)
// 獲取上次 state = 1
setNumber(state=>state+1)
// 獲取上次 state = 2
setNumber(state=>state+1)
}
console.log(number) // 3
return <div> <div>{ number }</div> <button onClick={ ()=> handerClick() } >點擊</button> </div>
}
複製代碼
點擊按鈕, 打印 3
三次setNumber
產生的update
會暫且放入pending queue
,在下一次函數組件執行時候,三次 update
被合併到 baseQueue
。結構以下圖:
接下來會把當前useState
或是useReduer
對應的hooks
上的baseState
和baseQueue
更新到最新的狀態。會循環baseQueue
的update
,複製一份update
,更新 expirationTime
,對於有足夠優先級的update
(上述三個setNumber
產生的update
都具備足夠的優先級),咱們要獲取最新的state
狀態。,會一次執行useState
上的每個action
。獲得最新的state
。
更新state
這裏有會有兩個疑問🤔️:
action
不就能夠了嘛?答案: 緣由很簡單,上面說了 useState
邏輯和useReducer
差很少。若是第一個參數是一個函數,會引用上一次 update
產生的 state
, 因此須要循環調用,每個update
的reducer
,若是setNumber(2)
是這種狀況,那麼只用更新值,若是是setNumber(state=>state+1)
,那麼傳入上一次的 state
獲得最新state
。
updateExpirationTime < renderExpirationTime
)?答案: 這種狀況,通常會發生在,當咱們調用setNumber
時候,調用scheduleUpdateOnFiber
渲染當前組件時,又產生了一次新的更新,因此把最終執行reducer
更新state
任務交給下一次更新。
function updateEffect(create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.effectTag |= fiberEffectTag
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create,
destroy,
nextDeps,
);
}
複製代碼
useEffect
作的事很簡單,判斷兩次deps
相等,若是相等說明這次更新不須要執行,則直接調用 pushEffect
,這裏注意 effect
的標籤,hookEffectTag
,若是不相等,那麼更新 effect
,而且賦值給hook.memoizedState
,這裏標籤是 HookHasEffect | hookEffectTag
,而後在commit
階段,react
會經過標籤來判斷,是否執行當前的 effect
函數。
function updateMemo( nextCreate, deps, ) {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1]; // 以前保存的 deps 值
if (areHookInputsEqual(nextDeps, prevDeps)) { //判斷兩次 deps 值
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
複製代碼
在組件更新過程當中,咱們執行useMemo
函數,作的事情實際很簡單,就是判斷兩次 deps
是否相等,若是不想等,證實依賴項發生改變,那麼執行 useMemo
的第一個函數,獲得新的值,而後從新賦值給hook.memoizedState
,若是相等 證實沒有依賴項改變,那麼直接獲取緩存的值。
不過這裏有一點,值得注意,nextCreate()
執行,若是裏面引用了usestate
等信息,變量會被引用,沒法被垃圾回收機制回收,就是閉包原理,那麼訪問的屬性有可能不是最新的值,因此須要把引用的值,添加到依賴項 dep
數組中。每一次dep
改變,從新執行,就不會出現問題了。
舒適小提示: 有不少同窗說 useMemo
怎麼用,到底什麼場景用,用了會不會起到副作用,經過對源碼原理解析,我能夠明確的說,基本上能夠放心使用,說白了就是能夠定製化緩存,存值取值而已。
function updateRef(initialValue){
const hook = updateWorkInProgressHook()
return hook.memoizedState
}
複製代碼
函數組件更新useRef作的事情更簡單,就是返回了緩存下來的值,也就是不管函數組件怎麼執行,執行多少次,hook.memoizedState
內存中都指向了一個對象,因此解釋了useEffect
,useMemo
中,爲何useRef
不須要依賴注入,就能訪問到最新的改變值。
上面咱們從函數組件初始化,到函數組件更新渲染,兩個維度分解講解了react-hooks
原理,掌握了react-hooks
原理和內部運行機制,有助於咱們在工做中,更好的使用react-hooks
。
最後, 送人玫瑰,手留餘香,以爲有收穫的朋友能夠給筆者點贊,關注一波 ,陸續更新前端超硬核文章。
react-hooks三部曲另外兩部
玩轉react-hooks,自定義hooks設計模式及其實戰 205+
👍贊
react-hooks如何使用 120+
贊👍
react進階系列
「react進階」年終送給react開發者的八條優化建議 880+
贊👍
「react進階」一文吃透React高階組件(HOC) 300+
贊👍
react源碼系列
開源項目系列