React的第一次渲染過程淺析

React的第一次渲染過程淺析

本篇文章暫時討論Sync模式(同步),源碼爲16.9,部分源碼內容不討論(hooks classComponent等等相關的代碼)。javascript

a demo

先看一段react的代碼html

function Counter(props) {
  return (
    <div>
      <div>{props.count}</div>
      <button
        onClick={() => {
          console.log('l am button');
        }}
      >
        add
      </button>
    </div>
  )
}
function App(props) {
  return <Counter count="12" key="12" />;
}

ReactDOM.render(<App />, document.getElementById('app'));

jsx語法能夠經過babel對應的jsx插件須要轉義成可執行的代碼(try it out), 上述代碼<App />:java

// 轉義後的代碼
function App(props) {
  return React.createElement(CounterButton, {
    key: "12"
  });
}

// 結果
{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ƒ App(props),
}

建立fiberRoot

傳入ReactDOM.render函數的三個參數elementcontainercallbacknode

container_reactRootContainer屬性在第一次建立是不存在的,先要建立它react

// ReactDOM.js
let rootSibling;
while ((rootSibling = container.lastChild)) {
  container.removeChild(rootSibling);
}

先將container即咱們傳入div#app的全部子節點刪除 獲得的結果:git

// root
{
  _internalRoot: {
    current: FiberNode,
    containerInfo: div#app,
    ...
  }
}

current 指向的是 root fiber節點, containerInfo 執行 dom元素 id爲app的divgithub

unbatchedUpdates數組

接着使用unbatchedUpdates調用updateContainerunbatchedUpdates來自調度系統ReactFiberWorkLoopbabel

// ReactFiberWorkLoop.js
function unbatchedUpdates(fn, a) {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      flushSyncCallbackQueue();
    }
  }
}

全局變量executionContext表明當前的執行上下文, 初始化爲 NoContentapp

// ReactFiberWorkLoop.js

const NoContext = /*                    */ 0b000000;
const BatchedContext = /*               */ 0b000001;
const EventContext = /*                 */ 0b000010;
const DiscreteEventContext = /*         */ 0b000100;
const LegacyUnbatchedContext = /*       */ 0b001000;
const RenderContext = /*                */ 0b010000;
const CommitContext = /*                */ 0b100000;

executionContext &= ~BatchedContext表明什麼含義尼?

首先 & 操做當且當兩個位上都爲1的時候返回1,| 只要有一位爲1,返回1

executionContext則是這些Context組合的結果:
將當前上下文添加Render

executionContext |= RenderContext

判斷當前是否處於Render階段

executionContext &= RenderContext === NoContext

去除Render:

executionContext &= ~RenderContext

executionContext &= ~BatchedContext則表明把當前上下文的BatchedContext標誌位置爲false,表示當前爲非批量更新

在react源碼中有不少相似的位運算,好比effectTag,workTag。

reconciler(調和)

updateContainer

計算當前時間和當前的過時時間,因本文只討論同步模式因此這裏的expirationTime

// ReactFiberExpirationTime.js
const Sync = MAX_SIGNED_31_BIT_INT;

// ReactFiberWorkLoop.js
function computeExpirationForFiber(
  currentTime,
  fiber,
  suspenseConfig,
) {
  const mode = fiber.mode
  if ((mode & BatchedMode) === NoMode) {
    return Sync
  }
}

expirationTime越大,表明優先級越高,因此同步模式擁有最高的優先級。

updateContainerAtExpirationTime建立於context相關內容,後續有專門文章介紹context,這裏先不討論。

scheduleRootUpdate

// ReactFiberReconciler.js
function scheduleRootUpdate(
  current,
  element,
  expirationTime,
  suspenseConfig,
  callback,
) {
  const update = createUpdate(expirationTime, suspenseConfig);
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }
  enqueueUpdate(current, update);
  scheduleWork(current, expirationTime);

  return expirationTime;
}

建立update,將callback添加到update上。

{
  callback: null
  expirationTime: 1073741823
  next: null
  nextEffect: null
  payload: {element: {
    $$typeof: Symbol(react.element)
    key: null
    props: {}
    ref: null
    type: ƒ App(props)
  }}
  priority: 97
  suspenseConfig: null
  tag: 0
}

