主頁 > 企業開發 > [通明境 · React架構]通俗地講React,優雅地理解React

[通明境 · React架構]通俗地講React,優雅地理解React

2022-09-20 08:26:51 企業開發

1 前言

大家好,我是心鎖,一枚23屆準畢業生,

如果讀者閱讀過我其他幾篇React相關的文章,就知道這次我是來填坑的了

原因是,寫了兩篇解讀react-hook的文章后我發現——并不是每位同學都清楚React的架構,包括我在內也只是綜合不同技術文章與閱讀部分原始碼有一個了解,但是除錯時真正沉淀成文章的還沒有,

B583B6CBE8F38DE4BCC790B448AE4848.jpg

所以這篇文章來啦~文章基于2022年八九月的React原始碼進行除錯及閱讀,將以通俗的形式揭秘React

閱讀本文,成本與收益如下

閱讀耗時:26min+

全文字數:1w+

全文字符:5.5w+

預期收益:通明境 · React架構

本文適合有閱讀React原始碼計劃的初學者或者正在閱讀React原始碼的工程師,我們一起形成頭腦風暴,

2 認識Fiber節點

2.1 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;
  ...
  this.ref = null;
  ...
}

Fiber節點本身存盤了一些最基本的資料,其中包括如上六項構成Instance,它們分別代表

  • tag:Fiber節點對應組件的型別,包括了Funtion、Class等

    image-20220828220452391
  • key:更新key會強制更新Fiber節點

  • type:保存組件本身,準確來說,對于函陣列件保存函式本身,對于類組件保存類本身,對于HostComponent,也就是如原生<div></div>這類原生標簽會保存節點名稱

  • elementType:保存組件型別和type大部分情況是一樣的,但是也有不一樣的情況,比如LazyComponent

  • stateNode:保存Fiber對應的真實DOM節點

  • ref: 和key一樣屬于base欄位

2.2 Fiber樹結構實作

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...
  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
	...
}

我們看到Fiber節點這四個屬性,它們的含義分別是

  • return:指向父節點Fiber
  • child:指向子節點Fiber
  • sibling:指向右邊的兄弟節點Fiber

這樣子一來,對于我們這里的組件,就構成了如圖的Fiber樹

const CountButton = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(v => v + 1);
  };

  useEffect(() => {
    console.log('Hello Mount Effect');
    return () => {
      console.log('Hello Unmount Effect');
    };
  }, []);
  useEffect(() => {
    console.log('Hello count Effect');
  }, [count]);
  return (
    <>
      <div>Render by state</div>
      <div>{count}</div>
      <button onClick={handleClick}>Add Count</button>
    </>
  );
};

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src=https://www.cnblogs.com/SourceHeartLock/archive/2022/09/19/{logo} className="App-logo" alt="logo" />
        <CountButton/>
      </header>
    </div>
  );
}

image-20220828154533980

2.3 函式式組件&&Fiber

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
	...
}

從原始碼上看,React為hook足足騰出了五個屬性專門處理在函式式組件中使用hook的場景,

這些個玩意兒氣其實我們在前邊的hook章節也或多或少有了解過,這里專門講述Fiber節點上存盤的這些結構的作用,

2.3.1 pendingProps

pendingProps,從FiberNode的建構式看,是mixed(可傳入)進來的

image-20220829013902960

也就是說,這部分props可以在Fiber間傳遞,主要用于更新/創造新Fiber節點時用來傳遞props

2.3.2 memoizedProps

memoizedPropspendingProps的區別是什么呢?

我們知道,props代表一個Function的引數,當props變化時Function也會再次執行,

8E0B48BD4AA1E478A961D2C5EC0ECDDB

一般來講,memoizedProps會在整個渲染流程結尾部分被更新,存盤FiberNode的props,

pendingProps一般在渲染開始時,作為新的Props出現

image-20220830163001997

舉個更便于理解的例子,在如圖的beginWork階段,會對比新的props和舊的props來確定是否更新,此時比較的就是workInProgress.pendingPropscurrent.memoizedProps

image-20220830163509519

2.3.3 updateQueue

上一篇我們講useEffect有講到,updateQueue以如圖的形式存盤useEffect運行時生成的各個effect

image-20220830163738294

lastEffect以環形鏈的形式存盤了單個節點的所有effect,

(當然,這里指的當然只是函式式組件)

2.3.4 memoizedState

useState章節,我們也有講過memoizedStatememoizedState存盤了我們呼叫hook時產生的hook物件,目前已知除了useContext不會有hook物件產生并掛載,其他hook都會掛載到這里,

image-20220830165109296

hook之間以.next相連形成單向鏈表,

而hook呼叫時產生的不管是effect(useEffect)還是state(useState),都是存盤在hook.memoizedState,體現在Fiber節點上,其實是存盤在hook.memoizedState.memoizedState,注意不要混淆,

2.3.5 dependencies

以下是除錯代碼

const BaseContext = createContext(1);
const BaseContextDemo = () => {
  const {base} = useContext(BaseContext);
  return <div>{base}</div>;
};

const CountButton = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(v => v + 1);
  };

  useEffect(() => {
    console.log('Hello Mount Effect');
    return () => {
      console.log('Hello Unmount Effect');
    };
  }, []);
  useEffect(() => {
    console.log('Hello count Effect');
  }, [count]);

  const ref = useRef();

  const [base, setBase] = useState(null);
  const initValue = https://www.cnblogs.com/SourceHeartLock/archive/2022/09/19/{
    base,
    setBase,
  };

  return (
    
      
Render by state
{count}
); };

在還沒有發出的useContext原理中,會記載useContext的實作原理,劇透就是FiberNode.dependencies這個屬性記載了組件中通過useContext獲取到的背景關系

image-20220906231735709

從除錯結果看,多個context也將通過.next相連,同時顯然,這是一條單向鏈表

2.4 操作依據

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...
  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
	...
}

我們看到這三個屬性

  • deletions:待洗掉的子節點,render階段diff演算法如果檢測到Fiber的子節點應該被洗掉就會保存到這里,

  • flags/subtreeFlags:都是二進制形式,分別代表Fiber節點本身的保存的操作依據與Fiber節點的子樹的操作依據,

