長文預警,若是以爲前戲太長可直接從第三章開始看~javascript
本文基於 React 16.8.6 進行講解css
使用的示例代碼:html
import React, { useState } from 'react'
import './App.css'
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Star');
// 調用三次setCount便於查看更新隊列的狀況
const countPlusThree = () => {
setCount(count+1);
setCount(count+2);
setCount(count+3);
}
return (
<div className='App'> <p>{name} Has Clicked <strong>{count}</strong> Times</p> <button onClick={countPlusThree}>Click *3</button> </div>
)
}
複製代碼
代碼很是簡單,點擊button使count+3,count的值會顯示在屏幕上。前端
本節參考:How Are Function Components Different from Classes?java
本節主要概念:react
咱們來看一個簡單的Greeting組件,它支持定義成類和函數兩種性質。在使用它時,不用關心他是如何定義的。git
// 是類仍是函數 —— 無所謂
<Greeting /> // <p>Hello</p>
複製代碼
若是 Greeting
是一個函數,React 須要調用它。github
// Greeting.js
function Greeting() {
return <p>Hello</p>;
}
// React 內部
const result = Greeting(props); // <p>Hello</p>
複製代碼
但若是 Greeting
是一個類,React 須要先將其實例化,再調用剛纔生成實例的 render
方法:react-native
// Greeting.js
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
// React 內部
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
複製代碼
React經過如下方式來判斷組件的類型:數組
// React 內部
class Component {}
Component.prototype.isReactComponent = {};
// 檢查方式
class Greeting extends React.Component {}
console.log(Greeting.prototype.isReactComponent); // {}
複製代碼
本節主要概念(瞭解便可):
Fiber(可譯爲絲)比線程還細的控制粒度,是React 16中的新特性,旨在對渲染過程作更精細的調整。
產生緣由:
mount/update
,沒法中斷(持續佔用主線程),這樣主線程上的佈局、動畫等週期性任務以及交互響應就沒法當即獲得處理,影響體驗React Fiber的方式:
把一個耗時長的任務分紅不少小片,每個小片的運行時間很短,雖然總時間依然很長,可是在每一個小片執行完以後,都給其餘任務一個執行的機會,這樣惟一的線程就不會被獨佔,其餘任務依然有運行的機會。
React Fiber把更新過程碎片化,執行過程以下面的圖所示,每執行完一段更新過程,就把控制權交還給React負責任務協調的模塊,看看有沒有其餘緊急任務要作,若是沒有就繼續去更新,若是有緊急任務,那就去作緊急任務。
維護每個分片的數據結構,就是Fiber。
有了分片以後,更新過程的調用棧以下圖所示,中間每個波谷表明深刻某個分片的執行過程,每一個波峯就是一個分片執行結束交還控制權的時機。讓線程處理別的事情
Fiber的調度過程分爲如下兩個階段:
render/reconciliation階段 — 裏面的全部生命週期函數均可能被執行屢次,因此儘可能保證狀態不變
Commit階段 — 不能被打斷,只會執行一次
Fiber的增量更新須要更多的上下文信息,以前的vDOM tree顯然難以知足,因此擴展出了fiber tree(即Fiber上下文的vDOM tree),更新過程就是根據輸入數據以及現有的fiber tree構造出新的fiber tree(workInProgress tree)
與Fiber有關的全部代碼位於packages/react-reconciler中,一個Fiber節點的詳細定義以下:
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) {
// Instance
this.tag = tag; this.key = key; this.elementType = null;
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;
// 重點
this.memoizedState = null;
this.contextDependencies = null; this.mode = mode;
// Effects
/** 細節略 **/
}
複製代碼
咱們只關注一下this.memoizedState
這個key
用來存儲在上次渲染過程當中最終得到的節點的state
,每次render
以前,React會計算出當前組件最新的state
而後賦值給組件,再執行render
。— 類組件和使用useState的函數組件均適用。
記住上面這句話,後面還會常常提到memoizedState
有關Fiber每一個key的具體含義能夠參見源碼的註釋
本節主要概念:
因爲React體系的複雜性以及目標平臺的多樣性。react
包只暴露一些定義組件的API。絕大多數React的實現都存在於 渲染器(renderers)中。
react-dom
、react-dom/server
、 react-native
、 react-test-renderer
、 react-art
都是常見的渲染器
這就是爲何無論目標平臺是什麼,react
包都是可用的。從react
包中導出的一切,好比React.Component
、React.createElement
、 React.Children
和 Hooks
都是獨立於目標平臺的。不管運行React DOM,仍是 React DOM Server,或是 React Native,組件均可以使用一樣的方式導入和使用。
因此當咱們想使用新特性時,react
和 react-dom
都須要被更新。
例如,當React 16.3添加了Context API,
React.createContext()
API會被React包暴露出來。 可是React.createContext()
其實並無_實現_ context。由於在React DOM 和 React DOM Server 中一樣一個 API 應當有不一樣的實現。因此createContext()
只返回了一些普通對象: **因此,若是你將react升級到了16.3+,可是不更新react-dom,那麼你就使用了一個尚不知道Provider 和 Consumer類型的渲染器。**這就是爲何老版本的react-dom
會報錯說這些類型是無效的。
這就是setState
儘管定義在React包中,調用時卻可以更新DOM的緣由。它讀取由React DOM設置的this.updater
,讓React DOM安排並處理更新。
Component.setState = function(partialState, callback) {
// setState所作的一切就是委託渲染器建立這個組件的實例
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼
各個渲染器中的updater觸發不一樣平臺的更新渲染
// React DOM 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMUpdater;
// React DOM Server 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactDOMServerUpdater;
// React Native 內部
const inst = new YourComponent();
inst.props = props;
inst.updater = ReactNativeUpdater;
複製代碼
至於updater的具體實現,就不是這裏重點要討論的內容了,下面讓咱們正式進入本文的主題:React Hooks
本節主要概念:
全部的Hooks在React.js
中被引入,掛載在React對象中
// React.js
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from './ReactHooks';
複製代碼
咱們進入ReactHooks.js
來看看,發現useState
的實現居然異常簡單,只有短短兩行
// ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
複製代碼
看來重點都在這個dispatcher
上,dispatcher
經過resolveDispatcher()
來獲取,這個函數一樣也很簡單,只是將ReactCurrentDispatcher.current
的值賦給了dispatcher
// ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
複製代碼
因此useState(xxx)
等價於 ReactCurrentDispatcher.current.useState(xxx)
看到這裏,咱們回顧一下第一章第三小節所講的React渲染器與setState,是否是發現有點似曾相識。
與updater是setState可以觸發更新的核心相似,ReactCurrentDispatcher.current.useState
是useState
可以觸發更新的關鍵緣由,這個方法的實現並不在react包內。下面咱們就來分析一個具體更新的例子。
以全文開頭給出的代碼爲例。
咱們從Fiber調度的開始:ReactFiberBeginwork
來談起
以前已經說過,React有能力區分不一樣的組件,因此它會給不一樣的組件類型打上不一樣的tag, 詳見shared/ReactWorkTags.js。
因此在beginWork的函數中,就能夠根據workInProgess(就是個Fiber節點)上的tag值來走不一樣的方法來加載或者更新組件。
// ReactFiberBeginWork.js
function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null {
/** 省略與本文無關的部分 **/
// 根據不一樣的組件類型走不一樣的方法
switch (workInProgress.tag) {
// 不肯定組件
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
// 加載初始組件
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
// 函數組件
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 更新函數組件
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 類組件
case ClassComponent {
/** 細節略 **/
}
}
複製代碼
下面咱們來找出useState發揮做用的地方。
mount過程執行mountIndeterminateComponent
時,會執行到renderWithHooks
這個函數
function mountIndeterminateComponent( _current, workInProgress, Component, renderExpirationTime, ) {
/** 省略準備階段代碼 **/
// value就是渲染出來的APP組件
let value;
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderExpirationTime,
);
/** 省略無關代碼 **/
}
workInProgress.tag = FunctionComponent;
reconcileChildren(null, workInProgress, value, renderExpirationTime);
return workInProgress.child;
}
複製代碼
執行前: nextChildren = value
執行後: value= 組件的虛擬DOM表示
至於這個value是如何被渲染成真實的DOM節點,咱們並不關心,state值咱們已經經過renderWithHooks取到並渲染
點擊一下按鈕:此時count從0變爲3
更新過程執行的是updateFunctionComponent函數,一樣會執行到renderWithHooks這個函數,咱們來看一下這個函數執行先後發生的變化:
執行前: nextChildren = undefined
**執行後:**nextChildren=更新後的組件的虛擬DOM表示
一樣的,至於這個nextChildren是如何被渲染成真實的DOM節點,咱們並不關心,最新的state值咱們已經經過renderWithHooks取到並渲染
因此,renderWithHooks
函數就是處理各類hooks邏輯的核心部分
ReactFiberHooks.js包含着各類關於Hooks邏輯的處理,本章中的代碼均來自該文件。
在以前的章節有介紹過,Fiber中的memorizedStated
用來存儲state
在類組件中state
是一整個對象,能夠和memoizedState
一一對應。可是在Hooks
中,React並不知道咱們調用了幾回useState
,因此React經過將一個Hook對象掛載在memorizedStated
上來保存函數組件的state
Hook對象的結構以下:
// ReactFiberHooks.js
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
複製代碼
重點關注memoizedState
和next
memoizedState
是用來記錄當前useState
應該返回的結果的queue
:緩存隊列,存儲屢次更新行爲next
:指向下一次useState
對應的Hook對象。結合示例代碼來看:
import React, { useState } from 'react'
import './App.css'
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Star');
// 調用三次setCount便於查看更新隊列的狀況
const countPlusThree = () => {
setCount(count+1);
setCount(count+2);
setCount(count+3);
}
return (
<div className='App'> <p>{name} Has Clicked <strong>{count}</strong> Times</p> <button onClick={countPlusThree}>Click *3</button> </div>
)
}
複製代碼
第一次點擊按鈕觸發更新時,memoizedState的結構以下
只是符合以前對Hook對象結構的分析,只是queue中的結構貌似有點奇怪,咱們將在第三章第2節中進行分析。
renderWithHooks的運行過程以下:
// ReactFiberHooks.js
export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: any, props: any, refOrContext: any, nextRenderExpirationTime: ExpirationTime, ): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
// 若是current的值爲空,說明尚未hook對象被掛載
// 而根據hook對象結構可知,current.memoizedState指向下一個current
nextCurrentHook = current !== null ? current.memoizedState : null;
// 用nextCurrentHook的值來區分mount和update,設置不一樣的dispatcher
ReactCurrentDispatcher.current =
nextCurrentHook === null
// 初始化時
? HooksDispatcherOnMount
// 更新時
: HooksDispatcherOnUpdate;
// 此時已經有了新的dispatcher,在調用Component時就能夠拿到新的對象
let children = Component(props, refOrContext);
// 重置
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
// 更新memoizedState和updateQueue
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.updateQueue = (componentUpdateQueue: any);
/** 省略與本文無關的部分代碼,便於理解 **/
}
複製代碼
核心: 建立一個新的hook,初始化state, 並綁定觸發器
初始化階段ReactCurrentDispatcher.current
會指向HooksDispatcherOnMount
對象
// ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
/** 省略其它Hooks **/
useState: mountState,
};
// 因此調用useState(0)返回的就是HooksDispatcherOnMount.useState(0),也就是mountState(0)
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// 訪問Hook鏈表的下一個節點,獲取到新的Hook對象
const hook = mountWorkInProgressHook();
//若是入參是function則會調用,可是不提供參數
if (typeof initialState === 'function') {
initialState = initialState();
}
// 進行state的初始化工做
hook.memoizedState = hook.baseState = initialState;
// 進行queue的初始化工做
const queue = (hook.queue = {
last: null,
dispatch: null,
eagerReducer: basicStateReducer, // useState使用基礎reducer
eagerState: (initialState: any),
});
// 返回觸發器
const dispatch: Dispatch<BasicStateAction<S>,>
= (queue.dispatch = (dispatchAction.bind(
null,
//綁定當前fiber結點和queue
((currentlyRenderingFiber: any): Fiber),
queue,
));
// 返回初始state和觸發器
return [hook.memoizedState, dispatch];
}
// 對於useState觸發的update action來講(假設useState裏面都傳的變量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
複製代碼
重點講一下返回的這個更新函數 dispatchAction
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) {
/** 省略Fiber調度相關代碼 **/
// 建立新的新的update, action就是咱們setCount裏面的值(count+1, count+2, count+3…)
const update: Update<S, A> = {
expirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 重點:構建query
// queue.last是最近的一次更新,而後last.next開始是每一次的action
const last = queue.last;
if (last === null) {
// 只有一個update, 本身指本身-造成環
update.next = update;
} else {
const first = last.next;
if (first !== null) {
update.next = first;
}
last.next = update;
}
queue.last = update;
/** 省略特殊狀況相關代碼 **/
// 建立一個更新任務
scheduleWork(fiber, expirationTime);
}
複製代碼
在dispatchAction
中維護了一份query的數據結構。
query是一個有環鏈表,規則:
因此每次插入新update時,就須要將原來的first指向query.last.next。再將update指向query.next,最後將query.last指向update.
下面結合示例代碼來畫圖說明一下:
前面給出了第一次點擊按鈕更新時,memorizedState中的query值
其構建過程以下圖所示:
即保證query.last始終爲最新的action, 而query.last.next始終爲action: 1
核心:獲取該Hook對象中的 queue,內部存有本次更新的一系列數據,進行更新
更新階段 ReactCurrentDispatcher.current
會指向HooksDispatcherOnUpdate
對象
// ReactFiberHooks.js
// 因此調用useState(0)返回的就是HooksDispatcherOnUpdate.useState(0),也就是updateReducer(basicStateReducer, 0)
const HooksDispatcherOnUpdate: Dispatcher = {
/** 省略其它Hooks **/
useState: updateState,
}
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
// 能夠看到updateReducer的過程與傳的initalState已經無關了,因此初始值只在第一次被使用
// 爲了方便閱讀,刪去了一些無關代碼
// 查看完整代碼:https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberHooks.js#L606
function updateReducer(reducer, initialArg, init) {
// 獲取初始化時的 hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
// 開始渲染更新
if (numberOfReRenders > 0) {
const dispatch = queue.dispatch;
if (renderPhaseUpdates !== null) {
// 獲取Hook對象上的 queue,內部存有本次更新的一系列數據
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
// 獲取更新後的state
do {
const action = update.action;
// 此時的reducer是basicStateReducer,直接返回action的值
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
// 對 更新hook.memoized
hook.memoizedState = newState;
// 返回新的 state,及更新 hook 的 dispatch 方法
return [newState, dispatch];
}
}
}
// 對於useState觸發的update action來講(假設useState裏面都傳的變量),basicStateReducer就是直接返回action的值
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
複製代碼
單個hooks的更新行爲全都掛在Hooks.queue下,因此可以管理好queue的核心就在於
結合示例代碼:
[count, setCount] = useState(0)
時,建立一個queuesetCount(x)
,就dispach一個內容爲x的action(action的表現爲:將count設爲x),action存儲在queue中,之前面講述的有環鏈表規則來維護updateReducer
中被調用,更新到memorizedState
上,使咱們可以獲取到最新的state值。官方文檔對於使用hooks有如下兩點要求:
以useState爲例:
和類組件存儲state不一樣,React並不知道咱們調用了幾回useState
,對hooks的存儲是按順序的(參見Hook結構),一個hook對象的next指向下一個hooks。因此當咱們創建示例代碼中的對應關係後,Hook的結構以下:
// hook1: const [count, setCount] = useState(0) — 拿到state1
{
memorizedState: 0
next : {
// hook2: const [name, setName] = useState('Star') - 拿到state2
memorizedState: 'Star'
next : {
null
}
}
}
// hook1 => Fiber.memoizedState
// state1 === hook1.memoizedState
// hook1.next => hook2
// state2 === hook2.memoizedState
複製代碼
因此若是把hook1放到一個if語句中,當這個沒有執行時,hook2拿到的state實際上是上一次hook1執行後的state(而不是上一次hook2執行後的)。這樣顯然會發生錯誤。
關於這塊內容若是想了解更多能夠看一下這篇文章
只有函數組件的更新纔會觸發renderWithHooks函數,處理Hooks相關邏輯。
仍是以setState爲例,類組件和函數組件從新渲染的邏輯不一樣 :
類組件: 用setState觸發updater,從新執行組件中的render方法
函數組件: 用useState返回的setter函數來dispatch一個update action,觸發更新(dispatchAction最後的scheduleWork),用updateReducer處理更新邏輯,返回最新的state值(與Redux比較像)
說了這麼多,最後再簡要總結下useState的執行流程~
初始化: 構建dispatcher函數和初始值
更新時:
ReactCurrentDispatcher.current
指向負責更新的DispatcheruseState
會被從新執行,在resolve dispatcher的階段拿到了負責更新的dispatcher。useState
會拿到Hook對象,Hook.query
中存儲了更新隊列,依次進行更新後,便可拿到最新的statememorizedState
也被設置爲最新的state關於咱們:
咱們是螞蟻保險體驗技術團隊,來自螞蟻金服保險事業羣。咱們是一個年輕的團隊(沒有歷史技術棧包袱),目前平均年齡92年(去除一個最高分8x年-團隊leader,去除一個最低分97年-實習小老弟)。咱們支持了阿里集團幾乎全部的保險業務。18年咱們產出的相互寶轟動保險界,19年咱們更有多個重量級項目籌備動員中。現伴隨着事業羣的高速發展,團隊也在迅速擴張,歡迎各位前端高手加入咱們~
咱們但願你是:技術上基礎紮實、某領域深刻(Node/互動營銷/數據可視化等);學習上善於沉澱、持續學習;性格上樂觀開朗、活潑外向。
若有興趣加入咱們,歡迎發送簡歷至郵箱:xingyan.hyx@antfin.com
本文做者:螞蟻保險-體驗技術組-星焰
掘金地址:STAR🌟