当前位置: 首页 > news >正文

React 虚拟DOM的前世今生

引文

通过本文你将了解到

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

一、前言

要了解虚拟DOM,我们先明确一下DOM的概念。

根据MDN的说法:

文档对象模型 (DOM) 是 HTML 和 XML 文档的编程接口。
它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。
简言之,它会将 web 页面和脚本或程序语言连接起来。

重点:

  • HTML和DOM是有映射关系的,DOM是对HTML的结构化描述
  • 通过DOM可以对文档进行访问和操作

我们可以简历这样一个对应关系:

JS通过浏览器暴露出来的接口操作DOM的修改,DOM修改之后浏览器根据新的DOM结构重新渲染出HTML界面。

看到这里可能会产生疑问,我们直接使用JS通过Web API操作DOM不就可以了吗?为什么还要使用虚拟DOM?

答案就是为了优化性能

二、 真实DOM操作的性能问题

使用JS操作DOM过程的性能可以分为两部分:

  1. js操作修改真实DOM的过程
  2. DOM树渲染到页面上的过程

虚拟DOM之前,前端开发者一般都会在JS文件中直接操作DOM元素,要了解他的性能,就要从浏览器内核的构成说起了。

我们以Webkit内核为例,他的架构图如下:
在这里插入图片描述
我们主要关注渲染引擎这一模块,他的渲染引擎由webCore和JS引擎构成,在webCore中,每个模块都有他单独的内存。

早时浏览器的内核中,DOM树由DOM模块负责控制管理,在浏览器内核中单独占有一块内存,而这块内存与JavaScript引擎所管理的内存并无直接关系。换句话说,JavaScript引擎不能直接操作真实DOM树。

为了给JavaScript提供操作DOM树的能力,浏览器在全局对象window上为JavaScript封装了一个document对象,然后在该对象上提供了大量的DOM操作接口,这些接口都是用C++实现的。

如下图,提示出的{ [ native code ] } 表示他是一个用C++编写的函数。

在这里插入图片描述
我们在调用这个函数时,JavaScript引擎并没有直接与DOM模块交互,而是由浏览器来操作DOM模块,随后再把操作结果返回给JavaScript引擎。下图是在内核中js与DOM的简单例图。

在这里插入图片描述

不仅仅是操作过程带来的性能影响,在大多数情况下,DOM树的更新会立即导致HTML的重新排列/渲染。

下面是在webkit内核中渲染引擎渲染时的工作流程:

在这里插入图片描述
paint是一个耗时的过程,然而layout是一个更耗时的过程,我们无法确定layout一定是自上而下或是自下而上进行的,甚至一次layout会牵涉到整个文档布局的重新计算。

所以一般来说,DOM操作越多,网页的性能就越差。

以下JS直接操作DOM的操作会使得浏览器立即执行重排/绘制:

  • 通过js获取需要计算的DOM属性
  • 添加或删除DOM元素
  • resize浏览器窗口大小
  • 改变字体
  • css伪类的激活,比如:hover
  • 通过js修改DOM元素样式且该样式涉及到尺寸的改变

三、React虚拟DOM应运而生

考虑到以上的性能问题,Facebook团队构建React时,考虑到要提升代码的抽象能力、避免人为的真实DOM操作、降低代码整体风险等因素,设计出了“虚拟DOM”。

简单的说,虚拟DOM实是一种用来模拟DOM结构的javascript对象,他并不能消除原生的DOM操作,你仍然需要通过浏览器提供的DOM接口来操作真实DOM树,才能使页面发生改变。

这样看来,虚拟DOM不就是多此一举吗?

答案是否定的,虚拟DOM对象带来了一个重要的优势,那就是可以在完全不访问真实DOM的情况下,掌握DOM的结构。

我们通过这个对象来掌握和控制DOM树的结构,这就为框架的性能优化带来了可能,比如我们打算进行3次DOM操作,但是经过虚拟DOM处理之后,将这3次DOM操作简化成了1次,然后交给浏览器的内核去修改真实的DOM树渲染新的HTML结构,这不就带来了性能上的提升吗?

并且,虚拟DOM的出现让开发者在开发时不需要关注DOM操作带来的性能问题,只需要关注数据与视图之间的关系,负责数据的处理,这就使得他的价值再次得到提升。

