React源碼解析之FunctionComponent(上)

前言:
React源碼解析之workLoop 中講到當workInProgress.tagFunctionComponent時,會進行FunctionComponent的更新:javascript

    //FunctionComponent的更新
    case FunctionComponent: {
      //React 組件的類型,FunctionComponent的類型是 function,ClassComponent的類型是 class
      const Component = workInProgress.type;
      //下次渲染待更新的 props
      const unresolvedProps = workInProgress.pendingProps;
      // pendingProps
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      //更新 FunctionComponent
      //能夠看到大部分是workInProgress的屬性
      //之因此定義變量再傳進去,是爲了「凍結」workInProgress的屬性,防止在 function 裏會改變workInProgress的屬性
      return updateFunctionComponent(
        //workInProgress.alternate
        current,
        workInProgress,
        //workInProgress.type
        Component,
        //workInProgress.pendingProps
        resolvedProps,
        renderExpirationTime,
      );
    }
複製代碼

本文就來分析FunctionComponent是如何更新的html

1、updateFunctionComponent
做用:
執行FunctionComponent的更新java

源碼:node

//更新 functionComponent
//current:workInProgress.alternate
//Component:workInProgress.type
//resolvedProps:workInProgress.pendingProps
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderExpirationTime,
{
  //刪掉了 dev 代碼
  //後面講 context 的時候再做說明
  const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
  const context = getMaskedContext(workInProgress, unmaskedContext);

  let nextChildren;
  //作update 標記可不看
  prepareToReadContext(workInProgress, renderExpirationTime);
  prepareToReadEventComponents(workInProgress);
  //刪掉了 dev 代碼

  //在渲染的過程當中,對裏面用到的 hook函數作一些操做
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderExpirationTime,
    );

  //若是不是第一次渲染,而且沒有接收到更新的話
  //didReceiveUpdate:更新上的優化
  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderExpirationTime);
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }

  // React DevTools reads this flag.
  //代表當前組件在渲染的過程當中有被更新到
  workInProgress.effectTag |= PerformedWork;
  //將 ReactElement 變成 fiber對象,並更新,生成對應 DOM 的實例,並掛載到真正的 DOM 節點上
  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}
複製代碼

解析:
(1) 在「前言」的代碼裏也能夠看到,傳入updateFunctionComponent的大部分參數都是workInProgress這個 fiber 對象的屬性react

我在看這段的時候,突然冒出一個疑問,爲何不直接傳一個workInProgress對象呢?
我本身的猜想是在外面「凍結」這些屬性,防止在updateFunctionComponent()中,修改這些屬性git

(2) 在updateFunctionComponent()中,主要是執行了兩個函數:
① renderWithHooks()
② reconcileChildren()github

執行完這兩個方法後,最終返回workInProgress.child,即正在執行更新的 fiber 對象的第一個子節點web

(3) bailoutOnAlreadyFinishedWork()React源碼解析之workLoop 中已經解析過,其做用是 跳過該節點及該節點上全部子節點的更新數組

(4) bailoutHooks() 的源碼很少,做用是 跳過 hooks 函數的更新:app

