从源码中探究React中的虚拟DOM

news2025/7/8 11:10:07

引文

通过本文你将了解到

  • 什么是虚拟DOM?
  • 虚拟DOM有什么优势?
  • React的虚拟Dom是如何实现的?
  • React是如何将虚拟Dom转变为真实Dom?

一、概念

虚拟DOM实际上是一种用来模拟DOM结构的javascript对象。当页面变化的时候通过一种算法来对比前后的虚拟DOM结构,找到需要更改的地方,然后再操作真实DOM,就可以更新页面。

二、虚拟DOM有哪些优势

2.1 浏览器兼容性最佳

React中的虚拟DOM具有强大的兼容性。我们现在知道,在React编译过程中,Reactbabel将JSX转为JS对象也就是虚拟DOM,之后通过render函数生成真实DOM。在编译虚拟DOM的过程中React还做了许多处理,比如diff对比和兼容处理就是在这个阶段。

JQuery中操作DOM十分便捷,比如原生的input就有onchange事件,但这都是在浏览器底层对DOM的处理,不同的DOM只能触发关联给他的一些事件,比如div标签本身没有onchange事件,但是虚拟Dom就不同了,虚拟Dom一方面模仿了原生Dom的行为,其次在事件方面也做了合成事件与原生事件的映射关系,比如:

{
  onClick: ['click'],
  onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange']
}

React暴露给我们的合成事件,其实在底层会关联到多个原生事件,通过这种做法抹平了不同浏览器之间的api差异,也带来了更强大的事件系统。

2.2 渲染机制的优化

我们知道React是state数据驱动视图,只要state发生了改变,那么render就会重新触发,以达到更新UI层的效果。

但是在某些处理逻辑中,某个state的值会多次发生变化,比如在某个事件中短时间内多次setState,这时React会进行多次渲染吗?

答案是不会。
在 React 中,state 的更新是异步的,React 会在更新页面之前进行优化,因此可能会把多次 setState() 调用合并为一次更新,以提高性能。

当然如果我们是直接操作Dom,那还有哪门子的异步和渲染等待,当你append完一个子节点,页面早渲染完了。所以虚拟Dom的对比提前,以及setState的异步处理,本质上也是为了尽可能少的操作DOM。

2.3 跨平台能力

因为 React 只是在 JavaScript 层面上操作虚拟 DOM,所以可以在不同平台上使用相同的代码来渲染用户界面。

之所以加入虚拟Dom这个中间层,除了解决部分性能问题,加强兼容性之外,还有个目的是将Dom的更新抽离成一个公共层,别忘了React除了做页面引用外,React还支持使用React Native创建端 App。

三、React中虚拟DOM的实现原理

下面以React源码为准,看一下React的底层如何实现虚拟DOM。

React中创建虚拟DOM的方法React.createElement,下面这段代码摘除了dev环境的报错逻辑。

/**
 * 创建并返回给定类型的新ReactElement。
 * See https://reactjs.org/docs/react-api.html#createelement
 */
function createElement(type, config, children) {
  let propName;

  // 创建一个全新的props对象
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  // 有传递自定义属性进来吗?有的话就尝试获取ref与key
  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    // 保存self和source
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;

    // 剩下的属性都添加到一个新的props属性中。注意是config自身的属性
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // 处理子元素,默认参数第二个之后都是子元素
  const childrenLength = arguments.length - 2;
  // 如果子元素只有一个,直接赋值
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    // 如果是多个,转成数组再赋予给props
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 处理默认props,不一定有,有才会遍历赋值
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      // 默认值只处理值不是undefined的属性
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 调用真正的React元素创建方法
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}

代码看似很多,其实逻辑非常清晰:

  1. 处理参数,对传进来的数据进行加工处理,比如提取config参数,处理props等
  2. 通过ReactElement构造函数返回ReactNode对象