四、虚拟DOM有哪些优势

渲染机制的优化

这一点回顾上文即可,这里不做赘述。

浏览器兼容性最佳

React中的虚拟DOM具有强大的兼容性。

回顾JQuery中操作DOM,比如原生的input就有onchange事件,但这都是在浏览器底层对DOM的处理,不同的DOM只能触发关联给他的一些事件,就像div标签本身没有onchange事件。

虚拟Dom就不同了,虚拟Dom一方面模仿了原生Dom的行为,其次在事件方面也做了合成事件与原生事件的映射关系,也就时说,虚拟 DOM为所

比如:

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

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

跨平台能力

因为 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与keyif (config != null) {if (hasValidRef(config)) {ref = config.ref;}if (hasValidKey(config)) {key = '' + config.key;}// 保存self和sourceself = 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) {// 如果是多个,转成数组再赋予给propsconst 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 阶段,调用函数生成rootNoderoot = 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节点,通过一些判断寻找FiberRootconst 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以及前面代码加工的contextvar 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 并入到父节点的 effectListif (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是如何实现的,这里就不多做说明啦~

相关文章:

React 虚拟DOM的前世今生

引文 通过本文你将了解到 什么是虚拟DOM&#xff1f;虚拟DOM有什么优势&#xff1f;React的虚拟Dom是如何实现的&#xff1f;React是如何将虚拟Dom转变为真实Dom&#xff1f; 一、前言 要了解虚拟DOM&#xff0c;我们先明确一下DOM的概念。 根据MDN的说法&#xff1a; 文档…...

Java环境变量配置

一、Path环境变量配置设置环境变量的值&#xff1a;C:\Program Files\Java\jdk-17\bin目前较新的JDK安装时会自动配置javac、java程序的路径到Path环境变量中去 &#xff0c;因此&#xff0c;javac、java可以直接使用。注意&#xff1a;以前的老版本的JDK在安装的是没有自动配置…...

超详细解读!数据库表分区技术全攻略

更多内容可以关注微信公众号&#xff1a;老程序员刘飞 分区的定义 分区是一种数据库优化技术&#xff0c;它可以将大表按照一定的规则分成多个小表&#xff0c;从而提高查询和维护的效率。在分区的过程中&#xff0c;数据库会将数据按照分区规则分配到不同的分区中&#xff0…...

Redis高可用集群方案

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 @[TOC](文章目录)主从复制哨兵模式(sentinel)Cluster集群在生产过程中,Redis不一定会单独部署。因为一旦redis服务因为某些原因导致无法提供数,那么redis就不可用了。那么实现redis高可用的方式就…...

企业微信机器人发送消息

前言 随着科技的发展,各企业公司的业务不断发展,那么就需要强有力的沟通软件,其中企业微信、钉钉的能力得到了大众的认可,今天这篇文章就讲其中的一个功能-调用企业微信机器人(下文简称应用)进行消息传递。它的好处有哪些呢?自然是可以让相关人员及时追踪任务进度。 一、…...

使用PHP+yii2调用asmx服务接口

一.创建服务端 1&#xff1a;创建一个ASP.NET web应用程序 2:选择空的模板 3&#xff1a;系统生成项目目录 4&#xff1a;右键项目-添加项-新建项 5&#xff1a;选择Web 服务&#xff08;ASMX&#xff09; 6&#xff1a;选择之后项目中会有一个Test.asmx服务程序&#xff0c;…...

【042】904. 水果成篮[滑动窗口]

你正在探访一家农场&#xff0c;农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示&#xff0c;其中 fruits[i] 是第 i 棵树上的水果 种类 。 你想要尽可能多地收集水果。然而&#xff0c;农场的主人设定了一些严格的规矩&#xff0c;你必须按照要求采摘水果&…...

Linux基础知识(一)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a;小刘主页 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的绽放&#xff0…...

Redis面试题

目录 Redis持久化机制:RDB和AOF Redis线程模型有哪些&#xff1f;单线程为什么快&#xff1f; Redis的过期键有哪些删除策略&#xff1f; Redis集群方案有哪些&#xff1f; redis事务怎么实现&#xff1f; 为什么redis不支持回滚&#xff1f; redis主从复制的原理是什么 …...

微服务之Eureka

&#x1f3e0;个人主页&#xff1a;阿杰的博客 &#x1f4aa;个人简介&#xff1a;大家好&#xff0c;我是阿杰&#xff0c;一个正在努力让自己变得更好的男人&#x1f468; 目前状况&#x1f389;&#xff1a;24届毕业生&#xff0c;奋斗在找实习的路上&#x1f31f; &#x1…...

日日顺于贞超:供应链数字化要做到有数、有路、有人

在供应链行业里面&#xff0c;关于“数字化”的讨论绝对是一个经久不衰的话题。 但关于这个话题的讨论又时常让人觉得“隔靴搔痒”&#xff0c;因为数字化变革为非一日之功&#xff0c;对于企业来说意味着投入和牺牲。企业既怕不做怕将来被淘汰&#xff0c;又怕投入过高、不达预…...

Js中blob、file、FormData、DataView、TypedArray

引言 最开始我们看网页时&#xff0c;对网页的需求不高&#xff0c;显示点文字&#xff0c;显示点图片就很满足了&#xff0c;所以对于浏览器而言其操作的数据其实并不多&#xff08;比如读取本地图片显示出来&#xff0c;或上传图片到服务器&#xff09;&#xff0c;那么浏览器…...

CTFer成长之路之任意文件读取漏洞

任意文件读取漏洞CTF 任意文件读取漏洞 afr_1 题目描述: 暂无 docker-compose.yml version: 3.2services:web:image: registry.cn-hangzhou.aliyuncs.com/n1book/web-file-read-1:latestports:- 80:80启动方式 docker-compose up -d 题目Flag n1book{afr_1_solved} W…...

制造企业为何要上数字化工厂系统?

以目前形势来看&#xff0c;数字化转型是制造企业生存的关键&#xff0c;而数字化工厂管理系统是一个综合性、系统性的工程&#xff0c;波及整个企业及其供应链生态系统。数字化工厂系统所要实现的互联互通系统集成、数据信息融合和产品全生命周期集成&#xff0c;将方方面面的…...

Facebook广告投放的正确姿势:玩转目标定位

如果你正在投放 Facebook广告&#xff0c;那么你一定有过这样的经历&#xff1a;明明设置了目标受众&#xff0c;但是广告却没有带来转化。在这方面&#xff0c;你可能忽略了一个很重要的因素——目标定位。想要打造高质量、高曝光率的 Facebook广告&#xff0c;如何才能成功实…...

思科C9115AXI-H型号AP上线C9800失败处理记录

问题描述 原先的AP故障&#xff0c;从DNAC上发现状态down。现场发现AP灯灭&#xff0c;端口不亮。随即定位为AP单点故障。暂时的处理方法为&#xff1a;更换新AP上线。更换完毕后发现绿灯亮后熄灭。进一步说明网线无异常&#xff0c;属于AP故障。 如果顺利&#xff0c;在DNAC上…...

WSO2通过设定Role来订阅对应的Api

WSO2通过设定Role来订阅对应的Api1. Add Role And User1.0 Add Role1.1 Add User 1.2 Add Mapping2. Upload Api2.1 Upload Three Apis2.2 Inspection3. AwakeningWSO2安装使用的全过程详解: https://blog.csdn.net/weixin_43916074/article/details/127987099. 1. Add Role An…...

使用 PyTorch+LSTM 进行单变量时间序列预测(附完整源码)

时间序列是指在一段时间内发生的任何可量化的度量或事件。尽管这听起来微不足道&#xff0c;但几乎任何东西都可以被认为是时间序列。一个月里你每小时的平均心率&#xff0c;一年里一只股票的日收盘价&#xff0c;一年里某个城市每周发生的交通事故数。 在任何一段时间段内记…...

操作系统(day12)-- 虚拟内存;页面分配策略

虚拟内存管理 虚拟内存的基本概念 传统存储管理方式的特征、缺点 一次性&#xff1a; 作业必须一次性全部装入内存后才能开始运行。驻留性&#xff1a;作业一旦被装入内存&#xff0c;就会一直驻留在内存中&#xff0c;直至作业运行结束。事实上&#xff0c;在一个时间段内&…...

Git commit 提交没有被远端分支合并,撤销本次commit

问题&#xff1a;今天修改代码&#xff0c;误把项目配置文件修改为本地数据库连接&#xff0c;需要撤销本次commit 记录。解决办法&#xff1a;第一步&#xff1a;使用git log 查看所有commit 记录。第二步&#xff1a;使用git show commitID 查看指定commit 文件修改记录。第三…...

Netty核心原理(线程模型、核心API)与入门案例详解

Netty核心原理&#xff08;线程模型、核心API&#xff09;与入门案例详解 文章目录Netty核心原理&#xff08;线程模型、核心API&#xff09;与入门案例详解Netty 介绍原生 NIO 存在的问题概述线程模型线程模型基本介绍传统阻塞 I/O 服务模型Reactor 模型单 Reactor 单线程Nett…...

【 java 8】Lambda 表达式

&#x1f4cb; 个人简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是阿牛&#xff0c;全栈领域优质创作者。&#x1f61c;&#x1f4dd; 个人主页&#xff1a;馆主阿牛&#x1f525;&#x1f389; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4d…...

改进YOLO系列 | 谷歌团队 | CondConv:用于高效推理的条件参数化卷积

CondConv:用于高效推理的条件参数化卷积 论文地址:https://arxiv.org/pdf/1904.04971.pdf 代码地址:https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet/condconv 卷积层是现代深度神经网络的基本构建模块之一。其中一个基本假设是,卷积核应该对数…...

SQL高级 --优化

一、SQL查询的解析 关联查询过多索引失效&#xff08;单值、符合&#xff09; 二、mysql explain使用简介 1、关于id的说明&#xff1a; 2 、select_type 常见和常用的值有如下几种&#xff1a; 分别用来表示查询的类型&#xff0c;主要是用于区别普通查询、联合查询、子…...

【C++】空间配置器

空间配置器&#xff0c;听起来高大上&#xff0c;那它到底是什么东西呢&#xff1f; 1.什么是空间配置器&#xff1f; 空间配置器是STL源码中实现的一个小灶&#xff0c;用来应对STL容器频繁申请小块内存空间的问题。他算是一个小型的内存池&#xff0c;以提升STL容器在空间申…...

nginx的介绍及源码安装

文章目录前言一、nginx介绍二、nginx应用场合三、nginx的源码安装过程1.下载源码包2.安装依赖性-安装nginx-创建软连接-启动服务-关闭服务3.创建nginx服务启动脚本4.本实验---纯代码过程前言 高可用&#xff1a;高可用(High availability,缩写为 HA),是指系统无中断地执行其功…...

通过openssl生成pfx证书

通过centos7上自带的openssl工具来生成。首先创建一个pfxcert目录。然后进入此目录。 1.生成.key文件&#xff08;内含被加密后的私钥&#xff09;&#xff0c;要求输入一个自定义的密码 [rootlocalhost cert]# openssl genrsa -des3 -out server.key 2048 Generating RSA priv…...

华为OD机试真题Python实现【敏感字段加密】真题+解题思路+代码(20222023)

敏感字段加密 题目 给定一个由多个命令字组成的命令字符串; 字符串长度小于等于127字节,只包含大小写字母,数字,下划线和偶数个双引号命令字之间以一个或多个下划线_进行分割可以通过两个双引号""来标识包含下划线_的命令字或空命令字(仅包含两个双引号的命令字…...

我的 System Verilog 学习记录(1)

引言 技多不压身&#xff0c;准备开始学一些 System Verilog 的东西&#xff0c;充实一下自己&#xff0c;这个专栏的博客就记录学习、找资源的一个过程&#xff0c;希望可以给后来者一些借鉴吧&#xff0c;IC找工作的都加把油&#xff01; 本文是准备先简单介绍一下环境搭建…...

金三银四,我不允许你们不知道这些软件测试面试题

01、您所熟悉的测试用例设计方法都有哪些&#xff1f;请分别以具体的例子来说明这些方法在测试用例设计工作中的应用。 答&#xff1a;有黑盒和白盒两种测试种类&#xff0c;黑盒有等价类划分法&#xff0c;边界分析法&#xff0c;因果图法和错误猜测法。白盒有逻辑覆盖法&…...