再更新添加到root fiber的更新隊列上,指的一提的是這裏的更新隊列updateQueue也採用了雙緩衝技術,兩條updateQueue經過alternate屬性
相互引用。這個鏈表大體爲:

{
  baseState: null
  firstCapturedEffect: null
  firstCapturedUpdate: null
  firstEffect: null
  firstUpdate: update
  lastCapturedEffect: null
  lastCapturedUpdate: null
  lastEffect: null
  lastUpdate: update
}

調用scheduleWork進入到調度階段。

scheduleWork(調度階段)

// ReactFiberWorkLoop.js
function scheduleUpdateOnFiber(fiber, expirationTime) {
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);

  if (expirationTime === Sync) {
    if (
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      let callback = renderRoot(root, Sync, true);
      while (callback !== null) {
        callback = callback(true);
      }
    }
  }
}

進入調度階段,首先調用markUpdateTimeFromFiberToRoot將fiber上的更新時間,此時的fiber樹只有一個root fiber光桿司令。

// ReactFiberWorkLoop.js
function markUpdateTimeFromFiberToRoot() {
  if (fiber.expirationTime < expirationTime) {
    fiber.expirationTime = expirationTime;
  }
  ...
  let alternate = fiber.alternate;

  let node = fiber.return;
  let root = null;
  if (node === null && fiber.tag === HostRoot) {
    root = fiber.stateNode;
  } else {
    ...
  }
  return root
}

這裏返回的root是個fiberRoot類型的節點。

繼續往下,條件expirationTime === Sync符合

executionContext & LegacyUnbatchedContext) !== NoContext &&
executionContext & (RenderContext | CommitContext)) === NoContext

這裏的兩個位運算,在unbatchedUpdates方法內將初始化的上下文NoContext添加了LegacyUnbatchedContext上下文,因此這裏獲得的結果是真。

renderRoot

renderRoot階段只要進行兩部分工做:一個是workLoop循環,即render階段 另外一個爲commitRoot,commit階段

// ReactFiberExpirationTime.js
const NoWork = 0

// ReactFiberWorkLoop.js
let workInProgressRoot = null
let renderExpirationTime = NoWork

function renderRoot(root, expirationTime) {
  ...
  if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
    prepareFreshStack(root, expirationTime);
  } 
  ...

  /* renderRoot-code-branch-01 */
}

此時的 workInProgressRootrenderExpirationTime都處於初始狀態。

function prepareFreshStack(root, expirationTime) {
  root.finishedWork = null;
  root.finishedExpirationTime = NoWork;
  ...
  workInProgressRoot = root;
  workInProgress = createWorkInProgress(root.current, null, expirationTime);
  renderExpirationTime = expirationTime;
  ...
}

prepareFreshStack顧名思義,準備一個新生的堆棧環境。
首先將finishedWork相關的變量初始化。
root賦給全局變量workInProgressRootexpirationTime賦給renderExpirationTime
爲root.current即root fiber節點建立一個workInProgress節點,並將該節點賦給全局變量workInProgressfiber節點也是應用了雙緩衝,兩個fiber節點經過alternate屬性保存了對方的引用 在更新的過程當中操做的是workInProgress節點。調度結束時 workInProgress fiber會替代current fiber

/* renderRoot-code-branch-01 */
if (workInProgress !== null) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;

  /* hooks-related ** start */
  let prevDispatcher = ReactCurrentDispatcher.current;
  if (prevDispatcher === null) {
    prevDispatcher = ContextOnlyDispatcher;
  }
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  /* hooks-related ** end */

  /* workLoop */
}

此時的workInProgress爲剛建立的那個節點。接着爲當前的上下文添加RenderContext,標誌着進入render階段。
hooks-related這部分代碼是與hooks先關的代碼,在這過程當中用戶調用hooks相關的API都不是在FunctionComponent的內部,因此都會報錯。

render階段

