我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 render() 方法是整个 React 应用的入口方法,所有的 jsx 渲染、hook 的挂载和执行等,都在这个里面。 从 React18 开始, 之前的使用方式: 新的使用方式: 对比后,发现有几点不一样的使用方式: 源码位置:ReactDOMRoot.js#L185。 createRoot()函数有两个参数,第 1 个是传入一个 dom 节点,第 2 个是可选的配置参数,我们暂时先不管 options 的配置,先把这些配置代码删去,只看大流程。 我们再提炼下其中的流程: 上面的每个函数我们都没有去关注他具体的实现,只是先看下大致的流程,避免因太多深入某一项,导致忘记大局流程,造成思维混乱。我们可以看到上面的 ReactDOMRoot()类还是在当前的文件中:ReactDOMRoot()的实现。 类的主体简单,就是将上层创建的 FiberRootNode 类型的节点放到实例的 _internalRoot 属性上。 这里用原型链的方式,为 ReactDOMRoot 类添加了两个方法:render() 和 unmout(); 终于讲到了 render() 方法,render() 大部分的操作都是进行参数的校验,避免开发者因之前使用 render() 方法的习惯,造成使用错误。最后调用 updateContainer() 函数会做很多,如会将 element 结构转为 fiber 树,并最终生成 html 节点渲染到 root.containerInfo 指定的 dom 元素中;将组件中声明的 hook 挂载到 hook 链表中。 我们现在再单独看下对参数的校验,这里不影响整体功能,您也可以直接跳过。这些参数的校验,主要是为了给使用之前版本的用户进行提示,毕竟很多开发者对框架的使用有很大的惯性,当 api 的使用方式有变动时,最好给到足够的提示,可以让用户知道怎么去适配最新的使用方式: unmount() 方法相对来说就简单很多,主要是用来清除数据、卸载 fiber 树等。 入口方法 render() 我们初步的流程大致了解了,不过有很多重要的函数都没有展开说,如 createContainer(), listenToAllSupportedEvents(), updateContainer()等等,接下来我们都会一一讲解到。1. render() 方法的使用 #
render()
方法的使用跟之前不一样了。import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
// React18.x
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
react-dom/client
引入;2. createRoot() #
export function createRoot(container: Element | Document | DocumentFragment, options?: CreateRootOptions): RootType {
// 判断container是否是合法的dom元素
if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.');
}
// 若container为body或已被作为root使用过,则在dev环境发出警告
warnIfReactDOMContainerInDEV(container);
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
/**
* 创建一个 FiberRootNode 类型的节点,fiberRootNode 是整个应用的根节点
* 在react的更新过程中,会有current(当前正在展示)和workInProgress(将要更新的)两个fiber树,
* fiberRootNode 默认指向到current,
* workInProgress更新并commit完毕后,fiberRootNode会指向到workProgress
* 调用链路: createContainer() -> createFiberRoot() -> {new FiberRootNode(), createHostRootFiber()} -> createFiber() -> new FiberNode()
* root节点是通过 new FiberRootNode() 初始化出来的实例,属性也非常多,
* 当前我们可以只关注其中的两个属性:
* root.current: 指向到哪棵fiber树;初始化时会指向到一颗空树,因为刚开始时还没有树;
* root.containerInfo: 创建当前节点时的dom节点
*/
const root = createContainer(
container,
ConcurrentRoot, // 1
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
// 将DOM节点 container 标记为已被作为root使用过
// 并通过一个属性指向到fiber节点:
// container['__reactContainer$'] = root.current; // root为fiber类型的节点
// 这里就形成了互相指向,root.containerInfo = container;
markContainerAsRoot(root.current, container);
// 获取container的真实element元素,若container是注释类型的元素,则使用其父级元素,否则直接使用container
// 大概是因为注释节点无法挂载事件
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE ? (container.parentNode: any) : container;
// 绑定所有可支持的事件到 rootContainerElement 节点上
listenToAllSupportedEvents(rootContainerElement);
// 使用ReactDOMRoot实例化一个对象,属性_internalRoot 指向到到 root
// 并有两个方法 render() 和 unmount()
return new ReactDOMRoot(root);
}
等),document 节点,文档片段节点等,都是合法的挂载对象;额外的,注释节点就不是一个合法的挂载对象;
createContainer()
函数的调用链路很深,一直到最终的 FiberNode() 函数。这里我们仅了解这些函数的大致功能,后续我们会一一进行解析。3. ReactDOMRoot() 类的实现 #
/**
* 创建一个实例,并可以调用render()方法
* @param {FiberRoot} internalRoot
* @constructor
*/
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
/**
* render的入口
* @param {ReactNodeList} children 通过createElement或babel转换后的element结构
* element结构 { $$typeof, type, props, key, ref }
* 不过这里如null, boolean等类型,也认为是有效的children类型
*/
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(children: ReactNodeList): void {};
// 卸载
ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = function(): void {};
3.1 render() 方法 #
updateContainer()
方法来实现后续的操作。/**
* render的入口
* @param {ReactNodeList} children 通过createElement或babel转换后的element结构
* element结构 { $$typeof, type, props, key, ref }
* 不过这里如null, boolean等类型,也认为是有效的children类型
*/
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(children: ReactNodeList): void {
const root = this._internalRoot; // FiberRootNode
if (root === null) {
// 若root为null,说明该树已被卸载
throw new Error('Cannot update an unmounted root.');
}
// 省略一堆的参数校验
updateContainer(children, root, null, null);
};
if (typeof arguments[1] === 'function') {
// 第2个参数是function时,给出提示,render方法不再支持callback,而应当放在useEffect()中
// 主要是为了给使用之前版本的用户进行提示
console.error(
'render(...): does not support the second callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
);
} else if (isValidContainer(arguments[1])) {
// 若第2个参数是一个挂载dom节点,给出提示,若是通过createRoot创建然后调用render的,第2个参数不用再传入dom节点
// 主要是为了给使用之前版本的用户进行提示
// 之前是ReactDOM.render(
3.2 unmount() 方法 #
ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = function(): void {
if (__DEV__) {
if (typeof arguments[0] === 'function') {
// 若传入了callback参数,则给出提示,要想在组件卸载时进行回调,
// 请使用useEffect()
console.error(
'unmount(...): does not support a callback argument. ' +
'To execute a side effect after rendering, declare it in a component body with useEffect().',
);
}
}
const root = this._internalRoot; // FiberRootNode节点,我们在new的时候,将其给到了该属性
if (root !== null) {
this._internalRoot = null; // 置为空
const container = root.containerInfo; // dom元素
flushSync(() => {
// 解除root中的所有fiber节点
updateContainer(null, root, null, null);
});
/**
* 我们在createRoot中,将root.current给到了container属性,标记container为已使用
* container['__reactContainer$'] = root.current;
* 这里我们将其解除指向:
* container['__reactContainer$'] = null;
*/
unmarkContainerAsRoot(container);
}
};
4. 总结 #
版权属于:
加速器之家
作品采用:
《
署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
》许可协议授权
评论