React16源码解读:揭秘ReactDOM,前端开发移动

packages -> react-dom -> src -> client -> ReactDOM.js

然后我们将代码定位到第632行,可以看到ReactDOM对象包含了很多我们可能使用过的方法,例如rendercreatePortalfindDOMNodehydrateunmountComponentAtNode等。本文中我们暂且只关心render方法,但为了方便对比,也可以简单看下hydrate方法:

const ReactDOM: Object = {

/**

  • 服务端渲染

  • @param element 表示一个ReactNode,可以是一个ReactElement对象

  • @param container 需要将组件挂载到页面中的DOM容器

  • @param callback 渲染完成后需要执行的回调函数

*/

hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {

invariant(

isValidContainer(container),

‘Target container is not a DOM element.’,

);

// TODO: throw or warn if we couldn’t hydrate?

// 注意第一个参数为null,第四个参数为true

return legacyRenderSubtreeIntoContainer(

null,

element,

container,

true,

callback,

);

},

/**

  • 客户端渲染

  • @param element 表示一个ReactElement对象

  • @param container 需要将组件挂载到页面中的DOM容器

  • @param callback 渲染完成后需要执行的回调函数

*/

render(

element: React$Element,

container: DOMContainer,

callback: ?Function,

) {

invariant(

isValidContainer(container),

‘Target container is not a DOM element.’,

);

// 注意第一个参数为null,第四个参数为false

return legacyRenderSubtreeIntoContainer(

null,

element,

container,

false,

callback,

);

},

};

发现没,render方法的第一个参数就是我们在上篇文章中讲过的ReactElement对象,所以说上篇文章的内容就是为了在这里打下基础的,便于我们对参数的理解。事实上,在源码中几乎所有方法参数中的element字段均可以传入一个ReactElement实例,这个实例就是通过 Babel 编译器在编译过程中使用React.createElement方法得到的。接下来在render方法中调用legacyRenderSubtreeIntoContainer来正式进入渲染流程,不过这里需要留意一下的是,render方法和hydrate方法在执行legacyRenderSubtreeIntoContainer时,第一个参数的值均为null,第四个参数的值恰好相反。

然后将代码定位到第570行,进入legacyRenderSubtreeIntoContainer方法的具体实现:

/**

  • 开始构建FiberRoot和RootFiber,之后开始执行更新任务

  • @param parentComponent 父组件,可以把它当成null值来处理

  • @param children ReactDOM.render()或者ReactDOM.hydrate()中的第一个参数,可以理解为根组件

  • @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器

  • @param forceHydrate 表示是否融合,用于区分客户端渲染和服务端渲染,render方法传false,hydrate方法传true

  • @param callback ReactDOM.render()或者ReactDOM.hydrate()中的第三个参数,组件渲染完成后需要执行的回调函数

  • @returns {*}

*/

