Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactDOM.render #52

Open
Cosen95 opened this issue Oct 27, 2020 · 0 comments
Open

ReactDOM.render #52

Cosen95 opened this issue Oct 27, 2020 · 0 comments

Comments

@Cosen95
Copy link
Owner

Cosen95 commented Oct 27, 2020

react 源码 16.13.1 版本

ReactDOM.render

首先来到packages/react-dom/src/client/ReactDOM.js 文件:

import {
  findDOMNode,
  render,
  hydrate,
  unstable_renderSubtreeIntoContainer,
  unmountComponentAtNode,
} from "./ReactDOMLegacy";

可以看到renderhydrate方法定义在packages/react-dom/src/client/ReactDOMLegacy.js文件中。

首先来看render方法:

/**
 * 客户端渲染
 * @param element 表示一个ReactElement对象
 * @param container 需要将组件挂载到页面中的DOM容器
 * @param callback 渲染完成后需要执行的回调函数
 */
export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function
) {
  invariant(
    isValidContainer(container),
    "Target container is not a DOM element."
  );
  if (__DEV__) {
    const isModernRoot =
      isContainerMarkedAsRoot(container) &&
      container._reactRootContainer === undefined;
    if (isModernRoot) {
      console.error(
        "You are calling ReactDOM.render() on a container that was previously " +
          "passed to ReactDOM.createRoot(). This is not supported. " +
          "Did you mean to call root.render(element)?"
      );
    }
  }
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback
  );
}

然后是hydrate方法:

/**
 * 服务端渲染
 * @param element 表示一个ReactNode,可以是一个ReactElement对象
 * @param container 需要将组件挂载到页面中的DOM容器
 * @param callback 渲染完成后需要执行的回调函数
 */
export function hydrate(
  element: React$Node,
  container: Container,
  callback: ?Function
) {
  invariant(
    isValidContainer(container),
    "Target container is not a DOM element."
  );
  if (__DEV__) {
    const isModernRoot =
      isContainerMarkedAsRoot(container) &&
      container._reactRootContainer === undefined;
    if (isModernRoot) {
      console.error(
        "You are calling ReactDOM.hydrate() on a container that was previously " +
          "passed to ReactDOM.createRoot(). This is not supported. " +
          "Did you mean to call createRoot(container, {hydrate: true}).render(element)?"
      );
    }
  }
  // TODO: throw or warn if we couldn't hydrate?
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    true,
    callback
  );
}

可以看到:render方法和hydrate方法在执行legacyRenderSubtreeIntoContainer时,第一个参数的值均为null,第四个参数的值恰好相反。

我们来看legacyRenderSubtreeIntoContainer方法:

//  packages/react-dom/src/client/ReactDOMLegacy.js
/**
 * 开始构建FiberRoot和RootFiber,之后开始执行更新任务
 *
 * @param {?React$Component<any, any>} parentComponent 父组件,可以把它当成null值来处理
 * @param {ReactNodeList} children ReactDOM.render()或者ReactDOM.hydrate()中的第一个参数,可以理解为根组件
 * @param {Container} container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param {boolean} forceHydrate 表示是否融合,用于区分客户端渲染和服务端渲染,render方法传false,hydrate方法传true
 * @param {?Function} callback ReactDOM.render()或者ReactDOM.hydrate()中的第三个参数,组件渲染完成后需要执行的回调函数
 * @returns
 */
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function
) {
  if (__DEV__) {
    topLevelUpdateWarnings(container);
    warnOnInvalidCallback(callback === undefined ? null : callback, "render");
  }

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

  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type."
  let root: RootType = (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,我们来看legacyCreateRootFromDOMContainer的具体实现:

/**
 * 创建并返回一个ReactSyncRoot实例
 *
 * @param {Container} container ReactDOM.render()或者ReactDOM.hydrate()中的第二个参数,组件需要挂载的DOM容器
 * @param {boolean} forceHydrate 是否需要强制融合,render方法传false,hydrate方法传true
 * @returns {RootType}
 */
function legacyCreateRootFromDOMContainer(
  container: Container,
  forceHydrate: boolean
): RootType {
  // 判断是否需要融合
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  // 针对客户端渲染的情况,需要将container容器中的所有元素移除
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    // 循环遍历每个子节点进行删除
    while ((rootSibling = container.lastChild)) {
      if (__DEV__) {
        if (
          !warned &&
          rootSibling.nodeType === ELEMENT_NODE &&
          (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
        ) {
          warned = true;
          console.error(
            "render(): Target node has markup rendered by React, but there " +
              "are unrelated nodes as well. This is most commonly caused by " +
              "white-space inserted around server-rendered markup."
          );
        }
      }
      container.removeChild(rootSibling);
    }
  }
  if (__DEV__) {
    if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
      warnedAboutHydrateAPI = true;
      console.warn(
        "render(): Calling ReactDOM.render() to hydrate server-rendered markup " +
          "will stop working in React v18. Replace the ReactDOM.render() call " +
          "with ReactDOM.hydrate() if you want React to attach to the server HTML."
      );
    }
  }
  // 返回一个ReactSyncRoot实例
  // 该实例具有一个_internalRoot属性指向fiberRoot
  return createLegacyRoot(
    container,
    shouldHydrate
      ? {
          hydrate: true,
        }
      : undefined
  );
}

/**
 * 根据nodeType和attribute判断是否需要融合
 *
 * @param {*} container DOM容器
 * @returns
 */
function shouldHydrateDueToLegacyHeuristic(container) {
  const rootElement = getReactRootElementInContainer(container);
  return !!(
    rootElement &&
    rootElement.nodeType === ELEMENT_NODE &&
    rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
  );
}

/**
 * 根据container来获取DON容器中的第一个子节点
 *
 * @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结构简化后如下:

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

在客户端渲染中是没有data-reactroot属性的,因此就可以区分出客户端渲染和服务端渲染。

React中的nodeType主要包含了五种,其对应的值和W3C中的nodeType标准是保持一致的,位于与DOMProperty.js同级的HTMLNodeType.js文件中:

// 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;

到这里,相信你一定可以很容易的区分出客户端渲染和服务端渲染。接下来我们将尝试去理解两个很容易混淆的概念:FiberRootRootFiber。这两个概念在React的整个任务调度过程中起着关键性的作用,如果不理解这两个概念,后续的任务调度过程就是空谈,所以这里也是我们必须要去理解的部分。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant