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