React18.2.0源码解析2:ReactDom核心API

作者:renzp94
时间:2023-06-07 12:40:13

ReactDom包是针对web平台的render包,主要分为客户端(client)API服务端(server)API

客户端(client)API

客户端API主要在packages/src/client目录下,一般我们使用时是import ReactDOM from 'react-dom/client',通过查看npm目录下存在一个client.js,即导出代码是使用的此文件代码。

packages/react-dom/npm/client.js

'use strict';

var m = require('react-dom');
if (process.env.NODE_ENV === 'production') {
  exports.createRoot = m.createRoot;
  exports.hydrateRoot = m.hydrateRoot;
} else {
  ...
}

由上述源码可知,其再次在文件中引入react-dom包,并且只导出了createRoothydrateRoot。在packages.json中可查看main: 'index.js',即:var m = require('react-dom');导出的是packages/react-dom/index.js中的内容

packages/react-dom/index.js

export {
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
  createPortal,
  createRoot,
  hydrateRoot,
  findDOMNode,
  flushSync,
  hydrate,
  render,
  unmountComponentAtNode,
  unstable_batchedUpdates,
  unstable_createEventHandle,
  unstable_flushControlled,
  unstable_isNewReconciler,
  unstable_renderSubtreeIntoContainer,
  unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
  version,
} from './src/client/ReactDOM';

由上述源码可知,packages/react-dom/index.js又导出了packages/react-dom/src/client/ReactDOM.js的内容,查看此文件发现其中都是实现没有再次导入导出。则最终import ReactDOM from 'react-dom/client'导出的API如下:

export {
  /** 
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
  */
  createPortal,
  createRoot,
  /** 
  hydrateRoot,
  findDOMNode,
  flushSync,
  hydrate,
  render,
  unmountComponentAtNode,
  unstable_batchedUpdates,
  unstable_createEventHandle,
  unstable_flushControlled,
  unstable_isNewReconciler,
  unstable_renderSubtreeIntoContainer,
  unstable_runWithPriority, 
  version,
  */
} from './src/client/ReactDOM';

其中注释掉的为不重要的API

createPortal

允许将子元素渲染到制定DOM中

<div>
  <p>段落1挂载父元素上</p>
  {createPortal(
    <p>段落2挂载到body上</p>,
    document.body
  )}
</div>
源码
/**
 * packages/react-dom/src/client/ReactDOM.js
 */
function createPortal(
  children: ReactNodeList,
  container: Element | DocumentFragment,
  key: ?string = null,
): React$Portal {
  if (!isValidContainer(container)) {
    throw new Error('Target container is not a DOM element.');
  }
  return createPortalImpl(children, container, null, key);
}
/**
 * packages/react-reconciler/src/ReactPortal.js
 */
export function createPortal(
  children: ReactNodeList,
  containerInfo: any,
  implementation: any,
  key: ?string = null,
): ReactPortal {
  if (__DEV__) {
    checkKeyStringCoercion(key);
  }
  return {
    $$typeof: REACT_PORTAL_TYPE,
    key: key == null ? null : '' + key,
    children,
    containerInfo,
    implementation,
  };
}

由源码可知,其实际上封装了reconciler中的createPortal方法,最终创建了一个REACT_PORTAL_TYPE类型的对象

createRoot

创建一个root,用于在浏览器DOM中显示React组件

const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
源码
/**
 * packages/react-dom/src/client/ReactDOM.js
 */
function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  if (__DEV__) {
    ...
  }
  return createRootImpl(container, options);
}
/**
 * packages/react-dom/src/client/ReactDOMRoot.js
 */
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  if (!isValidContainer(container)) {
    throw new Error('createRoot(...): Target container is not a DOM element.');
  }

  // 开发环境下的错误验证:
  // `createRoot接收的不能是body元素`
  // `不能对同一个元素进行createRoot和ReactDOM.render`
  // `不能对同一个元素进行两次createRoot`
  warnIfReactDOMContainerInDEV(container);

  let isStrictMode = false;
  let concurrentUpdatesByDefaultOverride = false;
  let identifierPrefix = '';
  let onRecoverableError = defaultOnRecoverableError;
  let transitionCallbacks = null;

  // 处理options
  if (options !== null && options !== undefined) {
    if (__DEV__) {
      ...
    }
    if (options.unstable_strictMode === true) {
      isStrictMode = true;
    }
    if (
      allowConcurrentByDefault &&
      options.unstable_concurrentUpdatesByDefault === true
    ) {
      concurrentUpdatesByDefaultOverride = true;
    }
    if (options.identifierPrefix !== undefined) {
      identifierPrefix = options.identifierPrefix;
    }
    if (options.onRecoverableError !== undefined) {
      onRecoverableError = options.onRecoverableError;
    }
    if (options.transitionCallbacks !== undefined) {
      transitionCallbacks = options.transitionCallbacks;
    }
  }

  // 创建fiber root
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
  // 将fiber root绑定到元素属性['__reactContainer$' + randomKey]上
  markContainerAsRoot(root.current, container);

  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;
  // 监听所以支持的事件
  listenToAllSupportedEvents(rootContainerElement);
  // 返回ReactRoot实例
  return new ReactDOMRoot(root);
}

其中,核心的是方法为createContainer,但此方法最终是调用packages/react-reconciler/src/ReactFiberRoot.jscreateFiberRoot方法,后面再说。其中在ReactDOMRoot类上还挂在了两个方法:renderunmount

render

function (children: ReactNodeList): void {
  const root = this._internalRoot;
  if (root === null) {
    throw new Error('Cannot update an unmounted root.');
  }
  if (__DEV__) {
    ...
  }
  updateContainer(children, root, null, null);
};

此方法最终是调用的react-reconciler中的updateContainer

unmount

// packages/react-dom/src/client/ReactDOM.js
function (): void {
  if (__DEV__) {
    ...
  }
  const root = this._internalRoot;
  if (root !== null) {
    this._internalRoot = null;
    const container = root.containerInfo;
    if (__DEV__) {
      if (isAlreadyRendering()) {
        console.error(
          'Attempted to synchronously unmount a root while React was already ' +
            'rendering. React cannot finish unmounting the root until the ' +
            'current render has completed, which may lead to a race condition.',
        );
      }
    }
    flushSync(() => {
      updateContainer(null, root, null, null);
    });
    unmarkContainerAsRoot(container);
  }
};
// packages/react-dom/src/client/ReactDOMComponentTree.js
export function unmarkContainerAsRoot(node: Container): void {
  node[internalContainerInstanceKey] = null;
}

unmount方法也是调用的updateContainer,将其所有属性只为null

通过reactreact-dom两个包来看,其实际代码都在react-reconciler包中