function workLoopSync() {
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

/* workLoop */
do {
  try {
    if (isSync) {
      workLoopSync()
    }
  } catch (error) {
    // ...
  }
  break
} while (true)

workLoop過程是一個遞歸的過程 從root階段向下遍歷到葉子節點,再從葉子節點執行一些遍歷的邏輯最後返回到root節點,此次過程執行beginWorkcompleteWork等操做,
在此過程當中建立fiber節點組裝fiber樹,建立對應的dom節點等等。

文章開始的代碼workLoop過程大體以下:

圖片描述

一個簡單的線上demo,根據代碼模擬workLoop執行過程地址(放在githubpage上的打開速度可能慢一些)

讓咱們開啓workLoop之旅吧!

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate
  ...
  let next = beginWork(current, unitOfWork, renderExpirationTime)
  unitOfWork.memoizedProps = unitOfWork.pendingProps

  if (next === null) {
    next = completeUnitOfWork(unitOfWork)
  }

  return next
}

在這個循環過程 beginWork順着element樹的向下深度遍歷 當遍歷到葉子節點時,即next爲null時, completeUnitOfWork則會定位next的值:

  1. 當前節點 是否有兄弟節點, 有,返回進行下一次beginWork;無則轉到2
  2. 當前節點置爲 父節點,父節點是否存在 存在,轉到1;不然返回null

固然這兩個過程所得工做不只僅就是這樣。

beginWork

// ReactFiberBeginWork.js
let didReceiveUpdate = false


function beginWork(
  current, workInProgress, renderExpirationTime
) {
  if (current !== null) {
    const oldProps = current.memoizedProps
    const newProps = workInProgress.pendingProps
    if (oldProps !== newProps || hasLegacyContextChanged()) {
      didReceiveUpdate = true;
    } else if (updateExpirationTime < renderExpirationTime) {
      ...
    }
  } else {
    didReceiveUpdate = true
  }

  workInProgress.expirationTime = NoWork;

  switch (workInProgress.tag) {
    case HostRoot: {
      return updateHostRoot(current, workInProgress, renderExpirationTime);
    }
    case 
  }
}

root fiber是存在current fiber的,但此時的oldPropsnewProps都爲null。雖然這裏不討論context,可是從

if (oldProps !== newProps || hasLegacyContextChanged()) {
  didReceiveUpdate = true;
}

咱們能夠看出舊的context API的低效。

在進入到beginWork以前先將expirationTime置爲NoWork

beginWork HostRoot
root fiber對應的更新爲HostRoot

// ReactFiberBeginWork.js
function updateHostRoot(current, workInProgress, renderExpirationTime) {
  const updateQueue = workInProgress.updateQueue;
  const nextProps = workInProgress.pendingProps;
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState !== null ? prevState.element : null;
  processUpdateQueue(
    workInProgress,
    updateQueue,
    nextProps,
    null,
    renderExpirationTime,
  );

  const nextState = workInProgress.memoizedState;
  const nextChildren = nextState.element;
  
  if (nextChildren === prevChildren) {
    ...
  }
  const root = workInProgress.stateNode
  if ((current === null || current.child === null) && root.hydrate) {
    ...
  } else {
    reconcileChildren(
      current,
      workInProgress,
      nextChildren,
      renderExpirationTime,
    );
  }
  return workInProgress.child;
}

scheduleRootUpdate建立的更新隊列咱們建立了一個更新隊列,裏面有一條更新。

processUpdateQueue對於所作的將隊列清空 將updatepayload合併到updateQueuebaseState屬性 同時添加到workInProgress節點的memoizedState
因此nextChildren就是memoizedStateelement屬性了。也就是

{
  $$typeof: Symbol(react.element)
  key: null
  props: {}
  ref: null
  type: ƒ App(props)
}

接着root.hydrate這個判斷是服務端渲染相關的代碼,這裏不涉及,因此走另外一個分支

// ReactFiberBeginWork.js
function reconcileChildren(
  current, workInProgress, nextChildren, renderExpirationTime
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}

根據 current 是否存在 走不一樣的分支,mountChildFibersmountChildFibers不一樣在於一個參數傳遞的問題。此時current.childnull

// ReactChildFiber.js
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);

ChildReconciler

ChildReconciler是一個高階函數,內部許多子方法,依次看來

