乐趣区

关于react.js:从react和reactdom的关系开始

在应用 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-PWA
serviceWorker.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.js
export function useState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

// module: react/src/ReactHooks.js
import 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 门路映射的使用,使其可能应答不同打包场景,防止代码连接解决。

退出移动版