从源码中探究React中的虚拟DOM
引文
通过本文你将了解到
- 什么是虚拟DOM?
- 虚拟DOM有什么优势?
- React的虚拟Dom是如何实现的?
- React是如何将虚拟Dom转变为真实Dom?
一、概念
虚拟DOM实际上是一种用来模拟DOM结构的javascript对象。当页面变化的时候通过一种算法来对比前后的虚拟DOM结构,找到需要更改的地方,然后再操作真实DOM,就可以更新页面。
二、虚拟DOM有哪些优势
2.1 浏览器兼容性最佳
React中的虚拟DOM具有强大的兼容性。我们现在知道,在React编译过程中,React和babel将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与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);
}
代码看似很多,其实逻辑非常清晰:
- 处理参数,对传进来的数据进行加工处理,比如提取config参数,处理props等
- 通过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;}
}
再梳理一下这个过程:
- root = container._reactRootContainer:如不存在,表示是 Initial mount 阶段,调用 legacyCreateRootFromDOMContainer 生成;如存在,表示是 update 阶段;
- fiberRoot = root._internalRoot:从 root 上拿到内部 _internalRoot 属性;
- 封装 callback 回调(通过 fiberRoot 找到其对应的 rootFiber,然后将 rootFiber.child.stateNode 作为 callback 中的 this 指向,调用 callback);
- mount 阶段,需要尽快完成,不允许批量更新(使用的是legacy渲染模式),使用 unbatchedUpdates 调用 updateContainer();
update 阶段,直接调用 updateContainer() 执行更新; - 返回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里确定调度更新的模式,有performSyncWorkOnRoot或performConcurrentWorkOnRoot方法。不同的调用取决于本次更新是同步更新还是异步更新。
updateContainer 不管什么模式都会走 performSyncWorkOnRoot,这个函数的核心功能分别是:
这个函数,而这个函数三个核心处理过程分别是:
- beginWork
- completeUnitOfWork
- 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 的工作其实可以分成两部分
- mount 时:会根据 Fiber.tag 的不同,执行不同类型的创建子 Fiber 节点的程序
- 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相关的处理,这部分代码我删掉了,其余,判断当前组件是否有实例,如果有就去更新实例,如果没有那就创建实例,所以我们聚焦到constructClassInstance与mountClassInstance、finishClassComponent三个方法,看命名就能猜到,前者一定是创造实例,后者是应该是挂载实例前的一些处理,先看第一个方法:
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 节点的 update、diff、flags 标记、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主要做了两件事情:
- 如果是原生 DOM 节点,调用 insertInContainerBefore 或 appendChildToContainer 来在相应的位置插入 DOM 节点
- 如果不是原生 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?虚拟DOM有什么优势?React的虚拟Dom是如何实现的?React是如何将虚拟Dom转变为真实Dom? 一、概念 虚拟DOM实际上是一种用来模拟DOM结构的javascript对象。当页面变化的时候通过一种算法来…...
容器架构概述
文章目录1. 介绍容器历史2. 描述 Linux 容器架构3. Podman 管理容器1. 介绍容器历史 近年来,容器迅速流行起来。然而,容器背后的技术已经存在了相对较长的时间。2001年,Linux引入了一个名为VServer的项目。VServer 是第一次尝试在高度隔离的…...
掌握MySQL分库分表(四)分库分表中间件Sharding-Jdbc,真实表、逻辑表、绑定表、广播表,常见分片策略
文章目录什么是ShardingSphere-JDBC?Sharding-Jdbc常见概念术语数据节点Node真实表逻辑表绑定表广播表数据库表分片(水平库、表)分片键 (PartitionKey)行表达式分片策略 InlineShardingStrategy(必备)标准分片策略Stan…...