// ReactChildFiber.js
function ChildReconciler(shouldTrackSideEffects) {
  function reconcileChildFibers(
    returnFiber,
    currentFirstChild,
    newChild,
    expirationTime
  ) {
    // Fragment相關內容 先跳過
    const isUnkeyedTopLevelFragment = false
    const isObject = typeof newChild === 'object' && newChild !== null;

    if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
      }
    }

    /**  **/
  }
}

這裏暫不討論 Fragment相關內容 直接將標誌位isUnkeyedTopLevelFragment置爲假。這裏的newChild對應着 App組件,isObject爲真,且newChild.$$typeof === REACT_ELEMENT_TYPE

reconcileSingleElement placeSingleChild

// ReactChildFiber.js
function reconcileSingleElement(
  returnFiber,
  currentFirstChild,
  element,
  expirationTime
) {
  const key = element.key
  let child = currentFirstChild
  while(child !== null) {
    ...
  }
  if (element.type === REACT_FRAGMENT_TYPE) {
    ...
  } else {
    const created = createFiberFromElement(
      element,
      returnFiber.mode,
      expirationTime,
    );
    // to do
    // created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
}

function placeSingleChild(newFiber) {
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.effectTag = Placement;
  }
  return newFiber
}

App組件對應的 fiber節點在以前並不存在,因此這裏建立fiber節點 並將fiber的父節點設爲 root fiber節點。以後在placeSingleChild爲fiber的effectTag打上 Placement
返回到beginWorkupdateHostRoot, 接着返回workInProgress.child,返回到completeUnitOfWork函數內,

next = beginWork()
if (next === null) {
  ...
}
return next

返回的爲新建立的App對應的 fiber,因此beginWork繼續執行。

回到剛纔的beginWork
建立的Function Component組件fiber默認的tag爲IndeterminateComponent,class Component會被指定爲ClassComponent

let fiber;
let fiberTag = IndeterminateComponent;
let resolvedType = type;
if (typeof type === 'function') {
  if (shouldConstruct(type)) {
    fiberTag = ClassComponent;
    ...
  } else {
    ...
  }
} else if (typeof type === 'string') {
  fiberTag = HostComponent;
}

回顧一下beginWork

let didReceiveUpdate = false

function beginWork() {
  ...
  if (current !== null) {
    ...
  } else {
    didReceiveUpdate = false
  }

  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderExpirationTime,
      );
    }
  }
}

mountIndeterminateComponent大體代碼:

function mountIndeterminateComponent(
  _current,
  workInProgress,
  Component,
  renderExpirationTime
) {
  if (_current !== null) {
    ...
  }

  const props = workInProgress.pendingProps
  
  ...
  let value = renderWithHooks(
    null,
    workInProgress,
    Component,
    props,
    context,
    renderExpirationTime,
  );

  if (typeof value === 'object' && value !== null && typeof value.render === 'function') {
    ...
  } else {
    workInProgress.tag = FunctionComponent;
    reconcileChildren(null, workInProgress, value, renderExpirationTime);
  }

  return workInProgress.child;
}

這裏的renderWithHooks先簡單當作 Component(props),後面部分介紹hooks相關代碼。

返回的value爲:

React.createElement(Counter, {
  count: "12",
  key: "12"
})

// value
{
  $$typeof: Symbol(react.element)
  key: "12"
  props: {}
  ref: null
  type: ƒ CounterButton(props)
}

reconcileChildren --> mountChildFibersCounter組件建立fiber與建立App的fiber邏輯基本相同。所不一樣的是effectTag沒有被標記。

beginWork Counter, renderWithHooks 返回的是div,接着建立下一次beginWork的fiber。

{
  $$typeof: Symbol(react.element)
  key: null
  props: {children: Array(2)}
  ref: null
  type: "div"
}

beginWork: HostComponent

case HostComponent:
  return updateHostComponent(current, workInProgress, renderExpirationTime);
// ReactDOMHostConfig.js
function shouldSetTextContent(type: string, props: Props): boolean {
  return (
    type === 'textarea' ||
    type === 'option' ||
    type === 'noscript' ||
    typeof props.children === 'string' ||
    typeof props.children === 'number' ||
    (typeof props.dangerouslySetInnerHTML === 'object' &&
      props.dangerouslySetInnerHTML !== null &&
      props.dangerouslySetInnerHTML.__html != null)
  );
}

