React Fiber构建completeWork源码解析

目录
  • 引言
  • 一. completeUnitOfWork
  • 二. completeWork
    • createInstance
    • createElement
    • appendAllChildren
  • 三. Effect
    • useEffect
    • resolveDispatcher
    • mountEffectImpl
    • pushEffect
  • 四. rootFiber-Effect

引言

之前我们介绍了beginWork,react使用的是深度优先遍历算法,整个fiber的构建都遵循此算法。

这也意味着,并不是所有节点beginWork完成后,才去进行completeWork。

当beginWork的next为null时,将进去completeWork。

一. completeUnitOfWork

function completeUnitOfWork(unitOfWork) {
  var completedWork = unitOfWork;
  do {
     // ...
     next = completeWork(current, completedWork, subtreeRenderLanes);
     // ...
     if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
       if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
              returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
            }
            returnFiber.lastEffect = completedWork.lastEffect;
        }
        var flags = completedWork.flags;
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          }else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
     }
     var siblingFiber = completedWork.sibling;
     if (siblingFiber !== null) {
        workInProgress = siblingFiber;
        return;
     }
     completedWork = returnFiber;
     workInProgress = completedWork;
  } while (completedWork !== null);
  // ...
}

根据深度优先算法,当beginWork完成其中某个子树干的最后一个节点时,进入completeUnitOfWork。根据最后的这个节点先完成completeWork,依次往上,直到找到相邻节点。

核心方法分为2部分,其一:completeWork,其二:完成effect挂载,并串联所有节点的effect(包括节点内部的effect)组装成环状链表。

二. completeWork

function completeWork(current, workInProgress, renderLanes) {
    var newProps = workInProgress.pendingProps;
    switch (workInProgress.tag) {
      case IndeterminateComponent:
      case LazyComponent:
      case SimpleMemoComponent:
      case FunctionComponent:
      case ForwardRef:
      case Fragment:
      case Mode:
      case Profiler:
      case ContextConsumer:
      case MemoComponent:
        return null;
      case ClassComponent:
        // ...
      case HostRoot:
       // ...
        updateHostContainer(workInProgress);
        return null;
      case HostComponent:
        // ...
         var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
         appendAllChildren(instance, workInProgress, false, false);
         workInProgress.stateNode = instance;
        // ...
     // ...
}

这里很神奇的是,在react内部updateHostContainer居然是个空函数,也许后续版本做do something吧。 普通节点将进入HostComponent。

createInstance

function createInstance(type, props, rootContainerInstance, hostContext, internalInstanceHandle) {
    var parentNamespace;
    {
      // TODO: take namespace into account when validating.
      var hostContextDev = hostContext;
      validateDOMNesting(type, null, hostContextDev.ancestorInfo);
      if (typeof props.children === 'string' || typeof props.children === 'number') {
        var string = '' + props.children;
        var ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type);
        validateDOMNesting(null, string, ownAncestorInfo);
      }
      parentNamespace = hostContextDev.namespace;
    }
    var domElement = createElement(type, props, rootContainerInstance, parentNamespace);
    precacheFiberNode(internalInstanceHandle, domElement);
    updateFiberProps(domElement, props);
    return domElement;
  }

createElement

function createElement(type, props, rootContainerElement, parentNamespace) {
  // script标签处理...
  // ...
  // flow相关webComponents处理
  // ...
  domElement = ownerDocument.createElement(type);
  // select标签特殊处理,单独设置multiple以及size
  // 非法标签告警处理...
  return domElement;
}

到这里,我们可以看到开始创建每个fiber节点对应的dom对象了。但是并没有插入到文档流中。 那么真实的dom是如何连接到fiber对象呢?

precacheFiberNode,会在每个真实dom对象下,挂载对应的节点的fiber,precache是以_reactFiber$+随机数的属性。

updateFiberProps,会在每个真实dom对象下,挂载对应的props children即element对象,以_reactProps$+随机数。