2022-06-16_555时基的迷人历史和先天缺陷!
https://www.eet-china.com/news/magazine220608.html 555时基的迷人历史和先天缺陷! 发布于2022-06-16 03:39:12 LARRY STABILE 流行数十年的555时基,业内不知晓的工程师应该寥寥无几!几乎所有的数字电路教材中,都有该芯片的身影…...
SpringBoot 基础知识汇总
一、环境准备Java:Spring Boot 3.0.2 需要 Java 17,并且与 Java 19 兼容Maven:Apache Maven 3.5 或更高版本兼容二、启动器以下应用程序启动器由 Spring Boot 在该组下提供:org.springframework.boot表 1.Spring 引导应用程序启动…...
centos7下用kvm启动Fedora36 Cloud镜像
环境 os:centos7 Arch: aarch64 安装qemu-kvm yum install qemu-kvm kvm virt-install libvirt systemctl start libvirtd.service创建镜像 下载aarch64架构的Fedora36镜像 wget https://mirrors.tuna.tsinghua.edu.cn/fedora/releases/36/Cloud/aarch64/images/Fedora-Cl…...

修复 K8s SSL/TLS 漏洞(CVE-2016-2183)指南
作者:老 Z,中电信数智科技有限公司山东分公司运维架构师,云原生爱好者,目前专注于云原生运维,云原生领域技术栈涉及 Kubernetes、KubeSphere、DevOps、OpenStack、Ansible 等。 前言 测试服务器配置 主机名IPCPU内存系…...

uniapp 引入彩色symbol和 指令权限
uniapp 引入iconfont图标库彩色symbol 1,先去阿里巴巴矢量图标库登录 然后点击下载至本地 2.下载本地,然后解压文件夹 3.打开终端cmd命令窗口 npm安装全局包npm i -g iconfont-tools 4.终端切换到上面解压的文件夹里面,运行iconfont-too…...

【C语言】初识结构体
Yan-英杰 悟已往之不谏 知来者之可追 目录 一、结构体的声明 二、结构体变量的定义和初始化 三、结构体成员的访问 四、结构体传参 一、结构体的声明 1.结构的基础知识 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。2.结构的…...

前端将base64图片转换成file文件
1、base64转成file具体代码 // base64图片转file的方法(base64图片, 设置生成file的文件名)function base64ToFile(base64, fileName) {// 将base64按照 , 进行分割 将前缀 与后续内容分隔开let data base64.split(,);// 利用正则表达式 从前缀中获取图…...

OAK相机跑各种yolo模型的检测帧率和深度帧率
编辑:OAK中国 首发:oakchina.cn 喜欢的话,请多多👍⭐️✍ 内容可能会不定期更新,官网内容都是最新的,请查看首发地址链接。 ▌前言 Hello,大家好,这里是OAK中国,我是助手…...

存储拆分后,如何解决唯一主键问题?
在单库单表时,业务 ID 可以依赖数据库的自增主键实现,现在我们把存储拆分到了多处,如果还是用数据库的自增主键,就会出现主键重复的情况。 所以我们不得不面对的一个选择,就是ID生成器,使用一个唯一的字符…...
仿射变换学习
affine_trans_(iamge,region,xld):仿射变换-作用到iamge,region、xld等都可以 vector_angle_to_rigid():得到一个刚性仿射变换矩阵 orientation_region():得到指定区域的弧度(与x轴正方向的弧度)…...
基于java的爬虫框架webmagic基本使用
简单记录一下java项目实现网页爬取数据的基本使用. 需要引入的依赖 <dependency><groupId>us.codecraft</groupId><artifactId>webmagic-core</artifactId><version>0.7.3</version></dependency><dependency><grou…...

Python每日一练(20230221)
目录 1. 不同路径 II 2. 字符串转换整数 (atoi) 3. 字符串相乘 1. 不同路径 II 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中…...

