在使用 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.render
,ReactDOM.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
接受type
、props
、children
,然后进行一些操作:
- 处理
props
,从props
中提取出key
和ref
- 处理
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
函数。函数中相关的逻辑梳理如下:
- 创建类组件实例
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
。
函数组件
// 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-reconcilercreateContainer
函数创建的 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 设置
updater
。setState
时调用的是updater
的enqueueSetState
方法; - 在创建函数组件前,react-dom 覆盖了
ReactCurrentDispatcher
的current
。创建函数组件时,调用的是 react-dom 中定义的 hooks 实现。
在 react-dom 的源码中,我们经常见到 react-reconciler,他们两者的关系是?react-reconciler 负责生成Fiber树、协调和调度、产生操作指令。而 react-dom 负责调用DOM API,将操作指令实施到DOM树上,可以将 react-dom 类比为 react-reconciler 和 DOM 之间的翻译器。
在探索过程中,还发现React项目对 rollup 路径映射的运用,使其能够应对不同打包场景,避免代码衔接处理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。