virtual DOM和真实DOM的区别_React源码剖析:react、 react-dom、react-reconciler 的关系

13390e31cc6ccee54a42ebf3abe023ec.png

在使用 React 时,我们会引用 react 和 react-dom 。而在 react-dom 中依赖 react-reconciler。那么三者各自负责什么部分,又有什么联系呢?

文章导览

文章将解释以下几个问题:

  • react 和 react-dom 各自负责什么?
  • ReactDOM.render 的输入 —— ReactElement 是什么?
  • 只调用了一次ReactDOM.render,如何实现状态响应?
  • react-dom 和 react-reconciler 如何分工?

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

react 和 react-dom 各自负责什么?

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 实例属性。

5680442be129fb98e9a4b8139b408ac8.png
React.Component 定义

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

8eca063d3e75f17111bfb74d4ebd8fea.png
setState 中调用 this.updater.enqueueSetState

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

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

  • 创建类组件实例 instance
  • 设置instance.updater为classComponentUpdater对象
  • 将 instance挂载到 workInProgress(实例对应的Fiber节点) 的stateNode属性上
  • 设置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属性已被 react-reconciler 中的对象替代。

函数组件

// 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-reconciler/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。

05f5512208ba3aee103d2dfbe2820d3e.png
scripts/rollup/forks.js

在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模块 —— 在 react-dom 包中维护的 hostConfig 文件。

f3fddfd4e1d7d81412971e78d2bcad6a.png
scripts/rollup/forks.js 中的路径映射

除了负责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 的输入 —— ReactElement 是什么?

ReactDOM.render 接收的参数,也是React.createElement的返回值 —— ReactElement,它的庐山真面目是一个对象,包含type,props,key,ref属性,通过 children 属性描述父子关系。

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

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

  • 在创建类组件实例阶段,react-dom 设置updater。setState时调用的是updater的enqueueSetState方法;
  • 在创建函数组件前,react-dom 覆盖了ReactCurrentDispatcher的current。创建函数组件时,调用的是 react-dom 中定义的 hooks 实现。

react-dom 和 react-reconciler 如何分工?

在 react-dom 的源码中,我们经常见到 react-reconciler,他们两者的关系是?

react-reconciler 负责协调:生成Fiber树(React中的虚拟DOM)、协调和调度、产生操作指令。

react-dom 负责渲染:调用DOM API,将操作指令实施到DOM树上,可以将 react-dom 类比为 react-reconciler 和 DOM 之间的翻译器。

另外,在探索过程中,还发现React项目对 rollup 路径映射的运用,使其能够应对不同打包场景,避免代码衔接的额外处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值