flags是React中很重要的一環,具體作用是通過二進制在每個Fiber節點保存其本身與子節點的flags,

3C717BA45856AD3B9EF1887255274A8C

至于具體如何保存,實際上是使用了二進制的特性,舉幾個例子

2.4.1 &運算

溫習一下&運算子的規則:只有1&1=1,其他情況為0

const NoFlags = /*                      */ 0b000000000000000000000000;
const PerformedWork = /*                */ 0b000000000000000000000001;
const Placement = /*                    */ 0b000000000000000000000010;
const Update = /*                       */ 0b000000000000000000000100;

const unknownFlags=Placement;
Boolean(unknownFlags & Placement) // true
Boolean(unknownFlags & Update) //false

React中會用一個未知的flags & 一個flag,此時是在判斷未知的flags中是否包含flag,

之所以說是是否包含,我們可以看看下邊的代碼,

const NoFlags = /*                      */ 0b000000000000000000000000;
const PerformedWork = /*                */ 0b000000000000000000000001;
const Placement = /*                    */ 0b000000000000000000000010;
const Update = /*                       */ 0b000000000000000000000100;

const unknownFlags = Placement|Update; //此時=0b000000000000000000000110
Boolean(unknownFlags & Placement) // true
Boolean(unknownFlags & Update) //true

2.4.2 |運算

溫習一下|運算子的規則:只有0&0=0,其他情況為1

上邊unknownFlags的例子我們不難發現,react利用了|運算子的特性來存盤flag

const unknownFlags = Placement|Update; //此時=0b000000000000000000000110

這樣的好處是快,判斷是否包含的時候,直接使用& 運算子,在有限的操作依據面前,使用二進制完全可以兜住所有情況,

2.4.3 ~運算

~運算子會把每一位取反,即1->0,0->1

在React中,~運算子同樣是常用操作

image-20220914115934463

那么作用是什么呢?其實也很容易從函式背景關系分析出來,對于圖中這個例子,react通過~運算子&運算子的結合,從flags中洗掉了Placement這個flag,

2.4.4 小總結:React中常見的操作

  • 通過unknownFlags & Placement判斷unknownFlags是否包含Placement

  • 通過unknownFlags |= PlacementPlacement合并進unknownFlags

  • 通過unknownFlags &= ~PlacementPlacementunknownFlags中刪去

關于有哪些flags,我們可以翻閱到ReactFiberFlags.js,這里會有詳細flags的記載

image-20220914112358670

2.5 雙快取樹的體現

我們曾說過,React的最基本作業原理雙快取樹,這引申出了我們需要知道這種機制在React中的實際體現,

這需要我們找到ReactFiber.old.js

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
	...
  this.alternate = null;
	...
}

由此我們知道,FIberNode上會有一個屬性alternate,而這個屬性正是我們期望的雙快取樹中,里樹與外樹的雙向指標,

正如圖所見,在初次渲染中,current===null,所以目前仍是白屏,而workInProgress已經在構建

image-20220828110638994

(圖誤,在renderWithHooks才對)

而當我們再次渲染,在renderWithHooks斷點,就可以觀察到workInProgress.alternate==current

image-20220828110948356

2.6* 優先級相關

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
	...
}

和lane有關的變數統一和調度優先級有關,暫時不涉及(因為還沒看)

2.7* React devtools Profiler

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  ...
  if (enableProfilerTimer) {
    this.actualDuration = Number.NaN;
    this.actualStartTime = Number.NaN;
    this.selfBaseDuration = Number.NaN;
    this.treeBaseDuration = Number.NaN;

    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }
	...
}

React并不只是react,react倉庫里包含了其他工程,其中就包含了我們的react profiler工具,在使用了profiler工具的情況下,react fiber會記錄一些運行時間,其實很多帶有Profiler的判斷陳述句都是和Profiler在配合,

image-20220914141423794

3 好好認識hook結構

我們上邊有講到FiberNode.memoizedState,我們知道這里保存的是mountWorkInProgressHook時產生的hook物件

{
  memoizedState: 0,
  baseState: 0,
  baseQueue: null,
  queue: ???,
  next:null
}

那么hook的各個項指什么?

3.1 baseState和memoizedState

其實很好理解,baseState對應上一次的state(effect),memoizedState為最新的state(effect),總之就是hook保存基本資料的地方,

04AC316ED382266CFE0B9C1F8B358DC6

3.2 queue

而hook.queue則是useState、useReducer的dispatcher存盤的地方,

  var queue:UpdateQueue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: initialState
  };
  hook.queue = queue;
  var dispatch = queue.dispatch = dispatchReducerAction.bind(null, currentlyRenderingFiber$1, queue);

對于queue的結構,我們逐一講解

3.2.1 lastRenderedState & lastRenderedReducer

  • queue.lastRenderedState屬性存盤上一個 state
  • queue.lastRenderedReducer 屬性存盤 reducer 內部狀態變更邏輯

其中queue.lastRenderedReduce可能不好理解,我們可以從代碼中理解,且看這里

image-20220907155356838

function basicStateReducer(state, action) {
  // $FlowFixMe: Flow doesn't like mixed types
  return typeof action === 'function' ? action(state) : action;
}
function mountState(initialState) {
  ...
  hook.memoizedState = hook.baseState = initialState;
  var queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  ...
}

這是dispatchSetState中的一段邏輯,處理的正是我們下邊將講述的,「不在渲染中」的處理階段(onClick觸發===異步觸發),

image-20220907160421253

那這里可以看到,我們可以從lastRenderedReducer得到eagerState

var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.

eagerState是什么? 實際上這里是通過lastRenderedReducer快速獲得了最近一次的state,

react會通過objectIs(eagerState,currentState)來確定是否不進行更新,這也是為什么我們更新state的時候要注意state為不可變資料,每次更新都需要更新一個新值才有效

if (objectIs(eagerState, currentState)) {
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return;
}

3.2.2 dispatch

dispatch 屬性存盤狀態變更函式,對應useState、useReducer 回傳值中的第二項

