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

来源:https://www.cnblogs.com/SourceHeartLock/archive/2022/09/19/react.html
-Advertisement-
Play Games

⚠️1.1萬長文⚠️ React源碼並非洪水猛獸,知道方法,就可以很輕易地馴服它(=^▽^=)。文章基於最新的React源碼進行調試及閱讀,將以通俗地方式解讀React ...


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={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 = {
    base,
    setBase,
  };

  return (
    <BaseContext.Provider value={initValue}>
      <div ref={ref}>
        <div>Render by state</div>
        <div>{count}</div>
        <button onClick={handleClick}>Add Count</button>
        <button onClick={() => setBase(i => ++i)}>Add Base</button>
        <BaseContextDemo />
      </div>
    </BaseContext.Provider>
  );
};

在還沒有發出的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:

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • sed高階用法 sed編輯器 sed是一種流編輯器,流編輯器會在編輯器處理數據之前基於預先提供的一組規則來編輯數據流。 1.sed編輯器工作流程 sed編輯器可以根據命令來處理數據流中的數據,這些命令要麼從命令行中輸入,要麼存儲在一個命令文本文件中。 sed的工作流程主要包括讀取、執行和顯示三個過程 ...
  • 零基礎學MySQL 筆記目錄:(https://www.cnblogs.com/wenjie2000/p/16378441.html) 一個問題 淘寶網,京東、微信,抖音都有各自的功能,那麼當我們退出系統的時候,下次再訪問時,為什麼信息還存在? =》資料庫 解決之道-文件、資料庫 為瞭解決上述問題, ...
  • 案例1:MySQL8.0實現資料庫冷備份和還原 10.0.0.10 -- MySQL8.0 #停止資料庫 [root@CentOS8 my.cnf.d]# systemctl stop mysqld.service #備份數據 [root@CentOS8 ~]# scp -pr /var/lib/m ...
  • 在日常開發工作中,我經常會遇到需要統計總數的場景,比如:統計訂單總數、統計用戶總數等。一般我們會使用MySQL 的count函數進行統計,但是隨著數據量逐漸增大,統計耗時也越來越長,最後竟然出現慢查詢的情況,這究竟是什麼原因呢?本篇文章帶你一下學習一下。 ...
  • 當前伺服器上創建表(單節點) 創建新表具有幾種種語法形式,具體取決於用例。預設情況下,僅在當前伺服器上創建表。分散式DDL查詢作為子句實現,該子句另外描述。 語法形式 使用顯式架構 CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cl ...
  • 摘要:華為雲資料庫助力德邦快遞打造的“基於數智融合的一站式物流供應鏈平臺”項目從500多個項目中脫穎而出,榮獲2022 IDC中國未來企業大獎優秀獎“未來智能領軍者”。 本文分享自華為雲社區《華為雲資料庫助力德邦快遞斬獲IDC中國未來企業大獎“未來智能領軍者”優秀獎》,作者: GaussDB 資料庫 ...
  • “What I cannot create, I do not understand.” – Richard Feynman I’m building a clone of sqlite from scratch in C in order to understand, and I’m going ...
  • 一個沉浸感十足的游戲,其場景中的全局光照效果一定功不可沒。 動態漫反射全局光照(DDGI)帶來的光影變化,是細膩延展的視覺語言,讓場景中每種顏色都有了“五彩斑斕”的詮釋,場景佈局光影,物體關係立顯,環境溫度降臨,拓展了畫面信息傳達的層次,點睛之筆。 直接光渲染 VS 動態漫反射全局光照 細膩的光照視 ...
一周排行
    -Advertisement-
    Play Games
  • 不廢話,直接代碼 private Stack<Action> actionStack = new Stack<Action>(); private void SetCellValues() { var worksheet = Globals.ThisAddIn.Application.ActiveS ...
  • OpenAPI 規範是用於描述 HTTP API 的標準。該標準允許開發人員定義 API 的形狀,這些 API 可以插入到客戶端生成器、伺服器生成器、測試工具、文檔等中。儘管該標準具有普遍性和普遍性,但 ASP.NET Core 在框架內預設不提供對 OpenAPI 的支持。 當前 ASP.NET ...
  • @DateTimeFormat 和 @JsonFormat 是 Spring 和 Jackson 中用於處理日期時間格式的註解,它們有不同的作用: @DateTimeFormat @DateTimeFormat 是 Spring 框架提供的註解,用於指定字元串如何轉換為日期時間類型,以及如何格式化日 ...
  • 一、背景說明 1.1 效果演示 用python開發的爬蟲採集軟體,可自動抓取抖音評論數據,並且含二級評論! 為什麼有了源碼還開發界面軟體呢?方便不懂編程代碼的小白用戶使用,無需安裝python、無需懂代碼,雙擊打開即用! 軟體界面截圖: 爬取結果截圖: 以上。 1.2 演示視頻 軟體運行演示視頻:見 ...
  • SpringBoot筆記 SpringBoot文檔 官網: https://spring.io/projects/spring-boot 學習文檔: https://docs.spring.io/spring-boot/docs/current/reference/html/ 線上API: http ...
  • 作為後端工程師,多數情況都是給別人提供介面,寫的好不好使你得重視起來。 最近我手頭一些活,需要和外部公司對接,我們需要提供一個介面文檔,這樣可以節省雙方時間、也可以防止後續扯皮。這是就要考驗我的介面是否規範化。 1. 介面名稱清晰、明確 顧名思義,介面是做什麼的,是否準確、清晰?讓使用這一眼就能知道 ...
  • 本文介紹基於Python語言,遍歷文件夾並從中找到文件名稱符合我們需求的多個.txt格式文本文件,並從上述每一個文本文件中,找到我們需要的指定數據,最後得到所有文本文件中我們需要的數據的合集的方法~ ...
  • Java JUC&多線程 基礎完整版 目錄Java JUC&多線程 基礎完整版1、 多線程的第一種啟動方式之繼承Thread類2、多線程的第二種啟動方式之實現Runnable介面3、多線程的第三種實現方式之實現Callable介面4、多線的常用成員方法5、線程的優先順序6、守護線程7、線程的讓出8、線 ...
  • 實時識別關鍵詞是一種能夠將搜索結果提升至新的高度的API介面。它可以幫助我們更有效地分析文本,並提取出關鍵詞,以便進行進一步的處理和分析。 該介面是挖數據平臺提供的,有三種模式:精確模式、全模式和搜索引擎模式。不同的模式在分詞的方式上有所不同,適用於不同的場景。 首先是精確模式。這種模式會儘量將句子 ...
  • 1 為啥要折騰搭建一個專屬圖床? 技術大佬寫博客都用 md 格式,要在多平臺發佈,圖片就得有外鏈 後續如博客遷移,國內博客網站如掘金,簡書,語雀等都做了防盜鏈,圖片無法遷移 2 為啥選擇CloudFlare R2 跳轉:https://dash.cloudflare.com/ 有白嫖額度 免費 CD ...