//跳過hooks更新
export function bailoutHooks(
  current: Fiber,
  workInProgress: Fiber,
  expirationTime: ExpirationTime,
{
  workInProgress.updateQueue = current.updateQueue;
  workInProgress.effectTag &= ~(PassiveEffect | UpdateEffect);
  //置爲NoWork 不更新
  if (current.expirationTime <= expirationTime) {
    current.expirationTime = NoWork;
  }
}
複製代碼

2、renderWithHooks
做用:
在渲染的過程當中,對裏面用到的 hooks 函數作一些操做

源碼:

//渲染的過程當中,對裏面用到的 hook函數作一些操做
export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any 
{
  renderExpirationTime = nextRenderExpirationTime;
  //當前正要渲染的 fiber 對象
  currentlyRenderingFiber = workInProgress;
  //第一次的 state 狀態
  nextCurrentHook = current !== null ? current.memoizedState : null;
  //刪除了 dev 代碼

  // The following should have already been reset
  // currentHook = null;
  // workInProgressHook = null;

  // remainingExpirationTime = NoWork;
  // componentUpdateQueue = null;

  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;
  // sideEffectTag = 0;

  // TODO Warn if no hooks are used at all during mount, then some are used during update.
  // Currently we will identify the update render as a mount because nextCurrentHook === null.
  // This is tricky because it's valid for certain types of components (e.g. React.lazy)

  // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
  // Non-stateful hooks (e.g. context) don't get added to memoizedState,
  // so nextCurrentHook would be null during updates and mounts.

  //刪除了 dev 代碼

    //第一次渲染調用HooksDispatcherOnMount
    //屢次渲染調用HooksDispatcherOnUpdate

    //用來存放 useState、useEffect 等 hook 函數的對象
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

  //workInProgress.type,這裏能當作 function 使用,說明 type 是 function
  let children = Component(props, refOrContext);
  //判斷在執行 render的過程當中是否有預約的更新

  //當有更新要渲染時
  if (didScheduleRenderPhaseUpdate) {
    do {
      //置爲 false 說明該循環只會執行一次
      didScheduleRenderPhaseUpdate = false;
      //從新渲染時fiber 的節點數
      numberOfReRenders += 1;

      // Start over from the beginning of the list
      //記錄 state,以便從新執行這個 FunctionComponent 內部的幾個 useState 函數
      nextCurrentHook = current !== null ? current.memoizedState : null;
      nextWorkInProgressHook = firstWorkInProgressHook;
      //釋放當前 state
      currentHook = null;
      workInProgressHook = null;
      componentUpdateQueue = null;

      if (__DEV__) {
        // Also validate hook order for cascading updates.
        hookTypesUpdateIndexDev = -1;
      }
      //HooksDispatcherOnUpdate
      ReactCurrentDispatcher.current = __DEV__
        ? HooksDispatcherOnUpdateInDEV
        : HooksDispatcherOnUpdate;

      children = Component(props, refOrContext);
    } while (didScheduleRenderPhaseUpdate);

    renderPhaseUpdates = null;
    numberOfReRenders = 0;
  }

  // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there's no re-entrancy.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  //定義新的 fiber 對象
  const renderedWork: Fiber = (currentlyRenderingFiber: any);
  //爲屬性賦值
  renderedWork.memoizedState = firstWorkInProgressHook;
  renderedWork.expirationTime = remainingExpirationTime;
  renderedWork.updateQueue = (componentUpdateQueue: any);
  renderedWork.effectTag |= sideEffectTag;

  if (__DEV__) {
    renderedWork._debugHookTypes = hookTypesDev;
  }

  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  //重置
  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;

  currentHook = null;
  nextCurrentHook = null;
  firstWorkInProgressHook = null;
  workInProgressHook = null;
  nextWorkInProgressHook = null;

  if (__DEV__) {
    currentHookNameInDev = null;
    hookTypesDev = null;
    hookTypesUpdateIndexDev = -1;
  }

  remainingExpirationTime = NoWork;
  componentUpdateQueue = null;
  sideEffectTag = 0;

  // These were reset above
  // didScheduleRenderPhaseUpdate = false;
  // renderPhaseUpdates = null;
  // numberOfReRenders = 0;

  invariant(
    !didRenderTooFewHooks,
    'Rendered fewer hooks than expected. This may be caused by an accidental ' +
      'early return statement.',
  );

  return children;
}
複製代碼

解析:
在開發者使用FunctionComponent來寫 React 組件的時候,是不能用setState的,取而代之的是useState()useEffect等 Hook API

因此在更新FunctionComponent的時候,會先執行renderWithHooks()方法,來處理這些 hooks

(1) nextCurrentHook 是根據current來賦值的,因此 nextCurrentHook 也能夠用來判斷是不是 組件第一次渲染

(2) 不管是HooksDispatcherOnMount仍是HooksDispatcherOnUpdate,它們都是 存放 useState、useEffect 等 hook 函數的對象:

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useEvent: updateEventComponentInstance,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useEvent: updateEventComponentInstance,
};
複製代碼

能夠看到,每一個 Hook API 都對應一個更新的方法,這些咱們後面再細說

(3) let children = Component(props, refOrContext);這行我其實沒看懂,由於ComponentworkInProgress.type,它的值能夠是function或是class,但我沒想到能夠當作方法去調用Component(props, refOrContext)

因此我如今暫時還不知道 children 究竟是個啥,後面若是有新發現的話,會在「前言」中提到。

(4) 而後是當didScheduleRenderPhaseUpdatetrue時,執行一個while循環,在循環中,會保存 state 的狀態,並重置 hook、組件更新隊列爲 null,最終再次執行Component(props, refOrContext),得出新的 children

didScheduleRenderPhaseUpdate:

// Whether an update was scheduled during the currently executing render pass.
//判斷在執行 render的過程當中是否有預約的更新
let didScheduleRenderPhaseUpdate: boolean = false;
複製代碼

這個循環,個人一個疑惑是,while中將didScheduleRenderPhaseUpdate置爲false,那麼這個循環只會執行一次,爲何要用while? 爲何沒用if...else

暫時也是沒有答案

(5) 定義新的 fiber 對象來保留操做 hooks 後獲得的一些變量,最後再將有關 hooks 的變量都置爲 null,return children

3、reconcileChildren
做用:
將 ReactElement 變成fiber對象,並更新,生成對應 DOM 的實例,並掛載到真正的 DOM 節點上

源碼:

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
{
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.

    //由於是第一次渲染,因此不存在current.child,因此第二個參數傳的 null
    //React第一次渲染的順序是先父節點,再是子節點

    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}
複製代碼

解析:
mountChildFibers()reconcileChildFibers()調用的是同一個函數ChildReconciler