function mountState(initialState) {
  var hook = mountWorkInProgressHook();

  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;
  var queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  hook.queue = queue;
  var dispatch = queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}

值得注意的就是dispatch會通過.bind事先注入currentlyRenderingFiber$1, queue兩個引數,此間通過bind系結的currentlyRenderingFiber$1,作用是判斷這個更新是在fiber的render階段還是異步觸發,

這也給了我們一個判斷fiber在render階段的條件

function isRenderPhaseUpdate(fiber: Fiber) {
 const alternate = fiber.alternate;
 return (
   fiber === currentlyRenderingFiber ||
   (alternate !== null && alternate === currentlyRenderingFiber)
 );
}

3.2.3 pending

pending 屬性存盤排隊中的狀態變更規則,單向環形鏈表結構,

在原始碼中,每一個規則以Update的結構連接

export type Update<S, A> = {|
  lane: Lane,
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
|};

那么我們知道了

  • eagerState 快取上一個狀態(React稱之為急迫的狀態)
  • action 代表狀態變更的規則,可以是本次要被修改的值,也可以是函式
  • hasEagerState 則是記錄是否執行過優化邏輯

eagerState在所有原始碼中只在這里使用,根據React原始碼,這里的優化指的是React會在eagerState===currentState的情況下,不做重渲染,如果狀態更新前后沒有變化,則可以略過剩下的步驟,

try {
  var currentState = queue.lastRenderedState;
  var eagerState = lastRenderedReducer(currentState, action);
  update.hasEagerState = true;
  update.eagerState = eagerState;
  if (objectIs(eagerState, currentState)) {
    enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
    return;
  }
} catch (error) {
} finally {
  {
    ReactCurrentDispatcher$1.current = prevDispatcher;
  }
}

image-20220909230608210

3.3 baseQueue

值得注意的是,baseQueue的結構來自queue.pending而不是queue

image-20220910231501779

(baseQueue被賦值queue.pending)

其余的大抵是沒啥好說的,baseQueue在除錯中的體現我暫時并沒有遇到,推測需要有比較大量的更新,

4 React架構

本章我們講述React的渲染流程,將覆寫React的render階段與commit階段的概念與流程概覽,不會非常深入,爭取留存印象,

4.1 React渲染關鍵節點

我們已經預先知道可以將React的渲染分成render階段和commit階段,也知道render階段的關鍵函式是beginWorkcompleteWorkcommit階段的關鍵函式則是commitRoot

在這個基礎上,我們從呼叫堆疊中可以找到這兩個階段的起始節點,

  • render階段

我們在beginWork中打上斷點,然后可以回溯呼叫堆疊找到出發點,

12B98F3540B6E694265C5D47D49495F8

從圖中,我們可以知道renderRoot觸發于performConcurrentWorkOnRoot

image-20220911124953226

除此之外,在performSyncWorkOnRoot中也可以走入renderRoot

image-20220911174952904

它們會根據情況走到renderRootConcurrent或者renderRootSync,這里即是render階段的開始點

那么我們得到第一個關鍵節點:

  • render階段開始于renderRootConcurrentrenderRootSync
  • commit階段

我們知道,render階段的尾巴是completeWork,commit階段的起步是commitRoot,我們嘗試在這completeWork方法中斷點,然后單步除錯到commitRoot

image-20220911173640119

上圖是我debug出來的結果,completeWorkcommitRoot之間的最近公共函式節點是performSyncWorkOnRoot/performConcurrentWorkOnRoot

那么我們知道,commitRoot即是commit階段的起點,

那么我們得到兩個關鍵資訊:

  • commit階段開始于commitRoot
  • render階段和commit階段通過performSyncWorkOnRoot/performConcurrentWorkOnRoot聯動

4.1.1 小總結

  • render階段開始于renderRootConcurrentrenderRootSync
  • commit階段開始于commitRoot
  • render階段和commit階段通過performSyncWorkOnRoot/performConcurrentWorkOnRoot聯動

4.2 狀態更新流程

4.2.1 找到root節點

正常render的第一步,是找到當前Fiber的root節點,

以useState造成的渲染舉例,React會通過enqueueConcurrentHookUpdate->getRootForUpdatedFiber找到當前節點的root節點,

function dispatchSetState(fiber, queue, action) {
  ...
    var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

    if (root !== null) {
      var eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  ...
}
function getRootForUpdatedFiber(sourceFiber) {
  ...
  detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
  var node = sourceFiber;
  var parent = node.return;
  while (parent !== null) {
    detectUpdateOnUnmountedFiber(sourceFiber, node);
    node = parent;
    parent = node.return;
  }
  return node.tag === HostRoot ? node.stateNode : null;
}

image-20220911230326450

尋找root節點是一個向上不斷尋找root節點的程序,在這個程序中react還會持續呼叫detectUpdateOnUnmountedFiber檢查是否呼叫了過期的更新函式,

image-20220911225903147

什么是過期的更新函式?舉個例子,通過useRef保存了setState方法,但是隨著組件更新ref中的setState方法并沒有更新,此時由于setState方法本質上是通過.bind的形式報存了函式及引數fiber節點,此時就會存在呼叫了一個已卸載組件的過期的setState方法,

4.2.2 調度同步/異步更新

找到root節點之后,那么就要進入render流程,這就存在一個問題,

我們上邊說了,render階段的觸發函式是performSyncWorkOnRootperformConcurrentWorkOnRoot,那么如何判斷應該進入同步更新還是異步更新呢?

這就要走到ensureRootIsScheduledensureRootIsScheduled會通過判斷newCallbackPriority === SyncLane來確定走同步render還是異步render,這里涉及調度器,暫時不講(還沒看還不會)

function ensureRootIsScheduled(root, currentTime) {
  ...
  var newCallbackNode;

  if (newCallbackPriority === SyncLane) {
    // Special case: Sync React callbacks are scheduled on a special
    // internal queue
    if (root.tag === LegacyRoot) {
      ...
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }

    ...
    
    newCallbackNode = null;
  } else {
    var schedulerPriorityLevel;
		
    ...
    
    newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
} 

那么可以看到,這里會有一個scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))或者scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root))的程序,