数据加工部分分为三步:

  • 第一步,判断config有没有传,不为null就做处理
    • 判断ref、key,__self、__source这些是否存在或者有效,满足条件就分别赋值给前面新建的变量。
    • 遍历config,并将config自身的属性依次赋值给前面新建props。
  • 第二步,处理子元素。默认从第三个参数开始都是子元素。
    • 如果子元素只有一个,直接赋值给props.children。
    • 如果子元素有多个,转成数组后再赋值给props.children。
  • 第三步,处理默认属性defaultProps
    • 一个纯粹的标签也可以理解成一个最最最基础的组件,而组件支持 defaultProps,所以这一步判断有没有defaultProps,如果有同样遍历,并将值不为undefined的部分都拷贝到props对象上。

逻辑抽离出来,看起来并不难~

我们在看一下ReactElement,同样是删除了dev环境逻辑:

const ReactElement = function (type, key, ref, self, source, owner, props) {
  const element = {
    // 这个标签允许我们将其标识为唯一的React Element
    $$typeof: REACT_ELEMENT_TYPE,
    // 元素的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录负责创建此元素的组件。
    _owner: owner,
  };
  return element;
};

这里主要是将前面的一些数据,生成一个element对象,也就是虚拟DOM。

这里提一下REACT_ELEMENT_TYPE,他的实现是:

export const REACT_ELEMENT_TYPE = Symbol.for('react.element');

如果你自己实现了上面打印虚拟DOM的场景,或许你有点印象。

在这里
在这里插入图片描述
$$typeof定义为Symbol(react.element),而Symbol一大特性就是标识唯一性,即便两个看着一模一样的Symbol,它们也不会相等。而React之所以这样做,本质也是为了防止xss攻击,防止外部伪造虚拟Dom结构。

至此,我们又了解到了React底层是如何实现虚拟DOM,这里留下一个问题,我看到别人的文章中有提到,但是我自己还没去复现。

React中虚拟Dom的是否允许修改,或者添加新的属性?

四、React中的虚拟DOM如何转变为真实DOM?

一般来说,使用React编写应用,ReactDOM.render是我们触发的第一个函数。那么我们先从ReactDOM.render这个入口函数开始分析render的整个流程

下面贴出来的源码也会把dev环境下的代码删掉,我们只关注具体的逻辑

虚拟DOM会通过ReactDOM.render进行渲染成真实DOM

class P extends React.Component {
  render() {
    return (
        <span className="span">
          <span>hello World!</span>
        </span>
    );
  }
}

4.1 React render

export function render(element: React$Element<any>,container: Container,callback: ?Function) {

// 校验container是否合法
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  
  // 调用 legacyRenderSubtreeIntoContainer 方法
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback,
    );
}

可以看出 render方法实际上只对container进行节点类型的校验,如果不是一个合法的Dom节点就会抛出错误,我们只需要关注核心逻辑legacyRenderSubtreeIntoContainer()

4.2 legacyRenderSubtreeIntoContainer