function legacyRenderSubtreeIntoContainer(

parentComponent: ?React$Component<any, any>,

children: ReactNodeList,

container: DOMContainer,

forceHydrate: boolean,

callback: ?Function,

) {

// TODO: Without any type, Flow says "Property cannot be accessed on any

// member of interp type." Whyyyyyy.

// 在第一次执行的时候,container上是肯定没有_reactRootContainer属性的

// 所以第一次执行时,root肯定为undefined

let root: _ReactSyncRoot = (container._reactRootContainer: any);

let fiberRoot;

if (!root) {

// Initial mount

// 首次挂载,进入当前流程控制中,container._reactRootContainer指向一个ReactSyncRoot实例

root = container._reactRootContainer = legacyCreateRootFromDOMContainer(

container,

forceHydrate,

);

// root表示一个ReactSyncRoot实例,实例中有一个_internalRoot方法指向一个fiberRoot实例

fiberRoot = root._internalRoot;

// callback表示ReactDOM.render()或者ReactDOM.hydrate()中的第三个参数

// 重写callback,通过fiberRoot去找到其对应的rootFiber,然后将rootFiber的第一个child的stateNode作为callback中的this指向

// 一般情况下我们很少去写第三个参数,所以可以不必关心这里的内容

if (typeof callback === ‘function’) {

const originalCallback = callback;

callback = function() {

const instance = getPublicRootInstance(fiberRoot);

originalCallback.call(instance);

};

}

// Initial mount should not be batched.

// 对于首次挂载来说,更新操作不应该是批量的,所以会先执行unbatchedUpdates方法

// 该方法中会将executionContext(执行上下文)切换成LegacyUnbatchedContext(非批量上下文)

// 切换上下文之后再调用updateContainer执行更新操作

// 执行完updateContainer之后再将executionContext恢复到之前的状态

unbatchedUpdates(() => {

updateContainer(children, fiberRoot, parentComponent, callback);

});

} else {

// 不是首次挂载,即container._reactRootContainer上已经存在一个ReactSyncRoot实例

fiberRoot = root._internalRoot;

// 下面的控制语句和上面的逻辑保持一致

if (typeof callback === ‘function’) {

const originalCallback = callback;

callback = function() {

const instance = getPublicRootInstance(fiberRoot);

originalCallback.call(instance);

};

}

// Update

// 对于非首次挂载来说,是不需要再调用unbatchedUpdates方法的

// 即不再需要将executionContext(执行上下文)切换成LegacyUnbatchedContext(非批量上下文)

// 而是直接调用updateContainer执行更新操作

updateContainer(children, fiberRoot, parentComponent, callback);

}

return getPublicRootInstance(fiberRoot);

}

上面代码的内容稍微有些多,咋一看可能不太好理解,我们暂且可以不用着急看完整个函数内容。试想当我们第一次启动运行项目的时候,也就是第一次执行ReactDOM.render方法的时候,这时去获取container._reactRootContainer肯定是没有值的,所以我们先关心第一个if语句中的内容:

if (!root) {

// Initial mount

// 首次挂载,进入当前流程控制中,container._reactRootContainer指向一个ReactSyncRoot实例

root = container._reactRootContainer = legacyCreateRootFromDOMContainer(

container,

forceHydrate,

);

}

这里通过调用legacyCreateRootFromDOMContainer方法将其返回值赋值给container._reactRootContainer,我们将代码定位到同文件下的第517行,去看看legacyCreateRootFromDOMContainer的具体实现:

/**

  • 创建并返回一个ReactSyncRoot实例

  • @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器

  • @param forceHydrate 是否需要强制融合,render方法传false,hydrate方法传true

  • @returns {ReactSyncRoot}

*/

function legacyCreateRootFromDOMContainer(

container: DOMContainer,

forceHydrate: boolean,

): _ReactSyncRoot {

// 判断是否需要融合

const shouldHydrate =

forceHydrate || shouldHydrateDueToLegacyHeuristic(container);

// First clear any existing content.

// 针对客户端渲染的情况,需要将container容器中的所有元素移除

if (!shouldHydrate) {

let warned = false;

let rootSibling;

// 循环遍历每个子节点进行删除

while ((rootSibling = container.lastChild)) {

container.removeChild(rootSibling);

}

}

// Legacy roots are not batched.

// 返回一个ReactSyncRoot实例

// 该实例具有一个_internalRoot属性指向fiberRoot

return new ReactSyncRoot(

container,

LegacyRoot,

shouldHydrate

? {

hydrate: true,

}
undefined,

);

}

/**

  • 根据nodeType和attribute判断是否需要融合

  • @param container DOM容器

  • @returns {boolean}

*/

function shouldHydrateDueToLegacyHeuristic(container) {

const rootElement = getReactRootElementInContainer(container);

return !!(

rootElement &&

rootElement.nodeType === ELEMENT_NODE &&

rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)

);

}

/**

  • 根据container来获取DOM容器中的第一个子节点

  • @param container DOM容器

  • @returns {*}

*/

function getReactRootElementInContainer(container: any) {

if (!container) {

return null;

}

if (container.nodeType === DOCUMENT_NODE) {

return container.documentElement;

} else {

return container.firstChild;

}

}

