在应用 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 门路映射的使用,使其可能应答不同打包场景,防止代码连接解决。