//true false
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
複製代碼

false 表示是第一次渲染,true 反之

4、ChildReconciler
做用:
reconcileChildren()

這個方法有 1100 多行,前面全是 function 的定義,最後返回reconcileChildFibers,因此咱們從後往前看

源碼:

//是否跟蹤反作用
function ChildReconciler(shouldTrackSideEffects{
  xxx
  xxx
  xxx


  function reconcileChildFibers(): Fiber | null {

  }

  return reconcileChildFibers;
}
複製代碼

解析:
第一次渲染時無反作用(sideEffect)的,因此shouldTrackSideEffects=false,屢次渲染是有反作用的,因此shouldTrackSideEffects=true

這個方法太長了,先看最後 return 的reconcileChildFibers

5、reconcileChildFibers
做用:
針對不一樣類型的節點,進行不一樣的節點操做

源碼:

 // This API will tag the children with the side-effect of the reconciliation
  // itself. They will be added to the side-effect list as we pass through the
  // children and the parent.
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    //新計算出來的 children
    newChild: any,
    expirationTime: ExpirationTime,
  
): Fiber | null 
{
    // This function is not recursive.
    // If the top level item is an array, we treat it as a set of children,
    // not as a fragment. Nested arrays on the other hand will be treated as
    // fragment nodes. Recursion happens at the normal flow.

    // Handle top level unkeyed fragments as if they were arrays.
    // This leads to an ambiguity between <>{[...]}</> and <>...</>.
    // We treat the ambiguous cases above the same.
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      //在開發中寫<div>{ arr.map((a,b)=>xxx) }</div>,這種節點稱爲 REACT_FRAGMENT_TYPE
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    //type 爲REACT_FRAGMENT_TYPE是不須要任何更新的,直接渲染子節點便可
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

    // Handle object types
    const isObject = typeof newChild === 'object' && newChild !== null;
    //element 節點
    if (isObject) {
      switch (newChild.$$typeof) {
        // ReactElement節點
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
        //ReactDOM.createPortal(child, container)
        //https://zh-hans.reactjs.org/docs/react-dom.html#createportal
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              expirationTime,
            ),
          );
      }
    }
    //文本節點
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          expirationTime,
        ),
      );
    }
    //數組節點
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        expirationTime,
      );
    }
    //IteratorFunction
    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(
        returnFiber,
        currentFirstChild,
        newChild,
        expirationTime,
      );
    }
    //若是未符合上述的 element 節點的要求,則報錯
    if (isObject) {
      throwOnInvalidObjectType(returnFiber, newChild);
    }


    //刪除了 dev 代碼

    //報出警告,可不看
    if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
      // If the new child is undefined, and the return fiber is a composite
      // component, throw an error. If Fiber return types are disabled,
      // we already threw above.

      //即workInProgress,正在更新的節點
      switch (returnFiber.tag) {
        case ClassComponent: {
          //刪除了 dev 代碼

        }
        // Intentionally fall through to the next case, which handles both
        // functions and classes
        // eslint-disable-next-lined no-fallthrough
        case FunctionComponent: {
          const Component = returnFiber.type;
          invariant(
            false,
            '%s(...): Nothing was returned from render. This usually means a ' +
            'return statement is missing. Or, to render nothing, ' +
            'return null.',
            Component.displayName || Component.name || 'Component',
          );
        }
      }
    }

    // Remaining cases are all treated as empty.

    //若是舊節點存在,可是更新的節點是 null 的話,須要刪除舊節點的內容
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }
複製代碼

解析:
① isUnkeyedTopLevelFragment
當咱們在開發中寫了 如

<div>{ arr.map((a,b)=>xxx) }</div>
複製代碼

的代碼的時候,這種節點類型會被斷定爲REACT_FRAGMENT_TYPE,React 會直接渲染它的子節點:

newChild = newChild.props.children;
複製代碼

② 若是 element type 是 object 的話,也就是ClassComponentFunctionComponent
會有兩種狀況:
一個是REACT_ELEMENT_TYPE,即咱們常見的 ReactElement 節點;
另外一個是REACT_PORTAL_TYPE,portal 節點,一般被應用於 對話框、懸浮卡、提示框上,具體請參考官方文檔:Portals

REACT_ELEMENT_TYPE 的話,會執行reconcileSingleElement方法

③ 若是是文本節點的話,會執行reconcileSingleTextNode方法

④ 若是執行到最後的deleteRemainingChildren話,說明待更新的節點是 null,須要刪除原有舊節點的內容

能夠看到ChildReconciler中的reconcileChildFibers方法的做用就是根據新節點newChild的節點類型,來執行不一樣的操做節點函數

下篇文章,會講reconcileSingleElementreconcileSingleTextNodedeleteRemainingChildren

GitHub:
ReactFiberBeginWork

ReactFiberHooks

ReactChildFiber


(完)

相關文章
相關標籤/搜索