legacyRenderSubtreeIntoContainer函数名的意思就是“组件子树继承渲染到容器中”,其实就是把虚拟的Dom树渲染到真实的Dom容器中

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {
// root:FiberRootNode 是整个应用的根结点
// 绑定在真实DOM节点的_reactRootContainer属性上
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot; 
// 判断 根节点是否存在
  if (!root) {
  //如果不存在,则说明是Initial mount 阶段,调用函数生成rootNode
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    // 取出root内部的_internalRoot属性
    fiberRoot = root._internalRoot;
    
    if (typeof callback === 'function') {
      const originalCallback = callback;
      // 封装 callback 回调
      callback = function() {
        //通过fiberRoot找到当前对应的rootFiber
        //将rootFiber.child.stateNode作为callback中的this指向
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    } 
    // 初始化不允许批量处理,使用 unbatchedUpdates 调用 updateContainer()同步生成
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // batchedUpdates 调用 updateContainer();
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

继续看一下getPublicRootInstance函数

export function getPublicRootInstance(
  container: OpaqueRoot,
): React$Component<any, any> | PublicInstance | null {
  // 取出当前Fiber节点,通过一些判断寻找FiberRoot
  const containerFiber = container.current;
  // 是否存在子Fiber节点
  if (!containerFiber.child) {
    return null;
  }
  // 判断子Fiber节点tag类型
  switch (containerFiber.child.tag) {
  	// 这里 HostComponent =  5,
    case HostComponent:
      return getPublicInstance(containerFiber.child.stateNode);
    default:
      return containerFiber.child.stateNode;
  }
}

再梳理一下这个过程:

  1. root = container._reactRootContainer:如不存在,表示是 Initial mount 阶段,调用 legacyCreateRootFromDOMContainer 生成;如存在,表示是 update 阶段;
  2. fiberRoot = root._internalRoot:从 root 上拿到内部 _internalRoot 属性;
  3. 封装 callback 回调(通过 fiberRoot 找到其对应的 rootFiber,然后将 rootFiber.child.stateNode 作为 callback 中的 this 指向,调用 callback);
  4. mount 阶段,需要尽快完成,不允许批量更新(使用的是legacy渲染模式),使用 unbatchedUpdates 调用 updateContainer();
    update 阶段,直接调用 updateContainer() 执行更新;
  5. 返回getPublicRootInstance(fiberRoot):返回公开的 Root 实例对象。

上面看似新建、更新两种情况都用的一个函数,React故意将他们命名为一样,其实new的时候还是走的new生成逻辑,update走的update逻辑。

就像这里

export const updateContainer = enableNewReconciler
  ? updateContainer_new
  : updateContainer_old;

FiberRoot和rootFiber的区别:

  • 首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。
  • fiberRootNode是整个应用的根节点,绑定在真实DOM节点的_reactRootContainer属性上
  • rootFiber是当前所在组件树的根节点,rootFiber在每次重新渲染的时候会重新构建。

4.3 updateContainer

这里必须关注一下updateContainer(),走到这里就进入了一个非常关键的处理

在这里插入图片描述

来看一下updateContainer的源码

function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  const current = container.current;
  // 获取更新任务的触发时间,可以理解为返回的时间越小,则执行的优先级越高
  const eventTime = requestEventTime();

  // lane 中文含义是车道, 在 React 中,lane 是一个number值,使用 32 个比特位表示32个“车道”。
  // 通过一系列处理确定任务真正的优先级,并申请相关的车道
  const lane = requestUpdateLane(current);
  if (enableSchedulingProfiler) {
    markRenderScheduled(lane);
  }
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  //返回一个包装好的任务 update ,存放一些数据 
  //  const update: Update<*> = {
  //   eventTime: number,
  //   lane: Lane,
  //   tag: 0 | 1 | 2 | 3,
  //   payload: any,
  //   callback: (() => mixed) | null,
  //   next: Update<State> | null,
  // };
  const update = createUpdate(eventTime, lane);
  update.payload = {element};

  callback = callback === undefined ? null : callback;

  // 将需要更新的任务对象关联进 Fiber 任务队列,形成环状链表
  enqueueUpdate(current, update);

  // 进入Fiber的协调、调度
  scheduleUpdateOnFiber(current, lane, eventTime);

  return lane;
}

这一部分React做了什么?

他为传进来的Fiber申请了lane,确定了优先级,生成一个更新的任务update类型的数据,将需要更新的任务关联进了Fiber的任务队列并且形成了环状链表,进入Fiber的协调调度函数scheduleUpdateOnFiber,安排Fiber节点挂载。

在后续scheduleUpdateOnFiber的处理中,会调用checkForNestedUpdates(),他处理任务更新的嵌套层数,如果嵌套层数过大( >50 ),就会认为是无效更新,则会抛出异常。

之后便根据markUpdateLaneFromFiberToRoot对当前的fiber树,自底向上的递归fiber的lane,根据lane做二进制比较或者位运算处理,ensureRootIsScheduled里确定调度更新的模式,有performSyncWorkOnRootperformConcurrentWorkOnRoot方法。不同的调用取决于本次更新是同步更新还是异步更新。

updateContainer 不管什么模式都会走 performSyncWorkOnRoot,这个函数的核心功能分别是:
这个函数,而这个函数三个核心处理过程分别是:

  1. beginWork
  2. completeUnitOfWork
  3. commitRoot

render 阶段会通过遍历的方式向下调和向上归并,从而创建一颗完整的Fiber Tree,调和的过程也就是 beginWork ,归并就是 completeWork 过程

4.4 performSyncWorkOnRoot 函数的三大核心处理

(1) beginWork

我们直接跳过一些逻辑看一个比较重要的方法,beginWork

这段代码太长,我们只看部分逻辑

function beginWork(  
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
  ) {
	// 删除部分无影响的代码
  workInProgress.lanes = NoLanes;

 //这里有一些判断FiberProps是否可以复用的逻辑,然后做一些处理
	
  switch (workInProgress.tag) {
    // 模糊定义的组件
    case IndeterminateComponent:
      {
        return mountIndeterminateComponent(current, workInProgress, workInProgress.type, 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);
      }
		// class组件
    case ClassComponent:
      {
        var _Component2 = workInProgress.type;
        var _unresolvedProps = workInProgress.pendingProps;

        var _resolvedProps = workInProgress.elementType === _Component2 ? _unresolvedProps : resolveDefaultProps(_Component2, _unresolvedProps);

        return updateClassComponent(current, workInProgress, _Component2, _resolvedProps, renderLanes);
      }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case ...:
      return ...
  }
}

这里beginWork做的很重要的一步,就是根据render传进来组件类型的不同来选择不同的组件更新的方法。

我们可以根据 current 是否为 null 来判断当前组件是处于 update 阶段还是 **mount **阶段
因此,beginWork 的工作其实可以分成两部分

  1. mount 时:会根据 Fiber.tag 的不同,执行不同类型的创建子 Fiber 节点的程序
  2. update 时:会根据一定的条件复用 current 节点,这样可以通过 clone current.child 来作为 workInProgress.child ,而不需要重新创建

比如我们最初定义了一个Class组件P,这里就会进入**updateClassComponent()**来更新组件

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  // 删除了添加context部分的逻辑
	// 获取组件实例
  var instance = workInProgress.stateNode;
  var shouldUpdate;
	// 如果没有实例,那就得创建实例
  if (instance === null) {
    if (current !== null) {
      current.alternate = null;
      workInProgress.alternate = null;

      workInProgress.flags |= Placement;
    }
    // 这里new Class创建组件实例
    constructClassInstance(workInProgress, Component, nextProps);
    // 挂载组件实例
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {
    shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes);
  } else {
    shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderLanes);
  }
  // Class组件的收尾工作
  var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes);
}