问题来了:每个fiber对应的真实dom对象,是单个构建单个存储?还是构建一个总的dom树?这之间是如何关联起来的?

appendAllChildren

appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
    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;
      }
}

这里已经很明显了,根据之前创建的fiber链表,循环node节点,普通节点将调用appendInitialChild。即使用:parentInstance.appendChild(child); 至此可以看到循环结束后,生成了整个待插入的DOM节点(页面首次渲染时)。

另外需要注意的是,根据fiber关联dom也是在这个阶段进行的(不是dom关联fiber)

最后如何存在ref,当前的fiber树对应的flags将和Ref的二进制数据取位运算或。(这很重要)

三. Effect

react推崇的是函数式编程,在一个函数组件里,如果存在useEffect等方法,那么react认为这是一个副作用函数组件。那么这些副作用是如何组织起来,又是在什么阶段运行的呢?

早在beginWork,fiber的flags默认都是二进制0,如果存在副作用,如:useEffect,ref,useLayoutEffect等等,首次将被设置为Placement。但为什么存在useEffect函数组件的fiber对象,flags都是256以上的数值?

我们以useEffect为例,一探究竟。

useEffect

function useEffect(create, deps) {
    var dispatcher = resolveDispatcher();
    return dispatcher.useEffect(create, deps);
}

没错,useEffect方法定义就两行代码。

resolveDispatcher

function resolveDispatcher() {
    var dispatcher = ReactCurrentDispatcher.current;
    if (!(dispatcher !== null)) {
      {
        throw Error( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem." );
      }
    }
    return dispatcher;
  }

我们继续看下ReactCurrentDispatcher的定义:

const ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: (null: null | Dispatcher),
};

最早的ReactCurrentDispatcher,是在renderRoot阶段。如果root或Lane被改变了,原来的dispatch可能被置空了或首次不存在,使用当前的ContextOnlyDispatcher替代。

在函数组件beginWork阶段,在执行函数组件生成element对象之前,会赋值HooksDispatcherOnMount,这就是dispatch。

我们来看看HooksDispatcher:

{
      readContext: function (context, observedBits) {
        return readContext(context, observedBits);
      },
      useCallback: function (callback, deps) {
        currentHookNameInDev = 'useCallback';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountCallback(callback, deps);
      },
      useContext: function (context, observedBits) {
        currentHookNameInDev = 'useContext';
        mountHookTypesDev();
        return readContext(context, observedBits);
      },
      useEffect: function (create, deps) {
        currentHookNameInDev = 'useEffect';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountEffect(create, deps);
      },
      useImperativeHandle: function (ref, create, deps) {
        currentHookNameInDev = 'useImperativeHandle';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountImperativeHandle(ref, create, deps);
      },
      useLayoutEffect: function (create, deps) {
        currentHookNameInDev = 'useLayoutEffect';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        return mountLayoutEffect(create, deps);
      },
      useMemo: function (create, deps) {
        currentHookNameInDev = 'useMemo';
        mountHookTypesDev();
        checkDepsAreArrayDev(deps);
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
        try {
          return mountMemo(create, deps);
        } finally {
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
      },
      useReducer: function (reducer, initialArg, init) {
        currentHookNameInDev = 'useReducer';
        mountHookTypesDev();
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
        try {
          return mountReducer(reducer, initialArg, init);
        } finally {
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
      },
      useRef: function (initialValue) {
        currentHookNameInDev = 'useRef';
        mountHookTypesDev();
        return mountRef(initialValue);
      },
      useState: function (initialState) {
        currentHookNameInDev = 'useState';
        mountHookTypesDev();
        var prevDispatcher = ReactCurrentDispatcher$1.current;
        ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
        try {
          return mountState(initialState);
        } finally {
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
      },
      useDebugValue: function (value, formatterFn) {
        currentHookNameInDev = 'useDebugValue';
        mountHookTypesDev();
        return mountDebugValue();
      },
      useDeferredValue: function (value) {
        currentHookNameInDev = 'useDeferredValue';
        mountHookTypesDev();
        return mountDeferredValue(value);
      },
      useTransition: function () {
        currentHookNameInDev = 'useTransition';
        mountHookTypesDev();
        return mountTransition();
      },
      useMutableSource: function (source, getSnapshot, subscribe) {
        currentHookNameInDev = 'useMutableSource';
        mountHookTypesDev();
        return mountMutableSource(source, getSnapshot, subscribe);
      },
      useOpaqueIdentifier: function () {
        currentHookNameInDev = 'useOpaqueIdentifier';
        mountHookTypesDev();
        return mountOpaqueIdentifier();
      },
      unstable_isNewReconciler: enableNewReconciler
    };

其中useEffect重点执行mountEffect(create, deps)

function mountEffect(create, deps) {
    {
      // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
      if ('undefined' !== typeof jest) {
        warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
      }
    }
    return mountEffectImpl(Update | Passive, Passive$1, create, deps);
  }

mountEffectImpl

function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  var hook = mountWorkInProgressHook();
    var nextDeps = deps === undefined ? null : deps;
    currentlyRenderingFiber$1.flags |= fiberFlags;
    hook.memoizedState = pushEffect(HasEffect | hookFlags, create, undefined, nextDeps);
}

hook对象数据结构如下:

{
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null
};

问题来了:

  • currentlyRenderingFiber是什么?和workInProgressFiber有什么关系?
  • 多个函数组件和单个函数组件中多个Hook是如何关联起来的?
  • 整个hook和fiber怎么关联起来的?
  • 完整的hooks数据结构又是什么?

currentlyRenderingFiber是当前正在rendering阶段的fiber对象,早在renderHook初始化阶段赋值了workInProgressFiber。所以当前函数组件的flags在这里被改变了,即有副作用的函数flags = flags | Update | Passive。

根据二进制位运算,根函数组件库flags = 518,当然这个数值也不是固定不变的,因为变化的beginWork阶段初始flags值。是根据不同的effects会有不同的初始值。

pushEffect

function pushEffect(tag, create, destroy, deps) {
    var effect = {
      tag: tag,
      create: create,
      destroy: destroy,
      deps: deps,
      // Circular
      next: null
    };
    var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
    if (componentUpdateQueue === null) {
      componentUpdateQueue = createFunctionComponentUpdateQueue();
      currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var lastEffect = componentUpdateQueue.lastEffect;
      if (lastEffect === null) {
        componentUpdateQueue.lastEffect = effect.next = effect;
      } else {
        var firstEffect = lastEffect.next;
        lastEffect.next = effect;
        effect.next = firstEffect;
        componentUpdateQueue.lastEffect = effect;
      }
    }
    return effect;
  }

对于tag的值,是HasEffect和Passive按位运算或的结果,实际上固定是5。

需要注意的是,函数组件的updateQueue和rootFiber的不一样,以及普通节点的数据结构和作用也都不一样。 函数组件的updateQueue关联的是effect。这和render初始化阶段rootFiber有巨大的差异。

上面的代码很简单,每个函数组件如果存在多个effect,那么会将这些effect顺序关联起来,这个函数组件的fiberr.updateQueue对应lastEffect,next即下一个effect,直到最后形成一个首尾相连的环状链表结构。

为什么是环状?这个待到后续调度阶段再解释。

再思考一个问题:这里只是解决了单个组件内的effect构建,那么整个fiber链表里effect构建是怎么样的?执行的顺序又是什么?

四. rootFiber-Effect

在completedWork的最后,根据深度优先遍历算法,将每个节点的firstEffect层层往上传递,一直到rootFiber。而lastEffect也是层层往上判断,直到上层最后一个effect,做为rootFiber的lastEffect。

每个fiber effect通过nextEffect链接起来,而fiber内部通过updateQueue链接自身的effect环状链表。

至此,completeWork阶段就完成了,rootFiber以及各fiber节点大部分属性都构建完成了。

下一章,将进入commit阶段,更多关于React Fiber构建completeWork的资料请关注我们其它相关文章!

以上就是React Fiber构建completeWork源码解析的详细内容,更多关于React Fiber构建completeWork的资料请关注我们其它相关文章!

(0)

相关推荐

  • React超详细讲述Fiber的使用

    目录 Fiber 概念 结构 Fiber树的遍历是这样发生的深度遍历 window.requestIdleCallback() requestAnimationFrame Fiber是如何工作的 结论 Fiber 概念 JavaScript引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待 如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿 破解JavaScrip

  • React Fiber树的构建和替换过程讲解

    目录 前言 mount 过程 update 过程 前言 React Fiber树的创建和替换过程运用了双缓存技术,即先在内存中创建 fiber 树,待 fiber 树创建完成以后,直接将旧的 fiber 树替换成新的 fiber 树,这样做的好处是省去了直接在页面上渲染时的计算时间,避免计算量大导致的白屏.卡顿,现在你一定还不太理解,下面进行详细讲解! mount 过程 以一下demo为例进行讲解: function App() { const [num, add] = useState(0);

  • React Fiber构建beginWork源码解析

    目录 引言 一. scheduleUpdateOnFiber 二. performSyncWorkOnRoot renderRootSync workLoopSync performUnitOfWork 三. beginWork updateHostRoot reconcileChildren reconcileChildFibers reconcileSingleElement createFiberFromElement mountIndeterminateComponent renderW

  • React Fiber 树思想解决业务实际场景详解

    目录 熟悉 Fiber 树结构 业务场景 熟悉 Fiber 树结构 我们知道,React 从 V16 版本开始采用 Fiber 树架构来实现渲染和更新机制. Fiber 在 React 源码中可以看作是一个任务执行单元,每个 React Element 都会有一个与之对应的 Fiber 节点. Fiber 节点的核心数据结构如下: type Fiber = { type: any, //类型 return: Fiber, //父节点 child: Fiber, // 指向第一个子节点 sibli

  • React Fiber与调和深入分析

    目录 一 引沿 二 什么是调和 三 什么是Filber 四 实现调和的过程 1. 创建FiberRoot 2. render阶段 五 总结 一 引沿 Fiber 架构是React16中引入的新概念,目的就是解决大型 React 应用卡顿,React在遍历更新每一个节点的时候都不是用的真实DOM,都是采用虚拟DOM,所以可以理解成fiber就是React的虚拟DOM,更新Fiber的过程叫做调和,每一个fiber都可以作为一个执行单元来处理,所以每一个 fiber 可以根据自身的过期时间expir

  • React Fiber原理深入分析

    目录 为什么需要 fiber fiber 之前 fiber 之后 fiber 节点结构 dom 相关属性 tag key 和 type stateNode 链表树相关属性 副作用相关属性 flags Effect List 其他 lane alternate fiber 树的构建与更新 mount 过程 update 过程 总结 react16 版本之后引入了 fiber,整个架构层面的 调度.协调.diff 算法以及渲染等都与 fiber 密切相关.所以为了更好地讲解后面的内容,需要对 fib

  • React Fiber构建completeWork源码解析

    目录 引言 一. completeUnitOfWork 二. completeWork createInstance createElement appendAllChildren 三. Effect useEffect resolveDispatcher mountEffectImpl pushEffect 四. rootFiber-Effect 引言 之前我们介绍了beginWork,react使用的是深度优先遍历算法,整个fiber的构建都遵循此算法. 这也意味着,并不是所有节点begin

  • react diff算法源码解析

    React中Diff算法又称为调和算法,对应函数名为reconcileChildren,它的主要作用是标记更新过程中那些元素发生了变化,这些变化包括新增.移动.删除.过程发生在beginWork阶段,只有非初次渲染才会Diff. 以前看过一些文章将Diff算法表述为两颗Fiber树的比较,这是不正确的,实际的Diff过程是一组现有的Fiber节点和新的由JSX生成的ReactElement的比较,然后生成新的Fiber节点的过程,这个过程中也会尝试复用现有Fiber节点. 节点Diff又分为两种

  • React事件机制源码解析

    React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的. 本文浅析的React版本为17.0.1,使用ReactDOM.render创建应用,不含优先级相关. 原理简述 React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身. 同时,React将事件分为3种类型--d

  • React实时预览react-live源码解析

    目录 引言 源码解读 输入内容 Provider generateElement 其他组件 总结 引言 react-live 是一个 react 的实时编辑器,可直接编辑 react 代码,并实时预览.可以看下官方的预览图: 本文针对的源码版本 src ├── components │ ├── Editor │ │ └── index.js │ └── Live │ ├── LiveContext.js │ ├── LiveEditor.js │ ├── LiveError.js │ ├── L

  • 微前端架构ModuleFederationPlugin源码解析

    目录 序言 背景 MF 基本介绍 应用场景 微前端架构 服务化的 library 和 components ModuleFederationPlugin 源码解析 入口源码 Exposes Remotes Shared 小结 总结 序言 本文是 Webpack ModuleFederationPlugin(后面简称 MF) 源码解析 文章中的第一篇,在此系列文章中,我将带领大家抽丝剥茧.一步步地去解析 MF 源码.当然为了帮助大家理解,可能中间也会涉及到 Webpack 源码中的其它实现,我会根

  • Redis源码解析:集群手动故障转移、从节点迁移详解

    一:手动故障转移 Redis集群支持手动故障转移.也就是向从节点发送"CLUSTER  FAILOVER"命令,使其在主节点未下线的情况下,发起故障转移流程,升级为新的主节点,而原来的主节点降级为从节点. 为了不丢失数据,向从节点发送"CLUSTER  FAILOVER"命令后,流程如下: a:从节点收到命令后,向主节点发送CLUSTERMSG_TYPE_MFSTART包:          b:主节点收到该包后,会将其所有客户端置于阻塞状态,也就是在10s的时间内

  • java TreeMap源码解析详解

    java TreeMap源码解析详解 在介绍TreeMap之前,我们来了解一种数据结构:排序二叉树.相信学过数据结构的同学知道,这种结构的数据存储形式在查找的时候效率非常高. 如图所示,这种数据结构是以二叉树为基础的,所有的左孩子的value值都是小于根结点的value值的,所有右孩子的value值都是大于根结点的.这样做的好处在于:如果需要按照键值查找数据元素,只要比较当前结点的value值即可(小于当前结点value值的,往左走,否则往右走),这种方式,每次可以减少一半的操作,所以效率比较高

  • .Net Core中ObjectPool的使用与源码解析

    一.对象池 运用对象池化技术可以显著地提升性能,尤其是当对象的初始化过程代价较大或者频率较高.下面是ObjectPool源码中涉及的几个类.当你看过.Net Core源码很多时,你会发现,微软的开发很多都是这种模式,通过Policy构建Provider,通过Provider创建最终的类. 二.使用 这个组件的目的主要是将对象保存到对象池,用的时候直接去取,不需要重新创建,实现对象的重复利用.但是有个问题,假如对象池中开始没有对象或者取得数量大于对象池中的数量怎么办?在对象池中对象的数量不足时,此

  • .NET Core源码解析配置文件及依赖注入

    写在前面 上篇文章我给大家讲解了ASP.NET Core的概念及为什么使用它,接着带着你一步一步的配置了.NET Core的开发环境并创建了一个ASP.NET Core的mvc项目,同时又通过一个实战教你如何在页面显示一个Content的列表.不知道你有没有跟着敲下代码,千万不要做眼高手低的人哦. 这篇文章我们就会设计一些复杂的概念了,因为要对ASP.NET Core的启动及运行原理.配置文件的加载过程进行分析,依赖注入,控制反转等概念的讲解等. 俗话说,授人以鱼不如授人以渔,所以文章旨在带着大

随机推荐