【Linux】vim拒绝服务安全漏洞修复
根据国家信息安全漏洞共享平台于2023年2月19日发布的安全漏洞通知,Linux系统自带的vim编辑器存在两个高危安全漏洞(CNVD-2023-09166、CNVD-2023-09647),攻击者可以利用该漏洞发起拒绝服务攻击,并可能运行(恶…...
moveit 2源码编译
文章目录前言下载编译过程创建开发环境拉取源代码更新rosdep下载安装moveit 2依赖包编译源码输出结果总结前言 本文用来记录moveit 2从源码编译的全流程。 本机环境: 系统:debian 11 ros版本:ros2 humble 处理器:intel i7 内存&a…...

2022年全国职业院校技能大赛(中职组)网络安全竞赛试题A模块(4)
目录 二、竞赛注意事项 (本模块20分) 一、项目和任务描述: 二、服务器环境说明 三、具体任务(每个任务得分以电子答题卡为准) A-1任务一 登录安全加固(Windows) 1.密码策略 a.更改或创建…...

微服务保护之sentinel熔断器
文章目录 目录 文章目录 前言 一、解决微服务雪崩的问题 二、使用步骤 三、熔断器的使用 3.1 限流规则 3.1.1流控模式 3.1.2流控效果 3.2 隔离和降级 3.2.1 隔离 3.2.2 降级 四、sentinel规则持久化 总结 前言 在基于 SpringCloud 构建的微服务体系中,服务间的调用…...
电动打气泵方案开发--鼎盛合PCBA方案
开车的每一个司机都必须要知道一个事情——定期检查轮胎气压是否正常,因为轮胎胎压不足会导致轮胎过早磨损,从而造成容易发生道路交通事故,并且对汽车的操控性和牵引力带来不良影响,甚至会出现爆胎或汽车失控等极其危险的事故。电…...
P3 QT记事本(3.4)
3.4 文件选择对话框 QFileDialog 3.4.1 QFileDialog 开发流程 使用 QFileDialog 的基本步骤通常如下: 实例化 :首先,创建一个 QFileDialog 对象的实例。 QFileDialog qFileDialog;设置模式 :根据需要设置对话框的模式&…...

(LeetCode 每日一题)3170. 删除星号以后字典序最小的字符串(贪心+栈)
题目:3170. 删除星号以后字典序最小的字符串 思路:贪心栈,时间复杂度0(n)。 对于每一个‘ * ’,优先选最右边的最小字符,才会使最终得到的字符串最小。 用栈,来记录每个字符的位置下标。细节看注释。 C版本…...
Gartner《How to Create and Maintain a Knowledge Base forHumans and AI》学习报告
核心观点 本研究是一份 Gartne关于如何创建和维护面向人类与人工智能(AI)的知识库的研究报告。报告强调了知识库在知识管理(KM)中的核心地位,尤其是在生成式人工智能(GenAI)时代,一个结构良好的知识库是知识管理成功的关键,反之则可能成为整个知识管理实践的失败点。…...

无人机光纤FC接口模块技术分析
运行方式 1. 信号转换:在遥控器端,模块接收来自遥控器主控板的电信号。 2.电光转换:模块内部的激光发射器将电信号转换成特定波长的光信号。 3.光纤传输:光信号通过光纤跳线传输。光纤利用全内反射原理将光信号约束在纤芯内进行…...
C++ const 修饰符深入浅出详解
C const 修饰符深入浅出详解 📅 更新时间:2025年6月6日 🏷️ 标签:C | const关键字 | 常量 | 多文件编程 | 现代C 文章目录 前言🌟 一、const 是什么?为什么要用?示例✅ const 的四大好处 &…...
Spring中循环依赖问题的解决机制总结
一、解决机制 1. 什么是循环依赖 循环依赖是指两个或多个Bean之间相互依赖对方,形成一个闭环的依赖关系。最常见的情况是当Bean A依赖Bean B,而Bean B又依赖Bean A时,就形成了循环依赖。在Spring容器初始化过程中,如果不加以特殊…...
Delphi SetFileSecurity 设置安全描述符
在Delphi中,使用Windows API函数SetFileSecurity来设置文件或目录的安全描述符时,你需要正确地构建一个安全描述符(SECURITY_DESCRIPTOR结构)。这个过程涉及到几个步骤,包括创建或修改安全描述符、设置访问控制列表&am…...
LeetCode 239. 滑动窗口最大值(单调队列)
题目传送门:239. 滑动窗口最大值 - 力扣(LeetCode) 题意就是求每个窗口内的最大值,返回一个最大值的数组,滑动窗口的最值问题。 做法:维护一个单调递减队列,队头为当前窗口的最大值。 设计的…...

RockyLinux9.6搭建k8s集群
博主介绍:✌全网粉丝5W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验…...
8.axios Http网络请求库(1)
一句话总结 Axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js,帮助你轻松发送请求、接收响应。 Axios is a Promise-based HTTP client for the browser and Node.js, making it easy to send requests and handle responses. …...