CA3CC756047F3449F8F0A001D7583135

值得注意的是,同步調度這里還更復雜,react一方面需要考慮是否是嚴格模式做不同的callback

image-20220911234438727

(ensureRootIsScheduled是一個很重要的函式,會Scheduled一起講會比較好)

另一方面還調度了flushSynCallbacks,這個函式做的事情很簡單,就是把syncQueue中的待執行任務全部執行

image-20220912000205282 image-20220911234507477

4.2.3 render階段

render階段分成了兩個階段,我們在狀態更新流程中不講細節,只講明基本作用,細節請看后邊的單章

經歷了調度更新,會來到render階段,render階段做了兩件事,

  • beginWork階段,在這個階段react做的事情是從root遞回到子葉,每次beginWork會對Fiber節點進行新建/復用邏輯,然后通過reconcileChildrenchild Fiber掛載到workInProgress.child并在child Fiber上記錄flags,最終遍歷整個Fiber樹
  • completeWork階段,在這個階段,是從子葉不斷向上遍歷到父親Fiber節點的程序,這個程序中,completeWork會把workInProgress Tree上的真實DOM掛載/更新上去,

那么總結來說,beginWork負責虛擬DOM節點Fiber Node的維護與flag記錄,completeWork負責真實DOM節點在Fiber Node的映射作業,

當然,這些操作只涉及節點維護,真正渲染到頁面上就是commit階段要負責的了

4.2.4 commit階段

commit階段,除了會處理一下和hook相關的事情之外,最主要做了就是負責把beginWork階段記錄的flags在真實DOM樹上進行操作,

總結來說:

  • 處理和useEffect\useInsertionEffect\useLayoutEffect相關的hook,處理class組件相關的生命周期鉤子
  • 基于flags做真實DOM樹操作,包括增刪改,以及輸入框型別節點的focus、blur等問題
  • 清理一些全域變數,并確保進入下一次調度

4.3 render階段

這里是延續狀態更新流程的render階段,

我們在狀態更新第一步就拿到了root節點,經過調度更新后會進入render階段,

此時我們有兩種走法,一種是通過renderRootSync來到workLoopSync,另一種則是通過renderRootConcurrent走到workLoopConcurrent,這兩者的區別是workLoopConcurrent會檢查瀏覽器是否有剩余時間片,

function workLoopConcurrent() {
  // 執行作業,直到調度程式要求我們讓步
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopSync() {
  // 已經超時了,因此無需檢查我們是否需要讓步就可以執行作業
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

workLoop做了什么呢?這就要從performUnitOfWork(workInProgress)說起,下邊的代碼是精簡邏輯 (只剩下beginWork這部分邏輯) 過后的performUnitOfWork函式,可以看到performUnitOfWork通過beginWork創建了一個新的節點賦給workInProgress

function performUnitOfWork(unitOfWork) {
  var current = unitOfWork.alternate; // currentFiber
  setCurrentFiber(unitOfWork); // 會將全域current變數設定為workInProgressFiber

  var next = beginWork$1(current, unitOfWork, renderLanes$1); // currentFiber
  
  resetCurrentFiber(); // 重置current變數為null
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  workInProgress = next;
  ...
}

4.3.1 beginWork

那么此處引出了render階段中最重要的兩個函式之一beginWork,beginWork正如上邊所說,這個函式的職責是回傳一個Fiber節點,這個節點可以復用currentFiber也可以創建一個新的,

我們其實在【useState原理】章節中有見過beginWork,當時我們強調了雙快取機制,這次我們可以更細地了解一下beginWork,

3EC7BC0E6EDF9F966E3F99EB3AEAE44A

我們提煉一下beginWork的核心邏輯,會發現beginWork通過current!==null來判斷是否是第一次執行,這里的邏輯是如果是第一次執行,那么Fiber沒有mount,自然為null,

function beginWork(current, workInProgress, renderLanes) {
  ...
  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    if (oldProps !== newProps || hasContextChanged() || (
     workInProgress.type !== current.type )) {
      didReceiveUpdate = true;
    } else {

      var hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);

      if (!hasScheduledUpdateOrContext &&
      (workInProgress.flags & DidCapture) === NoFlags) {
        // 沒有待更新的updates或者背景關系資訊,復用上次的Fiber節點
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
      }
      ...
    }
  } else {
    didReceiveUpdate = false;
		...
  }


  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {
    ...
    case FunctionComponent:
    ...
    case HostComponent:
    ...

  }

}

#1 update復用邏輯

看到這里,react在update的邏輯中,根據三個條件來判斷是否復用上一次的FIber

  • oldProps !== newProps,代表props是否變化

  • hasContextChanged(),

    var didPerformWorkStackCursor = createCursor(false); // Keep track of the previous context object that was on the stack.
    // We use this to get access to the parent context after we have already
    // pushed the next context provider, and now need to merge their contexts.
    
    

    image-20220912142905277

  • workInProgress.type !== current.type,fiber.type是否變化

function beginWork(current, workInProgress, renderLanes) {
  ...
  if (current !== null) {
    var oldProps = current.memoizedProps;
    var newProps = workInProgress.pendingProps;

    if (oldProps !== newProps || hasContextChanged() || (
     workInProgress.type !== current.type )) {
      didReceiveUpdate = true;
    } else {
			//此處是復用的邏輯
      ...
    }
  } else {
    didReceiveUpdate = false;
		...
  }
	...
}

#2 mount/update新建邏輯

不滿足更新條件的話,會根據workInProgress.tag新建不同型別的Fiber節點,對于不進行Fiber復用到更新也會進入這個邏輯

  switch (workInProgress.tag) {
    case IndeterminateComponent:
      {
        return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
      }
    case LazyComponent:
      {
        var elementType = workInProgress.elementType;
        return mountLazyComponent(current, workInProgress, elementType, renderLanes);
      }
    case FunctionComponent:
      {
        var Component = workInProgress.type;
        var unresolvedProps = workInProgress.pendingProps;
        var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
        return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
      }
    case ClassComponent:
      {
        var _Component = workInProgress.type;
        var _unresolvedProps = workInProgress.pendingProps;

        var _resolvedProps = workInProgress.elementType === _Component ? _unresolvedProps : resolveDefaultProps(_Component, _unresolvedProps);

        return updateClassComponent(current, workInProgress, _Component, _resolvedProps, renderLanes);
      }
		...
  }