// ReactFiberBeginWork.js
function updateHostComponent(
  current,
  workInProgress,
  renderExpirationTime,
) {
  const type = workInProgress.type
  const nextProps = workInProgress.pendingProps
  const prevProps = current !== null ? current.memoizedProps : null

  let nextChildren = nextProps.children
  const isDirectTextChild = shouldSetTextContent(type, nextProps)
  if (isDirectTextChild) {
    nextChildren = null
  } else if (...) {
    ...
  }

  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}

這裏的pendingProps,就是div的props 爲 span button的數組。
shouldSetTextContent則判斷當前元素可不能夠擁有子元素,或者children能夠做爲一個text節點 以後繼續調用 reconcileChildren --> mountChildFibers

此時nextChildren是一個數組結構 在ReactFiberChildreconcileChildFibers相應的代碼:

if (isArray(newChild)) {
  return reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChild,
    expirationTime,
  );
}

function reconcileChildrenArray(
  returnFiber,
  currentFirstChild,
  newChildren,
  expirationTime,
) {
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;

  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;

  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    ...
  }

  if (newIdx === newChildren.length) {
    ...
  }

  if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(
        returnFiber,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber === null) {
        continue;
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }
}

因爲第一次建立 此時的currentFirstChild爲null,reconcileChildrenArray代碼不少,可是第一次用到的很少,主要遍歷children 爲它們建立fiber,並添加到fiber樹上。
最後返回第一個child的fiber 也就是span對應的fiber。

接着對 span進行beginWork, 此時的isDirectTextChild標誌位爲true。nextChildren則爲null。reconcileChildFibers結果返回null。

此時回到workLoop的performUnitOfWork,由於next爲null,則進行下一步 completeUnitOfWork

completeUnitOfWork

function completeUnitOfWork(unitOfWork) {
  workInProgress = unitOfWork
  do {
    const current = workInProgress.alternate
    const returnFiber = workInProgress.return

    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      let next = completeWork(current, workInProgress, renderExpirationTime);

      if (next !== null) {
        return null
      }
      ...
      /* completeUnitOfWork-code-01 */
    } else {
      ...
    }
    /* completeUnitOfWork-code-02 */
    const siblingFiber = workInProgress.sibling;
    if (siblingFiber !== null) {
      return siblingFiber;
    }
    workInProgress = returnFiber;
    /* completeUnitOfWork-code-02 */
  } while (workProgress !== null)
}

此時傳入的unitOfWork爲span對應的fiber。 將全局變量workInProgress賦值爲unitWork

(workInProgress.effectTag & Incomplete) === NoEffect顯然爲true。調用completeWork返回下一次的工做內容

completeWork

function completeWork(
  current,
  workInProgress,
  renderExpirationTime
) {
  const newProps = workInProgress.pendingProps
  switch (workInProgress.tag) {
    ...
    case HostComponent: {
      const rootContainerInfo = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        ...
      } else {
        const currentHostContext = getHostContext();
        let instance = createInstance(
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
          workInProgress,
        );

        appendAllChildren(instance, workInProgress, false, false);

        if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
          workInProgress.stateNode = instance;
      }
    }
  }
  return null;
}

此處的rootContainerInfo先把他認爲是div#app,繼續忽略currentHostContext。建立過程能夠理解爲三步:

  1. createInstance: 建立dom等
  2. appendAllChildren: 將children的host Component添加到剛建立的dom上 組成dom樹。
  3. finalizeInitialChildren: 給dom設置屬性。

先詳細看一下createInstance實現

// ReactDOMComponentTree.js
export function updateFiberProps(node, props) {
  node[internalEventHandlersKey] = props;
}

export function precacheFiberNode(hostInst, node) {
  node[internalInstanceKey] = hostInst;
}

// ReactDOMHostConfig
function createInstance(
  type,
  props,
  rootContainerInstance,
  hostContext,
  internalInstanceHandle
) {
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}

createElement先暫時理解爲 document.createElement
precacheFiberNode則是 將fiber實例添加到dom上。
updateFiberProps 將fiber實例添加到dom上

