前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码解析之FunctionComponent(上)

React源码解析之FunctionComponent(上)

作者头像
进击的小进进
发布2019-12-02 21:10:16
1K0
发布2019-12-02 21:10:16
举报
文章被收录于专栏:前端干货和生活感悟

前言

在 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是如何更新的

一、updateFunctionComponent

作用: 执行FunctionComponent的更新

源码:

代码语言:javascript
复制
//更新 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 对象的属性

我在看这段的时候,忽然冒出一个疑问,为什么不直接传一个workInProgress对象呢? 我自己的猜测是在外面「冻结」这些属性,防止在updateFunctionComponent()中,修改这些属性

(2) 在updateFunctionComponent()中,主要是执行了两个函数: ① renderWithHooks() ② reconcileChildren()

执行完这两个方法后,最终返回workInProgress.child,即正在执行更新的 fiber 对象的第一个子节点

(3) bailoutOnAlreadyFinishedWork()在 React源码解析之workLoop 中已经解析过,其作用是 跳过该节点及该节点上所有子节点的更新

(4) bailoutHooks() 的源码不多,作用是 跳过 hooks 函数的更新:

代码语言:javascript
复制
//跳过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;
  }
}

二、renderWithHooks

作用: 在渲染的过程中,对里面用到的 hooks 函数做一些操作

源码:

代码语言:javascript
复制
//渲染的过程中,对里面用到的 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 函数的对象:

代码语言:javascript
复制
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:

代码语言:javascript
复制
// 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

三、reconcileChildren

作用: 将 ReactElement 变成fiber对象,并更新,生成对应 DOM 的实例,并挂载到真正的 DOM 节点上

源码:

代码语言:javascript
复制
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

代码语言:javascript
复制
//true false
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

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

四、ChildReconciler

作用:reconcileChildren()

这个方法有 1100 多行,前面全是 function 的定义,最后返回reconcileChildFibers,所以我们从后往前看

源码:

代码语言:javascript
复制
//是否跟踪副作用
function ChildReconciler(shouldTrackSideEffects) {
  xxx
  xxx
  xxx


  function reconcileChildFibers(): Fiber | null {
    
  }
  
  return reconcileChildFibers;
}

解析: 第一次渲染时无副作用(sideEffect)的,所以shouldTrackSideEffects=false,多次渲染是有副作用的,所以shouldTrackSideEffects=true

这个方法太长了,先看最后 return 的reconcileChildFibers

五、reconcileChildFibers

作用: 针对不同类型的节点,进行不同的节点操作

源码:

代码语言:javascript
复制
 // 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 当我们在开发中写了 如

代码语言:javascript
复制
<div>{ arr.map((a,b)=>xxx) }</div>

的代码的时候,这种节点类型会被判定为REACT_FRAGMENT_TYPE,React 会直接渲染它的子节点:

代码语言:javascript
复制
newChild = newChild.props.children;

② 如果 element type 是 object 的话,也就是ClassComponentFunctionComponent会有两种情况: 一个是REACT_ELEMENT_TYPE,即我们常见的 ReactElement 节点; 另一个是REACT_PORTAL_TYPE,portal 节点,通常被应用于 对话框、悬浮卡、提示框上,具体请参考官方文档:Portals(https://zh-hans.reactjs.org/docs/portals.html)

REACT_ELEMENT_TYPE 的话,会执行reconcileSingleElement方法

③ 如果是文本节点的话,会执行reconcileSingleTextNode方法

④ 如果执行到最后的deleteRemainingChildren话,说明待更新的节点是 null,需要删除原有旧节点的内容

可以看到ChildReconciler中的reconcileChildFibers方法的作用就是根据新节点newChild的节点类型,来执行不同的操作节点函数

下篇文章,会讲reconcileSingleElementreconcileSingleTextNodedeleteRemainingChildren

GitHub

ReactFiberBeginWork:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactFiberBeginWork.js

ReactFiberHooks:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactFiberHooks.js

ReactChildFiber:https://github.com/AttackXiaoJinJin/reactExplain/react16.8.6/packages/react-reconciler/src/ReactChildFiber.js

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、updateFunctionComponent
  • 二、renderWithHooks
  • 三、reconcileChildren
  • 四、ChildReconciler
  • 五、reconcileChildFibers
  • GitHub
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档