3FEEA7F7D362DFF7489B5CD937294085

根據我們在【useState】章節的識訓,不管是update還是mount都要走到reconcileChildren

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  if (current === null) {
    // mount時
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    // update時
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}

這里做的事情描述起來是比較好辦的,不過詳細起來就涉及diff演算法需要開單章

  • mount時,創建新的Child Fiber節點
  • update時,將當前組件與該組件在上次更新時對應的Fiber節點進行diff比較,將比較的結果生成新Fiber節點

當然,不管走到哪里,workInProgress都會得到一個child FIber

image-20220912161901164

不管是reconcileChildFibers還是mountChildFibers,都是通過呼叫ChildReconciler這個函式來運行的,

image-20220912163436219

而在整個ChildReconciler中,我們會經常性看到如圖一樣的操作,

image-20220912193434317

這便引出了操作依據一說,react用Fiber.flags并以二進制的形式存貯了對于每個Fiber的操作依據,這種方式比陣列更高效,可以方便地使用位運算發為Fiber.flags增刪不同的操作依據,

image-20220912193542891

點擊這里可以查看所有的操作型別

#3 diff演算法*

標記這個知識點,下次再說

4.3.2 completeWork

我們持續執行workLoop,會發現workInProgressrootFiber持續深入到了我的除錯代碼中的最底層(一個div),此時就到了render階段的第二個階段completeWork

function performUnitOfWork(unitOfWork) {
  ...

  if (next === null) {
    // 進入completeWork
    completeUnitOfWork(unitOfWork);
  } else {
    ...
  }

  ...
}

那么此時進入completeUnitOfWork,這里的核心邏輯是completeWork從子節點不斷訪問workInProgress.return向上回圈執行beginWork,如果遇到兄弟子節點,則會將workInProgress指向兄弟節點并回傳至performUnitOfWork,重新執行beginWork到completeWork的整個render階段,

image-20220912180238796

那么completeWork做了什么?這里是completeWork的基本邏輯框架(我把bubbleProperties提出來方便理解每個completeWork都會執行這前后兩條陳述句),做了popTreeContextbubbleProperties

function completeWork(current, workInProgress, renderLanes) {
  popTreeContext(workInProgress);

  switch (workInProgress.tag) {
    case FunctionComponent:
      ...
    case HostComponent:
      ...
    ...
  }
  bubbleProperties(workInProgress);
}

popTreeContext是和上邊beginWork相關的內容,這里的目的是使得正在進行的作業不處于堆疊頂部,對應pushContext的階段一般在beginWork的swtich中進入的函式中都可以找到

image-20220912192449157

bubbleProperties的核心邏輯我也提了出來,可以看到這里是做了一個層遍歷,遍歷了completedWorkFiber的所有child,將它們的return賦值為completedWorkFiber,同時,這里也涉及了subtreeFlags的計算,會將子節點的操作依據冒泡到父節點,

FA2E2BD1166CC5583D24B03D6E0E6B0A

而關于subtreeFlags的具體用處,在commit階段,我們后邊說,

function bubbleProperties(){
  ...
  var newChildLanes = NoLanes;
  var subtreeFlags = NoFlags;
  {
      var _child = completedWork.child;

      while (_child !== null) {
        newChildLanes = mergeLanes(newChildLanes, mergeLanes(_child.lanes, _child.childLanes));
        subtreeFlags |= _child.subtreeFlags;
        subtreeFlags |= _child.flags;

        _child.return = completedWork;
        _child = _child.sibling;
      }
    }

    completedWork.subtreeFlags |= subtreeFlags;
  
	}
  ...
}

后續的話,會根據workInProgress.tag來走不同的邏輯,我們這里主要說HostComponent的邏輯,代表原生組件,

9790B760B1E00F83BD11B87BB75D9B7A

下邊是我提煉出來的核心邏輯,這里同樣會區分updatemount

function completeWork(current, workInProgress, renderLanes) {
  popTreeContext(workInProgress);

  switch (workInProgress.tag) {
    ...
    case HostComponent:{
        popHostContext(workInProgress);
        var type = workInProgress.type;

        if (current !== null && workInProgress.stateNode != null) {
          updateHostComponent$1(current, workInProgress, type, newProps);
          ...
        } else {
          ...
          var currentHostContext = getHostContext();

          var rootContainerInstance = getRootHostContainer();
          var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
          appendAllChildren(instance, workInProgress, false, false);
          workInProgress.stateNode = instance;
          
          ...
        }

        bubbleProperties(workInProgress);
        return null;
    }
    ...
  }
}

#1 update時

update時,無需生成新的DOM節點,所以此時要處理props,在updateHostComponent中,第二部分會呼叫prepareUpdate->diffProperties獲得一個updatePayload掛載在workInProgress.updateQueue

image-20220912202620837

image-20220912230012226

具體會處理哪些props,我們深入到diffProperties就可以找到這一塊的邏輯

image-20220912230843810

image-20220912231013886

OK,那么我們回到上邊所說的updatePayload,除錯發現updatePayload是一個陣列,資料結構體現為一個偶數為key,奇數為value的陣列:

image-20220912231244691

到了這一步,update流程最后會走入markUpdate,至此,completeWork的update邏輯完畢

image-20220912231509268

#2 mount時

我們此時來看mount時的邏輯,這里最核心的邏輯簡化后其實只有幾句

function completeWork(current, workInProgress, renderLanes) {
  popTreeContext(workInProgress);
	...
  var currentHostContext = getHostContext();

  var rootContainerInstance = getRootHostContainer(); // 獲得root真實DOM
  
  var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);// 創建Fiber對應的真實DOM
  
  
  appendAllChildren(instance, workInProgress, false, false);//將創建的真實dom插入workInProgressFiber
  
  
  workInProgress.stateNode = instance;
  ...
  bubbleProperties(workInProgress);  
}