雖然是同樣將fiber添加到dom上 經過key的命名能夠發現用途不一樣,updateFiberProps是爲事件系統作準備的。internalInstanceKey估計就是爲了保持引用,取值判斷等用途

appendAllChildren 這裏先跳過,到complete div的時候具體分析一下。

因爲是第一次渲染也就不存在diff props的過程,這裏的finalizeInitialChildren的職責也相對簡單些,設置dom元素的一些初始值。在設置初始值的時候對應不一樣的dom元素有特殊的處理,這些部分咱們也先跳過

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  // return shouldAutoFocusHostComponent(type, props);
  ...
}

function setInitialProperties(
  domElement,
  tag,
  rawProps,
  rootContainerElement,
) {
  ...
  const isCustomComponentTag = true
  switch (tag) {
    ...
  }
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );
}

function setInitialDOMProperties(
  tag,
  domElement,
  rootContainerElement,
  nextProps,
) {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      ...
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      ...
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);
        }
      } else if (typeof nextProp === 'number') {
        setTextContent(domElement, '' + nextProp);
      }
    } else if (registrationNameModules.hasOwnProperty(propKey)) {
      ...
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

在設置dom屬性的時候,有幾個注意點 一個是style屬性的設置 最終的style屬性是字符串,而咱們寫的則是屬性名是駝峯命名的對象。感興趣的可自行查看setValueForStyles

span的children屬性是被當作文字節點設置

// setTextContent.js
function(node, text) {
  if (text) {
    let firstChild = node.firstChild;
    if (
      firstChild &&
      firstChild === node.lastChild &&
      firstChild.nodeType === TEXT_NODE
    ) {
      firstChild.nodeValue = text;
      return;
    }
  }
  node.textContent = text;
}

回到completeWork,最後將建立的dom添加到fiber的stateNode屬性上,返回null 結束completeWork調用

返回到completeUnitOfWork/* completeUnitOfWork-code-01 */

/* completeUnitOfWork-code-01 */
if (
  returnFiber !== null
  && (returnFiber.effectTag & Incomplete) === NoEffect
) {
  if (returnFiber.effect === null) {
    returnFiber.firstEffect = workInProgress.firstEffect
  }

  if (workInProgress.lastEffect !== null) {
    if (returnFiber.lastEffect !== null) {
      returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
    }
    returnFiber.lastEffect = workInProgress.lastEffect;
  }

  const effectTag = workInProgress.effectTag;

  if (effectTag > PerformedWork) {
    if (returnFiber.lastEffect !== null) {
      returnFiber.lastEffect.nextEffect = workInProgress;
    } else {
      returnFiber.firstEffect = workInProgress;
    }
    returnFiber.lastEffect = workInProgress;
  }
}

將span節點的 effectList歸併到父組件上(但此時span fiber上並無effect), 此時子組件沒有任何effect,且 effectTag 爲 0。

/* completeUnitOfWork-code-02 */
const siblingFiber = workInProgress.sibling;
if (siblingFiber !== null) {
  return siblingFiber;
}
workInProgress = returnFiber;
/* completeUnitOfWork-code-02 */

/* completeUnitOfWork-code-02 */,若是當前節點有兄弟節點,則返回,沒有則返回父節點繼續 completeWork。
此時span有一個建立了fiber可是沒有進行beginWork的兄弟節點button

button節點經歷過beginWork, completeWork,又回到了/* completeUnitOfWork-code-02 */處。button 節點沒有兄弟節點,workInProgress被置爲了 div 節點,進行
div的 completeWork

div的completeWork與 span和button不一樣之處在於appendAllChildren,以前跳過的部分如今分析一下

function appendAllChildren(
  parent,
  workInProgress,
) {
  let node = workInProgress.child;
  while (node !== null) {
    if (node.tag === HostComponent || node.tag === HostText) {
      // condition 01
      appendInitialChild(parent, node.stateNode.instance);
    } else if (...*2) {

    } else if (node.child !== null) {
      // condition 03
      node.child.return = node;
      node = node.child;
      continue;
    }

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

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

div的child爲 span且知足 condition 01,將span添加到div上,輪到button fiber 一樣將 button 添加到div 上。
condition 04處 是當前的返回出口:找到最後一個sibling,在向上查找到 div節點 返回。

咱們實際應用中,上述的 div>span-button 算是最簡單操做。有不少想 div 與 span、button 又隔了一層Function/Class Component。此時就須要利用到
condition 03 繼續向child查找,查找各個分叉向下距離workInProgress最近的host節點,將他們添加到workInProgress對應的dom上,這樣dom樹才能完整構成。

這樣 divcompleteWork就完成了,繼續到Counter組件:

Component組件的completeWork是直接被break,因此這裏只須要將effectList歸併到父節點。

/* completeUnitOfWork-code-02 */節點到Counter的returnFiberApp 節點,App節點與其餘節點不一樣的地方在於其effectTag爲3。這是怎麼來的尼?還記得咱們的 root fiber節點在beginWork時與其餘節點不一樣的地方在於:它是有 current節點的,因此做爲children的App,在placeSingleChild的時候effectTag被添加了Placement,在beginWorkmountIndeterminateComponent時,Component組件的effectTag被添加了PerformedWork

迴歸一下/* completeUnitOfWork-code-01 */處代碼,只有到App知足effectTag > PerformedWork,在以前出現的 host 節點的effectTag 都爲0,Function節點都爲 1(PerformedWork),都不符合添加effect的要求。因此到此時纔有一個effect,它被添加到了root Fiber上。

root fiber的completeWork,它的tagHostRoot

// ReactFiberCompleteWork.js

updateHostContainer = function (workInProgress) {
  // Noop
};

case HostRoot: {
  ...
  if (current === null || current.child === null) {
    workInProgress.effectTag &= ~Placement;
  }
  // updateHostContainer(workInProgress)
}

這裏current.child爲null,由於咱們以前beginWork時,改變的是workInProgress節點,這裏將Placement effectTag取消。結束 completeWork。

這時咱們已經到達了root節點,作一些收尾工做

// ReactWorkLoop.js
function completeUnitOfWork(unitOfWork) {
  workInProgress = unitOfWork
  do {

  } while (workInProgress !== null)

  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
  return null;
}

workLoopSync結束以後,將執行上下文由RenderContext重置爲上次的執行環境

root.finishedWork = root.current.alternate;
root.finishedExpirationTime = expirationTime;

以後將workLoop所作的工做添加到root的finishedWork

workLoopSync部分, 也能夠成爲render階段到此結束。回顧一下在此期間所作的主要工做。

  • 建立各個節點對應的workInProgress fiber節點
  • 建立dom節點,設置屬性,鏈接構成dom樹(並未append到container上)
  • 爲節點打上effectTag,構建完整的effectList鏈表,從葉子節點歸併到root fiber節點上。

commit階段

繼續回來renderRoot

function commitRoot() {
  ...
  workInProgressRoot = null

  switch (workInProgressRootExitStatus) {
    case RootComplete: {
      ...
      return commitRoot.bind(null, root);
    }
  }
}

workInProgressRoot置爲null,在completeWork時將workInProgressRootExitStatus置爲了RootCompleted,以後進入commitRoot階段。

暫不討論優先級調度相關的代碼,完整代碼戳我 這裏當作:

function commitRoot(root) {
  commitRootImpl.bind(null, root, renderPriorityLevel)
  if (rootWithPendingPassiveEffects !== null) {
    flushPassiveEffects();
  }
  return null;
}
  • commitBeforeMutationEffects
  • commitMutationEffects
  • commitLayoutEffects

commitRoot源碼主要內容是以上遍歷effectList的三個循環,看看他們作了什麼吧

let nextEffect = null

function commitRootImpl(root, renderPriorityLevel) {
    const finishWork = root.finishWork
    const expirationTime = root.finishedExpirationTime
    ...

    root.finishedWork = null;
    root.finishedExpirationTime = NoWork;
    
    let firstEffect
    if (finishedWork.effectTag > PerformedWork) {
        // 將自身effect添加到effect list上
        ...
    }

    if (firstEffect !== null) {
        const prevExecutionContext = executionContext;
        executionContext |= CommitContext;
        
        do {
            try {
                commitBeforeMutationEffects();
            } catch (error) {
                ..
            }
        } while (nextEffect !== null)

        ...

        ...
        nextEffect = null;
        executionContext = prevExecutionContext;
    }

}

先獲取effectList,在render階段生成的effect list並不包含自身的effect,這裏先添加(但此時finishedWork.effectTag其實爲0),獲取完整的effectList。
以後把當前的執行上下文置爲CommitContext, 正式進入commit階段。

此時effectList其實就是App節點的workInProgress fiber。這裏有一個全局變量nextEffect表示當前正在處理的effect

commitBeforeMutationEffects

function commitBeforeMutationEffects() {
    while (nextEffect !== null) {
        if ((nextEffect.effectTag & Snapshot) !== NoEffect) {
            ...
            const current = nextEffect.alternate;
            commitBeforeMutationEffectOnFiber(current, nextEffect);
            ...
        }
        nextEffect = nextEffect.nextEffect;
  }
}

這個App fiber上的effectTag爲 3 (Placement | Update),這個循環直接跳過了

function commitMutationEffects() {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag
        ...

        let primaryEffectTag = effectTag & (Placement | Update | Deletion)

        switch (primaryEffectTag) {
            ...
            case PlacementAndUpdate: {
                commitPlacement(nextEffect)
                nextEffect.effectTag &= ~Placement;

        // Update
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
            }
        }

        nextEffect = nextEffect.nextEffect;
    }
}

commitPlacement

commitPlacement主要是把dom元素添加到對應的父節點上,對於第一次渲染其實也只是將div添加到div#app上。並將當前的effectTag update去掉。

commitWork

// ReactFiberCommitWork.js
function commitWork(current, finishedWork) {
    switch (finishedWork.tag) {
        case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // Note: We currently never use MountMutation, but useLayout uses
      // UnmountMutation.
      commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
            return;
        
        case HostComponent: {
            ...
        }
    }
}

這裏commitWork有涉及到hook組件的部分,這裏暫時跳過。
對於 host組件實際上是有先後props diff的部分,這裏是第一次渲染,因此也就不存在,因此這裏也沒有多少第一渲染須要作的工做。

commitLayoutEffects

// ReactFiberWorkLoop.js

import { commitLifeCycles as commitLayoutEffectOnFiber } from 'ReactFiberCommitWork'

function commitLayoutEffects() {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;
        if (effectTag & (Update | Callback)) {
      recordEffect();
      const current = nextEffect.alternate;
      commitLayoutEffectOnFiber(
        root,
        current,
        nextEffect,
        committedExpirationTime,
      );
        }
        ...
        nextEffect = nextEffect.nextEffect
    }
    ...
}

App fiber上的effectTag如今剩下1(PerformedWork),並不符合因此噹噹循環也跳出。順便一提,若是咱們的ReactDOM.render有callback的話 將會在這裏執行。

三個循環結束以後將nextEffect置爲null;執行上下文變動成以前的執行上下文。
function commitRootImpl() {
    ...
    if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
    return null;
    }
}

如今咱們的執行上下文還剩下在upbatchedUpdate添加的LegacyUnbatchedContext,因此這裏直接返回。到這裏咱們第一渲染過程到這也就基本結束了。

總結一下commit工做:

  1. 處理beginWork產出 finishedWork的effectList
  2. 將dom添加到屏幕上(div#app container)
  3. callback調用
  4. hooks相關邏輯(未涉及)
  5. classComponent的生命週期邏輯(未涉及)
  6. 其餘

本文在走源碼的時候也有有許多部分沒有涵蓋 或者直接跳過的地方:

  • 更新過程 hooks組件更新 classComponent setState更新
  • Hooks
  • ClassComponent、 SimpleMemoComponent、HostPortal、SuspenseComponent、SuspenseListComponent等
  • 事件相關
  • context ref 等
  • scheduler模塊
  • 其餘

尾聲

本文是筆者跟着源碼debugger寫出來的文章,對於缺失的部分,計劃慢慢會有對應的介紹部分。另外本文屬於流水帳類型的文章,分析部分很是少,忘你們多多包涵、提提意見,你的參與就是個人動力。

相關文章
相關標籤/搜索