在应用 React 时,咱们会援用 react 和 react-dom 。而在 react-dom 中依赖 react-reconciler。那么三者各自负责什么局部,又有什么分割呢?

注:本文源码根据 React 16.14 版本。

React 和 ReactDOM 各自负责什么?

react 负责形容个性,提供React API。

类组件、函数组件、hooks、contexts、refs...这些都是React个性,而 react 模块只形容个性长什么样、该怎么用,并不负责个性的具体实现。

react-dom 负责实现个性。

react-dom、react-native 称为渲染器,负责在不同的宿主载体上实现个性,达到与形容绝对应的实在成果。比方在浏览器上,渲染出DOM树、响应点击事件等。

ReactDOM.render 的输出—— ReactElement

import React from 'react';import ReactDOM from "./ReactDOM";import './index.css';import App from './App';import * as serviceWorker from './serviceWorker';ReactDOM.render(  <React.StrictMode>    <App />  </React.StrictMode>,  document.getElementById('root'));// If you want your app to work offline and load faster, you can change// unregister() to register() below. Note this comes with some pitfalls.// Learn more about service workers: https://bit.ly/CRA-PWAserviceWorker.unregister();

下面是一段常见的 React 代码。在我的项目的入口,人为显示地调用ReactDOM.renderReactDOM.render 承受 “根组件实例”和“挂载节点”,而后进行外部逻辑转换,最终将DOM树渲染到挂载节点上。

那么,ReactDOM.render拿到的 “根组件实例” 具体是什么?
组件实例其实是一个对象,以children属性关联组件的父子关系。由React.createElement函数创立。

ReactElement 是 React.createElement 的输入,ReactDOM.render的输出,是 react 和 react-dom 之间最直观的分割。那么,咱们来扒一扒这个数据结构。

咱们个别会用JSX来形容组件构造,JSX实质上是一种语法扩大,通过Babel编译最终生成上面的语句:

React.createElement(  type,  [props],  [...children])

JSX最终将对组件的形容转换为对React.createElement的调用。React.createElement做了什么?
React.createElement承受typepropschildren,而后进行一些操作:

  • 解决props,从props中提取出keyref
  • 解决children,将children以单体或者数组的模式附加到props
  • 返回一个合乎 ReactElement 数据结构的对象

如果用TypeScript简略形容 ReactElement 数据结构,它长这样????

interface ReactElement {  $$typeof: Symbol | number; // 标识该对象是React元素,REACT_ELEMENT_TYPE = symbolFor('react.element') || 0xeac7,用Symbol取得一个全局惟一值  type: string | ReactComponent | ReactFragment  key: string | null  ref: null  | string | object  props: {    [propsName: string]: any      children?: ReactElement | Array<ReactElement>  },  _owner: {    current:  null | Fiber  }}

上面这段实例代码,间接输入组件实例对象,能够更加直观理解

import React from "react";import "./styles.css";export default function App() {  return (    <div className="App">      <Heading />      <SubHeading className="secondary"/>    </div>  );}function Heading() {  return <h1>Hello CodeSandbox</h1>;}function SubHeading() {  return <h2>Start editing to see some magic happen!</h2>;}
console.log(<App />);// Output{  type: function App() {}  key: null  ref: null  props: {}  _owner: null}
console.log(<App />.type());// Output{  type: "div"  key: null  ref: null  props: {}  className: "App"  children: [    {      type: function Heading() {}      key: null      ref: null      props: {}      _owner: null      _store: {}    },    {      type: function SubHeading() {}      key: null      ref: null      props: {        className: "secondary"      }      _owner: null    }  ],  _owner: null}

只调用了一次ReactDOM.render,如何实现状态响应?

既然负责实现个性的是 react-dom,那么在没有人为调用的状况下,react 中的 setState 和 hooks 是怎么触发状态响应、视图更新的呢?

首先形容论断。

通过 setState、hooks 个性去批改组件状态时,其实是间接调用了渲染器里的办法。那么渲染器里的办法是如何注入到个性中的呢?

在创立类组件实例时,ReactDOM 会设置实例的 updater 属性,在 setState 时本质上是调用 updater.enqueueSetState

在生成函数组件之前,ReactDOM 用本人的 hooks 实现设置 dispatcher,在调用 useState 时本质上是调用 dispatcher.current.useState

接下来,咱们来摸索向类组件和函数组件注入更新器的过程。

类组件

首先,react 定义了 Component 类的属性和办法。从 Component 定义里看,有一个 updater 实例属性。

在 setState 中,调用 this.updater.enqueueSetState 进行无效操作。

那么 updater 是在何处设置的呢?

创立类组件实例,执行的是 react-reconciler/src/ReactFiberClassComponent.old.js 文件中的constructClassInstance函数。函数中相干的逻辑梳理如下:

  1. 创立类组件实例 instance
  2. 设置instance.updaterclassComponentUpdater对象
  3. instance挂载到 workInProgress(实例对应的Fiber节点) 的stateNode属性上
  4. 设置instance._reactInternals为 workInProgress
function constructClassInstance(  workInProgress: Fiber,  ctor: any,  props: any,): any {  const instance = new ctor(props, context);  const state = (workInProgress.memoizedState =    instance.state !== null && instance.state !== undefined      ? instance.state      : null);  adoptClassInstance(workInProgress, instance);}function adoptClassInstance(workInProgress: Fiber, instance: any): void {  instance.updater = classComponentUpdater;  workInProgress.stateNode = instance;  // The instance needs access to the fiber so that it can schedule updates  setInstance(instance, workInProgress);}const classComponentUpdater = {  isMounted,  enqueueSetState(inst, payload, callback) {    const fiber = getInstance(inst);    const eventTime = requestEventTime();    const lane = requestUpdateLane(fiber);    const update = createUpdate(eventTime, lane);    update.payload = payload;    if (callback !== undefined && callback !== null) {      update.callback = callback;    }    enqueueUpdate(fiber, update);    scheduleUpdateOnFiber(fiber, lane, eventTime);  },  enqueueReplaceState(inst, payload, callback) {},  enqueueForceUpdate(inst, callback) {}};

通过上述过程,在实例化组件阶段设置组件实例的updater

函数组件

// module: react/src/React.jsexport function useState<S>(  initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {  const dispatcher = resolveDispatcher();  return dispatcher.useState(initialState);}// module: react/src/ReactHooks.jsimport ReactCurrentDispatcher from './ReactCurrentDispatcher';function resolveDispatcher() {  const dispatcher = ReactCurrentDispatcher.current;  return dispatcher;}

从下面的代码剖析useState实质上是执行ReactCurrentDispatcher.current.useState

那么这里的ReactCurrentDispatcher.current是在何处设置的呢?

生成函数组件实例的过程中,react-dom 执行 react-conciler/src/ReactFiberHooks.old.js 文件中的renderWithHooks函数。在创立实例前,对ReactCurrentDispatcher.current进行设置。如下述代码所示。

import ReactSharedInternals from 'shared/ReactSharedInternals';const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;export function renderWithHooks<Props, SecondArg>(  current: Fiber | null,  workInProgress: Fiber,  Component: (p: Props, arg: SecondArg) => any,  props: Props,  secondArg: SecondArg,  nextRenderLanes: Lanes,): any {  ReactCurrentDispatcher.current =    current === null || current.memoizedState === null    ? HooksDispatcherOnMount  : HooksDispatcherOnUpdate;  let children = Component(props, secondArg);  // We can assume the previous dispatcher is always this one, since we set it  // at the beginning of the render phase and there's no re-entrancy.  ReactCurrentDispatcher.current = ContextOnlyDispatcher;  return children;}

这里的ReactCurrentDispatcher来自于shared/ReactSharedInternals.js模块,但 react 中的调用来自于react/src/ReactCurrentDispatcher.js模块,并且两者并没有间接依赖。那么它们是怎么分割在一起的?

神秘在于rollup。

在rollup打包时,通过useForks插件进行门路映射。上述代码在打包 react 时,将shared/ReactSharedInternals门路映射到react/src/ReactSharedInternals模块。
注:useForks插件定义在scripts/rollup/plugins/use-forks-plugin.js文件
注:门路映射配置在scripts/rollup/forks.js文件,上述代码截图来自于此

react/src/ReactSharedInternals中,react/src/ReactCurrentDispatcher作为接口属性导出。

import ReactCurrentDispatcher from './ReactCurrentDispatcher';const ReactSharedInternals = {  ReactCurrentDispatcher};export default ReactSharedInternals;

总结起来就是:

  • shared/ReactSharedInternals 通过rollup映射到react/src/ReactSharedInternals模块;
  • react/src/ReactSharedInternals 模块中导出 react/src/ReactCurrentDispatcher
  • 赋值ReactCurrentDispatcher.current,其实是批改了援用对象的属性,从而达到互通成果。

react-dom 和 react-reconciler 的分工

不如试着追随Hello World Custom React Renderer自制一个 react-dom 吧。入手实现代码后,会有直观感触。

react-dom 负责DOM实现(调用载体API创立、插入、删除);具体的命令则是由 react-reconciler 给出。

react-dom 提供行为的具体实现,将其汇合在hostConfig对象中,传给 react-reconciler。

“行为的具体实现”,这个形容或者有些形象。举个具体的例子来说,比方 react-reconciler 须要的appendChildToContainer行为,在DOM上的具体实现是调用element.appendChild办法。

在 react-dom 的源码中并没有显式初始化 react-reconciler ,它是如何向 react-reconciler 传递 hostConfig 的呢?同样是通过 rollup 的门路映射实现的。

具体的操作是,将'react-reconciler/src/ReactFiberHostConfig'门路映射为以后打包情景对应的 hostConfig 模块。

比方,以后打包情景是打包 react-dom ,那么就映射到'react-reconciler/src/forks/ReactFiberHostConfig.dom.js'模块,该模块中导入导出 react-dom/src/client/ReactDOMHostConfig模块 —— 真正的 hostConfig 文件。

除了负责DOM实现,react-dom还做了什么?

咱们来看一看 ReactDOM.render 逻辑:

  • 创立了一个 ReactDOMBlockingRoot 类型的实例 root,记录到挂载节点的 _reactRootContainer 属性上,往后依据这个属性判断是否已有 React 利用挂载。
  • root 实例的 _internalRoot 属性记录由 react-reconciler createContainer函数创立的 FiberRoot
  • 调用 react-reconciler updateContainer ,传入 FiberRoot 和 ReactElement
  • 进入 react-reconciler 的掌控范畴,生成 Fiber 树,遍历优化,生成组件实例/原生节点,渲染到挂载节点上。

用代码大体描述上述过程:

class ReactDOMBlockingRoot {    _internalRoot: FiberRoot,    render () {        updateContainer()    }    unmount () {        updateContainer(null, root, null, () => {          unmarkContainerAsRoot(container);        });    }}function legacyRenderSubtreeIntoContainer(  parentComponent: ?React$Component<any, any>,  children: ReactNodeList,  container: Container,  forceHydrate: boolean,  callback: ?Function,) {    const root = container._reactRootContainer = new ReactDOMBlockingRoot(container, LegacyRoot, options)    fiberRoot = root._internalRoot;    unbatchedUpdates(() => {      updateContainer(children, fiberRoot, parentComponent, callback);    });}

总结

在这篇文章中,咱们理解到 react 和 react-dom 各自的职责:react 负责形容个性,react-dom 负责实现个性。

同时,钻研了 ReactDOM.render 接管的参数,也是React.createElement的返回值 —— ReactElement,揭开它的庐山真面目:一个对象,蕴含type,props,key,ref属性,通过 children 属性形容父子关系。

既然负责实现个性的是 react-dom,那么在没有人为调用的状况下,react 中的 setState 和 hooks 是怎么触发状态响应、视图更新的呢?于是咱们探索 react-dom 对类组件、函数组件的更新器注入。

  • 在创立类组件实例阶段,react-dom 设置updatersetState时调用的是updaterenqueueSetState办法;
  • 在创立函数组件前,react-dom 笼罩了ReactCurrentDispatchercurrent。创立函数组件时,调用的是 react-dom 中定义的 hooks 实现。

在 react-dom 的源码中,咱们常常见到 react-reconciler,他们两者的关系是?react-reconciler 负责生成Fiber树、协调和调度、产生操作指令。而 react-dom 负责调用DOM API,将操作指令施行到DOM树上,能够将 react-dom 类比为 react-reconciler 和 DOM 之间的翻译器。

在摸索过程中,还发现React我的项目对 rollup 门路映射的使用,使其可能应答不同打包场景,防止代码连接解决。