我們關注appendAllChildren,這里的邏輯是將新建的instance作為真實節點parent,將其插入到workInProgressFiber的真實節點中(因為一個Fiber節點不一定有真實節點,所以要找到可以插入的真實節點)

  appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
    // We only have the top Fiber that was created but we need recurse down its
    // children to find all the terminal nodes.
    var node = workInProgress.child;

    while (node !== null) {
      if (node.tag === HostComponent || node.tag === HostText) {
        appendInitialChild(parent, node.stateNode);
      } else if (node.tag === HostPortal) ; else if (node.child !== null) {
        node.child.return = node;
        node = node.child;
        continue;
      }

      if (node === workInProgress) {
        return;
      }

      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;
        }

        node = node.return;
      }

      node.sibling.return = node.return;
      node = node.sibling;
    }
  };

那么這里實際做的就是把真實DOM掛載到workInProgressFiber上,又由于我們上邊說了,complateWork是一個從子節點向上遍歷的程序,那么遍歷完畢的時候,我們就得到了一顆構建好的workInProgress Tree

768151FDD996975166D8ED800FB15F44

那么接著,就是commit階段了,

4.4 commit階段

首先我們要知道commit階段的職責是什么,

BF43DF9506C66549A9DE61E7BFD390C2

這樣的話,我們又要強調一下雙快取樹了,workInProgress樹是一顆在記憶體中構建的DOM樹,current樹則是頁面正在渲染的DOM樹,

在此基礎上,render階段已經完成了記憶體中構建下一狀態的workInProgress,那么此時commit階段正應該做將current樹與workInProgress樹調換的作業,

27C9BA14FEC45B6C24BF60C8F18C84B6

而調換作業中,由于render階段的真實DOM并沒有更新,只是做了標記,此時會需要commit階段負責把這些更新根據不同的操作標記在真實DOM上操作,

43885F5E1F8C7FF2B3392D297C855609

commit階段開始于commitRoot,往下就是呼叫commitRootImpl,我們會著重分析commitRootImpl

image-20220913001550758

image-20220913001951456

首先看入參,可以看到commitRootImpl的入參有四個,其中root為最基本的引數,傳入的是已準備就緒的workInProgressRootFiber

function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
)

image-20220913103915403

我們認為commit階段可以分為三個階段,分別代表

  • before mutation,在執行DOM操作前的階段
  • mutation,執行DOM操作
  • layout,執行DOM操作之后

當然,在這些流程之外,commit階段還會處理useEffect這類需要在commit階段執行的hook,

4.4.1 Before commit start

在commit開始之前,即before mutation之前的代碼可以從下邊看見,它們具體做了什么我直接在代碼中注釋了,請看注釋,

function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
) {
  do {
		// 這里會調度未執行完的useEffect,之所以上下各有一處,一方面是和React優先級有關,一方面也和因為調度`useEffect`等hook時重新進入了render階段重新進入到commit階段有關,
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);

  ...
	// 和flags類似的二進制
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    throw new Error('Should not already be working.');
  }

  // finishedWork是已經處理好的workInProgressRootFiber
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  ...
  if (finishedWork === null) {
    return null;
  }
 
  //重置待commit的rootFiber,重置commit優先級
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
	...
  // commitRoot總是同步完成
  // 所以在這里清除Scheduler系結的回呼函式等變數允許系結新的函式
  root.callbackNode = null;
  root.callbackPriority = NoLane;

  //一些優先級的計算
  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
  const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
  remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);

  markRootFinished(root, remainingLanes);

  if (root === workInProgressRoot) {
    // 完成后,重置全域變數
    workInProgressRoot = null;
    workInProgress = null;
    workInProgressRootRenderLanes = NoLanes;
  }


  // 當finishedWork中存在PassiveMask標記時,調度useEffect
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      pendingPassiveEffectsRemainingLanes = remainingLanes;
      pendingPassiveTransitions = transitions;
      scheduleCallback(NormalSchedulerPriority, () => {
        // 這里會調度useEffect的運行,詳情請看【useEffect】篇
        flushPassiveEffects();
        return null;
      });
    }
  }
    
	...
}

這里有一點值得注意的是,伴隨著flushPassiveEffects的呼叫,在堆疊中完全可能形成多次commit,這是來源于useEffect的副作用觸發了組件渲染,在這種情況下會再走一次狀態更新流程(當然這期間有優化)

image-20220913163639067

4.4.2 BeforeMutation