在看这段代码前,我们自己也可以提前想象下这个过程,比如Class组件你一定是得new才能得到一个实例,只有拿到实例后才能调用其render方法,拿到其虚拟Dom结构,之后再根据结构创建真实Dom,添加属性,最后加入到页面。

所以在updateClassComponent中,首先会对组件做context相关的处理,这部分代码我删掉了,其余,判断当前组件是否有实例,如果有就去更新实例,如果没有那就创建实例,所以我们聚焦到constructClassInstancemountClassInstancefinishClassComponent三个方法,看命名就能猜到,前者一定是创造实例,后者是应该是挂载实例前的一些处理,先看第一个方法:

function constructClassInstance(workInProgress, ctor, props) {
	// 删除了对组件context进一步加工的逻辑
	// ....
  
  // 这里创建了组件实例
  // 验证了前面的推测,这里new了我们的组件,并且传递了当前组件的props以及前面代码加工的context
  var instance = new ctor(props, context);
  var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;
  adoptClassInstance(workInProgress, instance);

  // 删除了对于组件生命周期钩子函数的处理,比如很多即将被废弃的钩子,在这里都会被添加 UNSAFE_ 前缀
  //.....
  return instance;
}

果然,这里通过new ctor(props, context)创建了组件实例。

下面看mountClassInstance()