其中在shouldHydrateDueToLegacyHeuristic方法中,首先根据container来获取 DOM 容器中的第一个子节点,获取该子节点的目的在于通过节点的nodeType和是否具有ROOT_ATTRIBUTE_NAME属性来区分是客户端渲染还是服务端渲染,ROOT_ATTRIBUTE_NAME位于packages/react-dom/src/shared/DOMProperty.js文件中,表示data-reactroot属性。我们知道,在服务端渲染中有别于客户端渲染的是,node服务会在后台先根据匹配到的路由生成完整的HTML字符串,然后再将HTML字符串发送到浏览器端,最终生成的HTML结构简化后如下:

在客户端渲染中是没有data-reactroot属性的,因此就可以区分出客户端渲染和服务端渲染。在 React 中的nodeType主要包含了五种,其对应的值和W3C中的nodeType标准是保持一致的,位于与DOMProperty.js同级的HTMLNodeType.js文件中:

// 代表元素节点

export const ELEMENT_NODE = 1;

// 代表文本节点

export const TEXT_NODE = 3;

// 代表注释节点

export const COMMENT_NODE = 8;

// 代表整个文档,即document

export const DOCUMENT_NODE = 9;

// 代表文档片段节点

export const DOCUMENT_FRAGMENT_NODE = 11;

经过以上分析,现在我们就可以很容易地区分出客户端渲染和服务端渲染,并且在面试中如果被问到两种渲染模式的区别,我们就可以很轻松地在源码级别上说出两者的实现差异,让面试官眼前一亮。怎么样,到目前为止,其实还是觉得挺简单的吧?

FiberRoot VS RootFiber


在这一小节中,我们将尝试去理解两个比较容易混淆的概念:FiberRootRootFiber。这两个概念在 React 的整个任务调度过程中起着关键性的作用,如果不理解这两个概念,后续的任务调度过程就是空谈,所以这里也是我们必须要去理解的部分。接下来接着上一小节的内容,继续分析legacyCreateRootFromDOMContainer方法中的剩余内容,在函数体的结尾返回了一个ReactSyncRoot实例,我们重新回到ReactDOM.js文件可以很容易找到ReactSyncRoot构造函数的具体内容:

/**

  • ReactSyncRoot构造函数

  • @param container DOM容器

  • @param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)

  • @param options 配置信息,只有在hydrate时才有值,否则为undefined

  • @constructor

*/

function ReactSyncRoot(

container: DOMContainer,

tag: RootTag,

options: void | RootOptions,

) {

this._internalRoot = createRootImpl(container, tag, options);

}

/**

  • 创建并返回一个fiberRoot

  • @param container DOM容器

  • @param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)

  • @param options 配置信息,只有在hydrate时才有值,否则为undefined

  • @returns {*}

*/

function createRootImpl(

container: DOMContainer,

tag: RootTag,

options: void | RootOptions,

) {

// Tag is either LegacyRoot or Concurrent Root

// 判断是否是hydrate模式

const hydrate = options != null && options.hydrate === true;

const hydrationCallbacks =

(options != null && options.hydrationOptions) || null;

// 创建一个fiberRoot

const root = createContainer(container, tag, hydrate, hydrationCallbacks);

// 给container附加一个内部属性用于指向fiberRoot的current属性对应的rootFiber节点

markContainerAsRoot(root.current, container);

if (hydrate && tag !== LegacyRoot) {

const doc =

container.nodeType === DOCUMENT_NODE

? container
container.ownerDocument;

eagerlyTrapReplayableEvents(doc);

}

return root;

}

从上述源码中,我们可以看到createRootImpl方法通过调用createContainer方法来创建一个fiberRoot实例,并将该实例返回并赋值到ReactSyncRoot构造函数的内部成员_internalRoot属性上。我们继续深入createContainer方法去探究一下fiberRoot完整的创建过程,该方法被抽取到与react-dom包同级的另一个相关的依赖包react-reconciler包中,然后定位到react-reconciler/src/ReactFiberReconciler.js的第299行:

/**

  • 内部调用createFiberRoot方法返回一个fiberRoot实例

  • @param containerInfo DOM容器

  • @param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)

  • @param hydrate 判断是否是hydrate模式

  • @param hydrationCallbacks 只有在hydrate模式时才可能有值,该对象包含两个可选的方法:onHydrated和onDeleted

  • @returns {FiberRoot}

*/

export function createContainer(

containerInfo: Container,

tag: RootTag,

hydrate: boolean,

hydrationCallbacks: null | SuspenseHydrationCallbacks,

): OpaqueRoot {

return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);

}

/**

  • 创建fiberRoot和rootFiber并相互引用

  • @param containerInfo DOM容器

  • @param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)

  • @param hydrate 判断是否是hydrate模式

  • @param hydrationCallbacks 只有在hydrate模式时才可能有值,该对象包含两个可选的方法:onHydrated和onDeleted

  • @returns {FiberRoot}

*/

export function createFiberRoot(

containerInfo: any,

tag: RootTag,

hydrate: boolean,

hydrationCallbacks: null | SuspenseHydrationCallbacks,

): FiberRoot {

// 通过FiberRootNode构造函数创建一个fiberRoot实例

const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

if (enableSuspenseCallback) {

root.hydrationCallbacks = hydrationCallbacks;

}

// Cyclic construction. This cheats the type system right now because

// stateNode is any.

// 通过createHostRootFiber方法创建fiber tree的根节点,即rootFiber

// 需要留意的是,fiber节点也会像DOM树结构一样形成一个fiber tree单链表树结构

// 每个DOM节点或者组件都会生成一个与之对应的fiber节点(生成的过程会在后续的文章中进行解读)

// 在后续的调和(reconciliation)阶段起着至关重要的作用

const uninitializedFiber = createHostRootFiber(tag);

// 创建完rootFiber之后,会将fiberRoot实例的current属性指向刚创建的rootFiber

root.current = uninitializedFiber;

// 同时rootFiber的stateNode属性会指向fiberRoot实例,形成相互引用

uninitializedFiber.stateNode = root;

// 最后将创建的fiberRoot实例返回

return root;

}

一个完整的FiberRootNode实例包含了很多有用的属性,这些属性在任务调度阶段都发挥着各自的作用,可以在ReactFiberRoot.js文件中看到完整的FiberRootNode构造函数的实现(这里只列举部分属性):

/**

  • FiberRootNode构造函数

  • @param containerInfo DOM容器

  • @param tag fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)

  • @param hydrate 判断是否是hydrate模式

  • @constructor

*/

function FiberRootNode(containerInfo, tag, hydrate) {

// 用于标记fiberRoot的类型

this.tag = tag;

// 指向当前激活的与之对应的rootFiber节点

this.current = null;

// 和fiberRoot关联的DOM容器的相关信息

this.containerInfo = containerInfo;

// 当前的fiberRoot是否处于hydrate模式

this.hydrate = hydrate;

// 每个fiberRoot实例上都只会维护一个任务,该任务保存在callbackNode属性中

this.callbackNode = null;

// 当前任务的优先级

this.callbackPriority = NoPriority;

}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

更多面试题

**《350页前端校招面试题精编解析大全》**内容大纲主要包括 HTML,CSS,前端基础,前端核心,前端进阶,移动端开发,计算机基础,算法与数据结构,项目,职业发展等等

资料获取方式:点击蓝色传送门免费获取

海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-VW9ygZNM-1712200719509)]

[外链图片转存中…(img-6xzTUAgF-1712200719510)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-ma6p6MiM-1712200719510)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

更多面试题

**《350页前端校招面试题精编解析大全》**内容大纲主要包括 HTML,CSS,前端基础,前端核心,前端进阶,移动端开发,计算机基础,算法与数据结构,项目,职业发展等等

资料获取方式:点击蓝色传送门免费获取

[外链图片转存中…(img-aCzIVldB-1712200719511)]

  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值