commit階段的正式開始,在于commitBeforeMutationEffects這個函式,可以看到當react確定subtreeFlags或者root.flags上可以找到BeforeMutationMask | MutationMask | LayoutMask | PassiveMask時,會觸發commit的邏輯

  var subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
  var rootHasEffect = (finishedWork.flags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;

  if (subtreeHasEffects || rootHasEffect) {
    ...
    var shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(root, finishedWork);
    ...
  } else {
    // No effects.
    root.current = finishedWork;
  }

那么我們首先來看commitBeforeMutationEffects,那么可以看到commitBeforeMutationEffects緊接著呼叫了commitBeforeMutationEffects_begin

244392FC225E2177F8435874B3A49BE3

而commitBeforeMutationEffects_begin做的事情是從finishedWork向下遍歷fiber樹,一直到遍歷到某個Fiber節點不再有BeforeMutationMask標記,此時會進入commitBeforeMutationEffects_complete

function commitBeforeMutationEffects(root, firstChild) {
  // 處理焦點相關的邏輯,處理原因是因為真實DOM的增刪導致可能出現的焦點變化
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  // nextEffect是一個全域變數,firstChild對應上方傳參`finishedWork`
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();
	
  // 處理Blur相關的邏輯
  var shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;
  return shouldFire;
}
  
function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    var child = fiber.child;

    if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) {
      child.return = fiber;
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

commitBeforeMutationEffects_complete同樣是做了一次遍歷,這次的程序則是不斷向上回傳,呼叫程序中不斷執行commitBeforeMutationEffectsOnFiber

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    var fiber = nextEffect;
    setCurrentFiber(fiber);

    try {
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }

    resetCurrentFiber();
    var sibling = fiber.sibling;

    if (sibling !== null) {
      // 注意這里,發現了嘛,和completeWork非常相似的邏輯對吧
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

繼續到commitBeforeMutationEffectsOnFiber,發現這里只有兩個簡單的內容

  • 一個是對于ClassComponent會呼叫getSnapshotBeforeUpdate
  • 另一個則是會HostRoot進行clearContainer(root.containerInfo)

image-20220913171907770

image-20220913171936689

# 小結

那么我們對BeforeMutation階段進行小結,現在我們知道React在BeforeMutation主要做了兩件事

  • 處理真實DOM增刪后的 focusblur邏輯
  • 呼叫ClassComponent的getSnapshotBeforeUpdate生命周期鉤子

4.4.3 Mutation

commit第二階段,我們會進入commitMutationEffects->commitMutationEffectsOnFiber

  if (subtreeHasEffects || rootHasEffect) {
    ...
    commitMutationEffects(root, finishedWork, lanes);
    ...
  } else {
    // No effects.
    root.current = finishedWork;
  }

image-20220913173111382

commitMutationEffectsOnFiber是一個368行的函式,它會根據Fiber.tagFiber.flags走不同的Mutation邏輯

image-20220913173553696

目前來說,除了ScopeComponent外的所有Component型別都會執行

recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);

所以我們首先走入recursivelyTraverseMutationEffects,可以看到recursivelyTraverseMutationEffects主要分成兩部分,

839D576FEA3CCCABF59671E8FCB3ADBA

上邊的部分負責從Fiber.deletions中取出具體的deletions執行commitDeletionEffects,后邊則是向下遍歷節點遞回執行commitMutationEffectsOnFiber

function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
  // Deletions effects can be scheduled on any fiber type. They need to happen
  // before the children effects hae fired.
  var deletions = parentFiber.deletions;

  if (deletions !== null) {
    for (var i = 0; i < deletions.length; i++) {
      var childToDelete = deletions[i];

      try {
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }

  var prevDebugFiber = getCurrentFiber();

  if (parentFiber.subtreeFlags & MutationMask) {
    var child = parentFiber.child;

    while (child !== null) {
      setCurrentFiber(child);
      commitMutationEffectsOnFiber(child, root);
      child = child.sibling;
    }
  }

  setCurrentFiber(prevDebugFiber);
}

image-20220913231000339

我通覽這部分涉及的flags,發現會執行以下內容:

  • Update->Insertion:執行React18推出的新hook,useInsertionEffect,會包含destorycreate兩個階段
image-20220913231805283
  • Update->Layout:執行useLayoutEffect上一次執行殘留的destory函式

    image-20220913232322735

  • Placement:

image-20220913233647464

  • Deletions:洗掉節點

  • Update,more

    image-20220913235058171

  • Hydrating :SSR相關,由于博主目前為止沒有實踐過SSR,所以不說,

  • Ref:safelyDetachRef

  • ContentReset

  • Visibility

    ...

打住,有點多了!我們只關注UpdateDeletionsPlacement,并且只關注HostComponent

223E85C04FF58A4406FA7DB4DC511E8D

#1 Update

關于FunctionComponent的Update,做的事情其實就在上方前亮點

而對于HostComponent,react 會執行這些內容:

image-20220913235933430

這里最核心的就是commitUpdate,React會通過updateProperties將DOM屬性更新到真實節點上

function commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
  // Apply the diff to the DOM node.
  updateProperties(domElement, updatePayload, type, oldProps, newProps); // Update the props handle so that we know which props are the ones with
  // with current event handlers.

  updateFiberProps(domElement, newProps);
}

image-20220914000716411

image-20220914000658640

(我們其實遇到過類似的函式??)

react還會把這個屬性也更新上去,在我這篇文章中有這個屬性的應用

image-20220914000841062

#2 Placement

我們只說HostComponent的邏輯,只有真實節點會走到這里,另外兩個tagHostRootHostPortal,相比HostComponent只是缺少了ContextReset的內容,

image-20220914001322392

(如果其他型別的tag走到commitPlacement是會報錯的)

那么這里其實主要就是三步:

  • 獲取Fiber節點存在HostFiber的父節點,并最侄訓得真實DOM

    image-20220914001717436

    image-20220914002256829

  • 獲取Fiber節點的兄弟真實DOM節點

  • insertOrAppendPlacementNodeIntoContainer,將節點插入或添加到父容器中

image-20220914002533784

image-20220914002638999

走Placement完畢,可以很明顯看到頁面渲染

2022-09-14 00.32.26

(appendChildToContainer函式涉及真實DOM的插入/添加操作)

#3 Deletion

deletions是在beginWork的diff程序中獲得的

  • 呼叫被洗掉節點的componentWillUnmount生命周期鉤子,從頁面移除Fiber節點對應DOM節點

image-20220914003826446

  • 安全解綁ref

image-20220914003755958

4.4.4 Layout

進入layout階段,證明DOM節點已經渲染完畢了

//將current指向已經完成的workInProgress
root.current = finishedWork;

commitLayoutEffects(finishedWork, root, lanes);
function commitLayoutEffects(finishedWork, root, committedLanes) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  
  var current = finishedWork.alternate;
  commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
  
  inProgressLanes = null;
  inProgressRoot = null;
}

commitLayoutEffects->commitLayoutEffectOnFiber會按照我們熟悉的流程做遞回

image-20220914135909948

image-20220914135920901

(commitLayoutEffectOnFiber和recursivelyTraverseLayoutEffects遞回呼叫)

我們需要關注的是commitLayoutEffectOnFiber中的內容