function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
	// 此方法主要是对constructClassInstance创建的实例进行数据组装,为其赋予props,state等一系列属性
  var instance = workInProgress.stateNode;
  instance.props = newProps;
  instance.state = workInProgress.memoizedState;
  instance.refs = emptyRefsObject;
  initializeUpdateQueue(workInProgress);
  
  // 删除了部分特殊情况下,对于instance的特殊处理逻辑
}

虽然命名是挂载,但其实离真正的挂载还远得很,本方法其实是为constructClassInstance创建的组件实例做数据加工,为其赋予props state等一系列属性。

在上文代码中,其实还有个finishClassComponent方法,此方法在组件自身都准备完善后调用,我们期待已久的render方法处理就在里面:

function finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes) {
  var instance = workInProgress.stateNode;
  ReactCurrentOwner$1.current = workInProgress;
  var nextChildren;
  if (didCaptureError && typeof Component.getDerivedStateFromError !== 'function') {
			// ...
  } else {
    {
      setIsRendering(true);
      // 关注点在这,通过调用组件实例的render方法,得到内部的元素
      nextChildren = instance.render();

      setIsRendering(false);
    }
  } 
  if (current !== null && didCaptureError) {
    forceUnmountCurrentAndReconcile(current,workInProgress,nextChildren,renderLanes,
    );
  } else {
  //reconcileChildren 做的事情就是 react 的另一核心之一 —— diff 过程
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }
  workInProgress.memoizedState = instance.state;
  return workInProgress.child;
}

(2) completeWork

workInProgress 为 null 时,也就是当前任务的 fiber 树遍历完之后,就进入到了 completeUnitOfWork 函数。

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    // ... 
    // 对节点进行completeWork,生成DOM,更新props,绑定事件
    next = completeWork(current, completedWork, subtreeRenderLanes);

    if (
      returnFiber !== null &&
      (returnFiber.flags & Incomplete) === NoFlags
    ) {
      // 将当前节点的 effectList 并入到父节点的 effectList
      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;
      }

      // 将自身添加到 effectList 链,添加时跳过 NoWork 和 PerformedWork的 flags,因为真正的 commit 时用不到
      const flags = completedWork.flags;

      if (flags > PerformedWork) {
        if (returnFiber.lastEffect !== null) {
          returnFiber.lastEffect.nextEffect = completedWork;
        } else {
          returnFiber.firstEffect = completedWork;
        }
        returnFiber.lastEffect = completedWork;
      }
    }
  } while (completedWork !== null);

  // ...
}

经过了 beginWork 操作,workInProgress 节点已经被打上了 flags 副作用标签。completeUnitOfWork 方法中主要是逐层收集 effects 链,最终收集到root上,供接下来的 commit 阶段使用。

到这里,我们可以理解例子P组件虚拟Dom都准备完毕,现在要做的是对于虚拟Dom这种最基础的组件做转成真实Dom的操作,见如下代码:

function completeWork(current, workInProgress, renderLanes) {
  var newProps = workInProgress.pendingProps;
	// 根据tag类型做不同的处理
  switch (workInProgress.tag) {
    // 标签类的基础组件走这条路
    case HostComponent:
      {
        popHostContext(workInProgress);
        var rootContainerInstance = getRootHostContainer();
        var type = workInProgress.type;

        if (current !== null && workInProgress.stateNode != null) {
          // ...
        } else {
          // ...
          } else {
            // 关注点1:创建虚拟Dom的实例
            var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
            appendAllChildren(instance, workInProgress, false, false);
            workInProgress.stateNode = instance; // Certain renderers require commit-time effects for initial mount.
            // 关注点2:初始化实例的子元素
            if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
              markUpdate(workInProgress);
            }
          }
        }
      }
  }
}

createInstance调用createElement方法创建真正的DOM实例,React会根据你的标签类型来决定怎么创建Dom。比如我们的span很显然就是通过**ownerDocument.createElement(type)**创建,如下图:

在这里插入图片描述

创建完成后,此时的span节点还是一个啥都没有的空span,所以后续来到finalizeInitialChildren方法,这里开始对创建的span节点的children子元素进一步加工,再通过里面的一些函数做一些对节点的加工处理,比如设置节点的标签样式等等。

