ReactDOM.render、hydrate源码

一、使用入口示例

// 目录
src/index.js

// 代码
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

2、源码流程图

3、源码解析

1、入口文件

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

2、render与hydrate定义

// 目录
packages\react-dom\src\client\ReactDOMLegacy.js

// 代码
/**
 * 服务端渲染
 * @param element 表示一个ReactNode,可以是一个ReactElement对象
 * @param container 需要将组件挂载到页面中的DOM容器
 * @param callback 渲染完成后需要执行的回调函数
 */
export function hydrate(
    element: React$Node,
    container: Container,
    callback: ?Function
) {
    // 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 渲染完成后需要执行的回调函数
 */
export function render(
    element: React$Element<any>,
    container: Container,
    callback: ?Function
) {
    // 注意第一个参数为null,第四个参数为false
    return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback
    );
}

说明:render和hydrate方法都调legacyRenderSubtreeIntoContainer方法进入渲染流程,唯一区别就是第四个参数来区分,true表示服务端渲染,false表示客户端渲染

3、legacyRenderSubtreeIntoContainer源码:

// 目录
packages\react-dom\src\client\ReactDOMLegacy.js

/**
 * 开始构建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: Container,
    forceHydrate: boolean,
    callback: ?Function
) {
    // TODO: Without `any` type, Flow says "Property cannot be accessed on any
    // member of intersection type." Whyyyyyy.
    // 在第一次执行的时候,container上是肯定没有_reactRootContainer属性的
    // 所以第一次执行时,root肯定为undefined
    let root: RootType = (container._reactRootContainer: any);
    let fiberRoot;

    if (!root) {
        // Initial mount
        // 首次挂载,进入当前流程控制中,container._reactRootContainer指向一个ReactDOMBlockingRoot实例
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
            container,
            forceHydrate
        );

        // root表示一个FiberRoot实例,实例中有一个_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上已经存在一个ReactDOMBlockingRoot实例
        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);
    }

    // 返回一个RootFiber
    return getPublicRootInstance(fiberRoot);
}

说明:通过container._reactRootContainer来判断是否为首次渲染,是的话就调用legacyCreateRootFromDOMContainer方法创建ReactDOMBlockingRoot实例(调度Root),然后调用updateContainer更新,最后调用getPublicRootInstance方法返回一个RootFiber。

4、legacyCreateRootFromDOMContainer源码:

// 目录
packages\react-dom\src\client\ReactDOMLegacy.js

/**
 * 删除子节点,创建并返回一个ReactDOMBlockingRoot实例
 * @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param forceHydrate 是否需要强制融合,render方法传false,hydrate方法传true
 * @returns {FiberRoot}
 */
function legacyCreateRootFromDOMContainer(
    container: Container,
    forceHydrate: boolean
): RootType {
    // 判断是否需要融合
    const shouldHydrate =
        forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
    // First clear any existing content.
    // 针对客户端渲染的情况,需要将container容器中的所有元素移除
    if (!shouldHydrate) {
        let rootSibling;
        // 循环遍历每个子节点进行删除
        while ((rootSibling = container.lastChild)) {
            container.removeChild(rootSibling);
        }
    }

    // 返回一个FiberRoot实例
    // 该实例具有一个_internalRoot属性指向fiberRoot
    return createLegacyRoot(
        container,
        shouldHydrate
            ? {
                    hydrate: true
              }
            : undefined
    );
}

/**
 * 根据nodeType和attribute判断是否需要融合
 * @param container DOM容器
 * @returns {boolean}
 */
function shouldHydrateDueToLegacyHeuristic(container) {
    // 获取DOM容器中的第一个子节点
    const rootElement = getReactRootElementInContainer(container);
    // ELEMENT_NODE = 1 》代表元素节点
    // ROOT_ATTRIBUTE_NAME 》 data-reactroot
    // 是否具有ROOT_ATTRIBUTE_NAME属性来区分是客户端渲染还是服务端渲染
    // 客户端返回false, 服务器端返回true
    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;
    }

    // DOCUMENT_NODE = 9;》代表整个文档,即document
    if (container.nodeType === DOCUMENT_NODE) {
        return container.documentElement;
    } else {
        // 返回第一个子节点
        return container.firstChild;
    }
}

说明:
(1)补充一个知识点!!

1、!可将变量转换成boolean类型,null、undefined和空字符串取反都为true,其余都为false

!null = true;
!undefined = true;
!"" = true;

2、!! 用来做类型判断,在第一步!(变量)之后再做逻辑取反运算
if (!!a) {
    //a有内容才执行的代码...
}
等价于:
var a;
if (a != null && typeof(a) != undefined && a! = ''){
    //a有内容才执行的代码...
}

(2)shouldHydrateDueToLegacyHeuristic方法说明

  • 首先根据container来获取DOM容器中的第一个子节点
  • 通过子节点的nodeType和是否具有ROOT_ATTRIBUTE_NAME属性来区分是客户端渲染还是服务端渲染
  • ROOT_ATTRIBUTE_NAME位于packages/react-dom/src/shared/DOMProperty.js,表示data-reactroot属性

(3)服务端与客户端的区分是,node服务会在后台先根据匹配到的路由生成完整的HTML字符串,然后再将HTML字符串发送到浏览器端,最终生成的HTML结构简化后如下:

<body>
    <div id="root">
        <div data-reactroot=""></div>
    </div>
</body>

(4)在客户端渲染中是没有data-reactroot属性的,因此就可以区分出客户端渲染和服务端渲染
(5)在React中的nodeType主要包含了五种,其对应的值和W3C中的nodeType标准是保持一致的

// 目录
packages\react-dom\src\shared\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;

5、createLegacyRoot > ReactDOMBlockingRoot > createRootImpl源码

/**
 * 创建并返回一个ReactDOMBlockingRoot实例
 * @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param options 配置信息,只有在hydrate时才有值,否则为undefined
 * @returns {ReactDOMBlockingRoot}
 */
export function createLegacyRoot(
    container: Container,
    options?: RootOptions
): RootType {
    //LegacyRoot => 0
    return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

/**
 * 定义ReactDOMBlockingRoot构造函数
 * @param {*} container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param {*} tag  fiberRoot节点的标记(LegacyRoot、BatchedRoot、ConcurrentRoot)
 * @param {*} options 配置信息,只有在hydrate时才有值,否则为undefined
 */
function ReactDOMBlockingRoot(
    container: Container,
    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: Container,
    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(container, doc);
    }
    return root;
}

说明:createRootImpl方法通过调用createContainer方法来创建一个fiberRoot实例,并将该实例返回并赋值到ReactDOMBlockingRoot构造函数的内部成员_internalRoot属性上。