function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) {
  // When updating this function, also update reappearLayoutEffects, which does
  // most of the same things when an offscreen tree goes from hidden -> visible.
  var flags = finishedWork.flags;

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
      {
        recursivelyTraverseLayoutEffects(finishedRoot, finishedWork, committedLanes);

        //調度useLayoutEffect的create
        if (flags & Update) {
          commitHookLayoutEffects(finishedWork, Layout | HasEffect);
        }

        break;
      }

    case ClassComponent:
      {
        recursivelyTraverseLayoutEffects(finishedRoot, finishedWork, committedLanes);

        //調度componentDidUpdate、componentDidMount等class組件的生命周期鉤子
        if (flags & Update) {
          commitClassLayoutLifecycles(finishedWork, current);
        }
        if (flags & Callback) {
          commitClassCallbacks(finishedWork);
        }

        //用真實DOM更新ref
        if (flags & Ref) {
          safelyAttachRef(finishedWork, finishedWork.return);
        }

        break;
      }
    ...
    case HostComponent:
      {
        recursivelyTraverseLayoutEffects(finishedRoot, finishedWork, committedLanes);

        // 這里會調度組件的docus、img的src標簽
        if (current === null && flags & Update) {
          commitHostComponentMount(finishedWork);
        }

        //用真實DOM更新ref
        if (flags & Ref) {
          safelyAttachRef(finishedWork, finishedWork.return);
        }

        break;
      }
    ...
  }
}

此時React會做一些收尾的作業,正如我在給文章收尾一樣,內容是比較少(水)的,

  • 調度useLayoutEffect的開始階段

  • 調度componentDidUpdate、componentDidMount等class組件的生命周期鉤子

  • 真實dom上的focus處理、img標簽的src處理

  • AttachRef,獲取真實DOM,更新ref

更多內容其實都非常好理解,我推薦直接動手看,

4.4.5 After commit end

當然,在layout階段結束后仍有一些收尾作業,

  var rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

	//上邊執行useEffect時會標記rootDoesHavePassiveEffects=true
	//這里會對相關內容進行清除
  if (rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = false;
    rootWithPendingPassiveEffects = root;
    pendingPassiveEffectsLanes = lanes;
  } else {
    releaseRootPooledCache(root, remainingLanes);
  }

  ...
  //和react-refresh-runtime相關的模塊
  onCommitRoot(finishedWork.stateNode, renderPriorityLevel);

  ...

	// 確保root有一個新的調度,我想找機會試試把這句話注釋
  ensureRootIsScheduled(root, now());

	// 一些錯誤處理
  if (recoverableErrors !== null) {
    var onRecoverableError = root.onRecoverableError;

    for (var i = 0; i < recoverableErrors.length; i++) {
      var recoverableError = recoverableErrors[i];
      var componentStack = recoverableError.stack;
      var digest = recoverableError.digest;
      onRecoverableError(recoverableError.value, {
        componentStack: componentStack,
        digest: digest
      });
    }
  }

  if (hasUncaughtError) {
    hasUncaughtError = false;
    var error$1 = firstUncaughtError;
    firstUncaughtError = null;
    throw error$1;
  }

	// React注釋:請再次閱讀,因為被動效果可能會更新它
  if (includesSomeLane(pendingPassiveEffectsLanes, SyncLane) && root.tag !== LegacyRoot) {
    flushPassiveEffects();
  } 


	// 無限重渲染的計數
  remainingLanes = root.pendingLanes;
  if (includesSomeLane(remainingLanes, SyncLane)) {
    if (root === rootWithNestedUpdates) {
      nestedUpdateCount++;
    } else {
      nestedUpdateCount = 0;
      rootWithNestedUpdates = root;
    }
  } else {
    nestedUpdateCount = 0;
  } // If layout work was scheduled, flush it now.

	// 執行一些同步任務,這樣無需等待在下一次回圈的時候進行,這里可以參考ensureRootIsScheduled
  flushSyncCallbacks();

  return null;

那么至此,commit階段算已經完成了,

AF0F30822F8D36A8C83A07F6E8777722

但是React的渲染卻不能算完成,正如我一開始讀原始碼的初衷是為了知道,我在useEffect里呼叫了更新,這個執行時機和觸發渲染原理是什么情況,

到了這里我會明白,由于我們上述的各種effect、生命周期狗子,此時完全可能再次觸發更新,

而react也會很自然地走進一個新的render+commit的程序,先將觸發更新的內容更新后再繼續原本未更新的,

E1D350692AF8C9B7A98A83277B0D87C3

對于React來講,會在flushWork執行完畢后才真正進入空閑,但是這就是后話了

image-20220914021943510

(flushWork函式)

5 總結

不管在面試還是在生活中,都曾有人問我為什么要看React原始碼,

我剛開始是因為對于hook的架構感興趣而去看的,而現在隨著閱讀逐漸深入,我發現閱讀react原始碼一方面給了我比較強的成就感,這也是我可以堅持下來的原因,另一方面,我們真的會在閱讀中體會到某些思想上的高明,

比如,二進制flags、useEffect形成的環形更新鏈條

閱完本文,期待你對React18的Fiber架構有了更新的認識,也理解了React狀態更新的全流程,更期望你可以將學到的東西真實應用在自己的生活、作業中,我認為這才是讀原始碼最重要的,

那么這里留幾個關于React的問題,默想3分鐘,把識訓沉淀在腦海中,

  • 總結一下beginWork和completeWork的作業內容
  • useLayoutEffect在什么時機執行
  • react是在什么時候、怎么存盤、怎么應用操作依據的?

6 尾聲

Hi~你好,再次認識一下,我是心鎖,致力于前端開發的軟體開發工程師,

這是我第一篇單字符數破5w,字數破1w的文章,耗時一個月零四天,

所以非常期待你的點贊、收藏、分析~

后續呢,我會進行必要的切割,分多文方便閱讀,同時補充更多細節,所以非常期待你的關注

  • https://github.com/GrinZero 這是我的github,我會在上邊更新腦子里突然蹦出來的主意,歡迎你的follow,后續也會把react解讀更新上去,
image-20220914145845619

(部分專案成果集合圖)

  • https://juejin.cn/user/1645288319627576/posts 這是我的掘金個人主頁,期待你的關注,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/509146.html

標籤:其他

上一篇:記錄--通過手寫,分析axios核心原理

下一篇:CSS Flexbox 布局

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more