那么到这里,其实我们的组件P已经准备完毕,包括真实Dom也都创建好了,就等插入到页面了,那这些Dom什么时候插入到页面的呢?

(3) commit

completeUnitOfWork结束后,render 阶段便结束了,后面就到了commit阶段。

其实到这里可以算是render阶段的完成,这里在内存中构建 workInProgress Fiber 树的所有工作都已经完成,这其中包括了对 Fiber 节点的 updatediffflags 标记、subtreeFlags(effectList) 的收集等一系列操作,在 completeWork阶段形成了 effectList 链表,连接所有需要被更新的节点。

下面,为了将这些需要更新的节点应用到真实 DOM 上却不需要遍历整棵树,在 commit 阶段,会通过遍历这条 EffectList 链表,执行对应的操作,来完成对真实 DOM 的更新。

这个阶段我们直接看他是怎么把真实DOM节点插入到容器中的,直接定位到insertOrAppendPlacementNodeIntoContainer方法,直译过来就是将节点插入或者追加到容器节点中:

function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
  var tag = node.tag;
  var isHost = tag === HostComponent || tag === HostText;
  if (isHost || enableFundamentalAPI ) {
    var stateNode = isHost ? node.stateNode : node.stateNode.instance;
    if (before) {
      // 在容器节点前插入
      insertInContainerBefore(parent, stateNode, before);
    } else {
      // 在容器节点后追加
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal) ; else {
    var child = node.child;
		// 只要子节点不为null,继续递归调用
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      var sibling = child.sibling;
			// 只要兄弟节点不为null,继续递归调用
      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

这里React主要做了两件事情:

  1. 如果是原生 DOM 节点,调用 insertInContainerBeforeappendChildToContainer 来在相应的位置插入 DOM 节点
  2. 如果不是原生 DOM 节点,会对当前 Fiber 节点的所有子 Fiber 节点调用 insertOrAppendPlacementNodeIntoContainer对自身进行遍历,直到找到 DOM 节点,然后插入

我们再看一看appendChildToContainer的实现:

function appendChildToContainer(container, child) {
  var parentNode;

  if (container.nodeType === COMMENT_NODE) {
    parentNode = container.parentNode;
    parentNode.insertBefore(child, container);
  } else {
    parentNode = container;
    // 将子节点插入到父节点中
    parentNode.appendChild(child);
  var reactRootContainer = container._reactRootContainer;

  if ((reactRootContainer === null || reactRootContainer === undefined) && parentNode.onclick === null) {
    // TODO: This cast may not be sound for SVG, MathML or custom elements.
    trapClickOnNonInteractiveElement(parentNode);
  }
}

结合我们前面自己写的例子

class P extends React.Component {
  render() {
    return (
        <span className="span">
          <span>hello World!</span>
        </span>
    );
  }
}

由于我们定义的组件非常简单,P组件只有一个span标签,所以这里的parentNode其实就是容器根节点,当执行完parentNode.appendChild(child) ,可以看到页面就出现了“hello World!”了。
在这里插入图片描述

6.5 小结

至此,组件的虚拟Dom生成,真实Dom的创建,加工以及渲染全部执行完毕。

上面有多次提到fiber节点,其实我们在创建完真实Dom后,它还是会被加工成一个fiber节点,而此节点中通过child可以访问到自己的子节点,通过sibling获取自己的兄弟节点,最后通过return属性获取自己的父节点,通过这些属性为构建Dom树提供了支撑,至于fiber是如何实现的,这里就不多做说明啦~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/360965.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

美国拟发布纽扣电池或硬币电池安全标准和通知要求ANSI C18. 3M

2023年2月10日&#xff0c;美国向WTO提交G/TBT/N/USA/1964号通报&#xff0c;拟发布纽扣电池或硬币电池以及含有此类电池的消费品的安全标准和通知要求&#xff0c;征求意见截止日期为2023年3月13日&#xff0c;拟通过日期和生效日期待定。联[1]系 拟定规则通知根据H.R.5313瑞…

微服务保护之sentinel熔断器

文章目录 目录 文章目录 前言 一、解决微服务雪崩的问题 二、使用步骤 三、熔断器的使用 3.1 限流规则 3.1.1流控模式 3.1.2流控效果 3.2 隔离和降级 3.2.1 隔离 3.2.2 降级 四、sentinel规则持久化 总结 前言 在基于 SpringCloud 构建的微服务体系中&#xff0c;服务间的调用…

宝塔搭建实战人才求职管理系统mobile手机端vue源码(五)

大家好啊&#xff0c;我是测评君&#xff0c;欢迎来到web测评。 上一期给大家分享骑士cms会员管理member前端vue在本地运行打包、宝塔发布部署的方式&#xff0c;本期给大家分享&#xff0c;mobile移动端vue怎么在本地运行&#xff0c;打包&#xff0c;实现线上功能更新替换的方…

南卡和wiwu电容笔哪款更值得入手?开学季电容笔推荐

在如今科技飞速发展的时代&#xff0c;数码产品层出不穷&#xff0c;尤其是电容笔。最近一段时间&#xff0c;电容笔非常受欢迎&#xff0c;很多人们都会使用电容笔来搭配平板电脑&#xff0c;不管在学习上或者工作上都随处可见。而随着电容笔的种类越来越多&#xff0c;人们对…

墨菲安全参与信息通信软件供应链安全社区成员大会并获自主研发创新成果奖

2023年2月16日&#xff0c;首届ICT软件供应链安全治理论坛暨信息通信软件供应链安全社区第二届成员大会在北京成功举办&#xff0c;多位业界顶级专家与工业和信息化部网络安全管理局相关领导出席&#xff0c;为现场观众分享了关于软件供应链可持续性与安全治理行业的前瞻与思考…

ICLR 2023|VLDet:从图像-文本对中学习区域-词语对齐的开放词汇式目标检测

原文链接&#xff1a;https://www.techbeat.net/article-info?id4614&isPreview1 作者&#xff1a;林闯 目标检测任务在AI工业界具有非常广泛的应用&#xff0c;但由于数据获取和标注的昂贵&#xff0c;检测的目标一直被限制在预先设定好的有限类别上。而在学术界&#xf…

2023最新软件测试面试题(带答案)

1. 请自我介绍一下(需简单清楚的表述自已的基本情况&#xff0c;在这过程中要展现出自信&#xff0c;对工作有激情&#xff0c;上进&#xff0c;好学) 面试官您好&#xff0c;我叫###&#xff0c;今年26岁&#xff0c;来自江西九江&#xff0c;就读专业是电子商务&#xff0c;毕…

OpenGL学习日记之模型绘制

自己编译运行过程中遇到的一些问题 下载Assimp已编译的lib(因为我们公司的电脑有很多权限和限制&#xff0c;也不能自己安装一些没有报备的软件&#xff0c;所以愁方便我就没有用cMake自己编译了)找到一位免费分享的博主的。 https://blog.csdn.net/lady_killer9/article/deta…

聊聊8万8的私董会,很扎心

聊聊8万8的私董会&#xff0c;很扎心 道几句真心话&#xff0c;很扎心&#xff0c;但也很现实。 如果你喜欢刷抖音&#xff0c;这种感觉应该会更加明显。 股市哀鸿遍野&#xff0c;实体一地鸡毛&#xff0c;我们办公室楼下的门面换了一波又一波。 别说那些不起眼的小生意&a…

第48章 抽离Axios拦截守及其全局变量定义

1 准备工作 1.1 重构src\store\index.js import { createStore } from vuex; export default createStore({ state: { //通过该全局变量&#xff0c;获取全局化存储的1个指定用户的令牌字符串。 token: localStorage.getItem(Token) ? localStorage.getItem(Token) : , //通…

CHAPTER 3 Web Server - httpd配置(二)

Web Server - httpd配置二3.1 httpd配置3.1.1 基于用户的访问控制3.1.2 basic认证配置示例&#xff1a;1. 添加用户2. 添加网页文件3. 定义安全域4. 修改父目录权限5. 访问效果6. 在配置文件中定义一个".htaccess"隐藏文件7. 添加组3.1.3 虚拟主机1. 构建方案2. 基于…

Storage

WebStorage主要提供了一种机制&#xff0c;可以让浏览器提供一种比cookie更直观的key、value存储方式&#xff1a; localStorage&#xff1a;本地存储&#xff0c;提供的是一种永久性的存储方法&#xff0c;在关闭掉网页重新打开时&#xff0c;存储的内容依然保留&#xff1b;…

TCP/IP网络协议族分成及其每层作用

1、可以分为应用层、传输层、网络层、链路层 2、各层的作用 应用层(可以想象成是快递打包过程) 决定了向用户提供应用服务时通信的活动&#xff0c;将要进行的操作或者数据进行一个打包。 传输层&#xff08;可以理解为选择顺丰、圆通等快递公司&#xff09; 提供数据传输的方…

从混沌到清晰,阿里全球商品类目域建设思考

作者&#xff1a;丁浩然 阿里全球化业务平台团队 商品是电商产品体系核心之一&#xff0c;类目则是商品模型核心之一&#xff0c;类目系统提供的基础业务数据贯穿了整个电商体系。本文将为大家分享商品类目域在全球化过程中的建设与思考。 众所周知&#xff0c;商品是电商产品体…

vue后台管理系统项目-table选择多行数据分页列表、一键全选重置功能

table选择多行数据 功能介绍&#xff1a; 1.列表分页功能&#xff1b; 2.一键全选&#xff0c;选中列表所有数据&#xff1b; 3.全选&#xff0c;选中当前页数据&#xff1b; 4.重置&#xff0c;清除选中状态&#xff1b; 5.列表搜索查询&#xff1b; 效果&#xff1a; 1.列表分…

剑指 Offer 28. 对称的二叉树

剑指 Offer 28. 对称的二叉树 难度&#xff1a;easy\color{Green}{easy}easy 题目描述 请实现一个函数&#xff0c;用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样&#xff0c;那么它是对称的。 例如&#xff0c;二叉树 [1,2,2,3,4,4,3] 是对称的。 但是下…

项目管理中,如何制定一个好的项目计划?

项目计划&#xff0c;是一个项目的起点。计划不清晰&#xff0c;执行力再强也只会让项目跑偏。 制定一个好的项目计划有哪些要点&#xff1a; 1、确定目标 项目目标是项目所要达到的期望结果&#xff0c;拥有明确的目标能够帮助我们做好规划&#xff0c;用有效的方式做正确…

一文带你看透前端世界里的日期时间,对就是Date

很高兴我们能够通过不同空间&#xff0c;不同时间&#xff0c;通过这篇博客相识&#xff0c;那一定是一种缘分&#xff0c;一种你和狗哥的缘分。今天我希望通过这篇博客对我所熟知的前端世界里的日期时间做一个汇总&#xff0c;不止是代码上的汇总哦&#xff01; 目录 一、时区…

​一致魔芋在北交所上市:市值突破11亿元,吴平夫妇为实控人​

2月21日&#xff0c;湖北一致魔芋生物科技股份有限公司&#xff08;下称“一致魔芋”&#xff0c;BJ:839273&#xff09;在北京证券交易所上市。本次上市&#xff0c;一致魔芋的发行价为11.38元/股&#xff0c;发行1350万股&#xff0c;募资总额约为1.54亿元。 本次发行后&…

【语音之家】AI产业沙龙—动手体验语音AI开发利器 - NVIDIA NeMo代码实战

由CCF语音对话与听觉专委会 、中国人工智能产业发展联盟&#xff08;AIIA&#xff09;评估组、NVIDIA、语音之家、希尔贝壳共同主办的【语音之家】AI产业沙龙—动手体验语音AI开发利器 - NVIDIA NeMo代码实战&#xff0c;将于2023年2月28日19:00-20:30线上直播&#xff0c;同时…