剖析ReactDOM.render( ... )
ReactDOM.render 函数的三个参数
import React from 'react';
import ReactDOMfrom 'react-dom';
import UpdateCounter from './pages/UpdateCounter';
ReactDOM.render(<UpdateCounter name="Taylor" />, document.getElementById('root'))
第一个参数:<UpdateCounter name="Taylor" /> 是一个 React 元素。React 元素是由 JSX 语法创建的,它实际上是一个描述组件结构和配置的 JavaScript 对象。在这个例子中, 表示了一个将被渲染到页面上的 UpdateCounter 组件,它接受一个名为 "Taylor" 的 name 属性。
第二个参数:document.getElementById('root') 是一个 DOM 元素,它表示了 React 应用程序最终渲染在页面中的容器。ReactDOM.render 函数将会把第一个参数(React 元素)渲染到这个指定的 DOM 元素中。
第三个参数:这是一个可选的回调函数,它在应用程序渲染完成后被调用。这个回调函数会在应用程序完成渲染后执行,可以用于执行一些操作。
最后, ReactDOM.render 函数的返回值是 legacyRenderSubtreeIntoContainer 函数的执行结果。这个函数是 React 内部用于渲染子树的函数。实际上,ReactDOM.render 返回的是一个引用,指向渲染的组件实例。这个引用可以用来在组件外部操作组件,例如调用组件的方法或获取组件的状态。
render返回值
在React源码中,ReactDOM.render 函数实际上是通过嵌套了多层函数的方式来实现的。为了便于理解和调试,可以在 ReactDOM.render 函数的内部使用 console.log(...) 将函数执行结果输出,以便查看最终的返回值。这种方法可以帮助开发者了解函数的执行过程和返回结果,有助于调试和理解代码的运行逻辑。以下是示例代码:
function ReactDOMLegacyRoot(container, options) {
// ...
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
// ...
}
function legacyRenderSubtreeIntoContainer(
parentComponent,
element,
container,
forceHydrate,
callback
) {
// ...
return ReactMount._renderSubtreeIntoContainer(
parentComponent,
wrappedElement,
container,
forceHydrate,
callback
);
}
function ReactMount(parentComponent, element, container, shouldReuseMarkup, context, callback) {
// ...
const nextWrappedElement = React.createElement(
ReactDOMBlockingRoot,
{ ...rootOptions, children: element },
null
);
const wrappedCallback = callback === undefined || callback === null ? null : () => {
if (this._internalRoot !== null) {
// Call the original callback here
callback();
}
};
const root = ReactDOM.createRoot(container, rootOptions);
// ...
const work = new ReactSyncRoot(root);
return work.perform(
work._render,
null,
nextWrappedElement,
this,
this._renderCallback,
wrappedCallback
);
}
function ReactDOMBlockingRoot(props) {
// ...
const [expirationTime, root] = ReactCurrentBatchConfig.getBlockingRootAndExecutionContext(
FiberRoot,
expirationTime
);
// ...
return createLegacyRoot(
container,
tag,
options,
expirationTime,
callback
);
}
const ReactDOM = {
// ...
createRoot(container, options) {
const hydrate = options != null && options.hydrate === true;
return createLegacyRoot(container, ConcurrentRoot, options, hydrationExpirationTime, hydrate);
},
// ...
};
const root = ReactDOM.createRoot(container, options);
console.log(root);
使用 ReactDOM.createRoot 函数创建一个 React 根节点时,它实际上返回了一个对象,该对象代表了 React 应用程序的根。这个对象拥有一系列方法,比如 render,可以用来将组件渲染到 DOM 中。
ReactDOM.createRoot(container, options):这个函数创建了一个 React 根节点。container 参数表示将要渲染组件的 DOM 容器,options 参数是一个对象,可以包含配置选项。在这里,我们创建了一个根节点 root。
ReactDOMBlockingRoot(props):这是一个 React 组件,它接受一些属性(props)作为参数。在这个函数内部,我们获取了一个包含当前时间的 expirationTime,并根据该时间创建了一个 React 同步渲染的根节点(ReactSyncRoot)。这个函数实际上创建了一个新的 React 根节点。
ReactMount(parentComponent, element, container, shouldReuseMarkup, context, callback):这是一个内部函数,用来处理组件的挂载。它接受组件、元素、容器、标志等参数,并在内部创建了一个包含元素的 React 元素。然后,它创建了一个 ReactSyncRoot 实例 work,并调用 work.perform 方法来执行渲染任务。
ReactMount._renderSubtreeIntoContainer(parentComponent, wrappedElement, container, forceHydrate, callback):这个函数接受了一个父组件、包装后的元素、容器等参数,并负责将元素渲染到指定的容器中。
ReactDOMLegacyRoot(container, options):这是一个构造函数,它创建了一个 React 传统模式的根节点,并将其赋值给 this._internalRoot。这个函数实际上创建了一个新的 React 根节点。
legacyRenderSubtreeIntoContainer 函数在内部调用了 ReactMount._renderSubtreeIntoContainer 函数,将元素渲染到容器中。
最终,ReactDOM.createRoot(container, options) 创建了一个 React 根节点,然后通过一系列的函数调用和内部逻辑,将组件渲染到了指定的容器中。console.log(root) 用于将根节点的实例输出到控制台,以便开发者查看它的属性和方法。这种方式有助于开发者理解 React 的内部运作机制。
ReactDOM.render 函数的返回值是当前应用程序根组件的实例: 当你调用 ReactDOM.render(, document.getElementById('root')) 这样的代码时,ReactDOM.render 函数会将 元素渲染到 root 容器中,并返回 组件的实例。这个实例代表了整个应用程序的根。
组件实例是 React 应用程序运行时在内存中的一种临时状态: 在 React 应用程序运行时,每个组件在内存中都有一个对应的实例。这个实例包含了组件的状态(state)和属性(props)等信息。这些信息在组件的生命周期中可以随着用户操作或者其他事件的发生而发生改变。
组件实例的属性包括了自身类定义的属性以及继承于 React.Component 的属性: 在 React 中,组件实例的属性包括了组件自身定义的属性,比如在类组件中通过 this.state 定义的状态,以及继承自 React.Component 的属性,比如 this.props,this.context,this.setState 等。这些属性可以用来控制组件的渲染和行为。
所以,当 ReactDOM.render 函数执行成功后,返回的组件实例可以被用来操作、更新、或者监听组件的状态和属性的变化。这个实例代表了整个应用程序的根,是你与 React 应用程序交互的入口点。
组件实例的本质
在这里笔者先贴上Component部分的源码
// 源码位置:packages/react/src/ReactBaseClasses.js
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
// 部分属性定义在原型上
Component.prototype.setState = function (partialState, callback) {
// 执行setState时会先校验入参的类型是否正确,入参类型必须是object或function
(function () {
if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
{
throw ReactError(Error('setState(...): 参数类型必须是object或者function'));
}
}
})();
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
function Component(props, context, updater) { ... }:
这是一个 JavaScript 函数,用于创建 React 类组件的实例。在 React 内部,所有的类组件都是通过这个构造函数创建的。它接收三个参数:props(组件的属性),context(组件的上下文),updater(用于处理组件更新的对象)。在函数内部,它将这些参数赋值给组件实例的属性。其中,this.props 存储组件的属性,this.context 存储组件的上下文,this.refs 是一个空对象,this.updater 存储用于更新组件的对象,如果没有提供 updater,则默认为 ReactNoopUpdateQueue。
Component.prototype.setState = function (partialState, callback) { ... }:
这是定义在 Component 类的原型上的方法,即所有由 Component 构造出的组件实例都可以调用这个方法。setState 方法用于更新组件的状态。它接收两个参数:partialState(要更新的状态的部分)和 callback(在状态更新完成后执行的回调函数)。在方法内部,首先会检查 partialState 的类型是否是对象或函数,如果不是,将抛出错误。然后,它调用 this.updater.enqueueSetState 方法,将组件实例、要更新的状态和回调函数传递给 updater。这个方法的目的是将状态更新的任务放入队列,等待后续执行。
总的来说,这段源码描述了 React 类组件的基本结构和状态更新机制。Component 构造函数用于创建组件实例,而 setState 方法用于触发状态的更新。这种机制是 React 中实现组件的基础。
仔细体会这段源码,可以抽象为以下几点:
- 组件实例的属性初始化:在 Component 构造函数内部,this.props、this.context、this.refs 和 this.updater 被初始化。props 和 context 是外部传入的数据,refs 是用于引用 DOM 元素的对象(在这个例子中是空对象),而 updater 是一个用于处理组件更新的对象。这些属性在组件的整个生命周期内都可用。
- setState 方法:setState 是类组件中非常重要的方法之一。当我们调用 setState 时,React 会将更新任务放入更新队列。这个队列会在适当的时机被处理,触发组件的重新渲染。setState 接收一个状态的部分(可以是对象或函数),以及一个可选的回调函数。状态的部分会被合并到组件的当前状态中,而回调函数则会在状态更新后被调用。
- 参数类型检查:在 setState 方法内部,有一个匿名函数被用来检查传入的 partialState 是否为对象或函数。如果不是,会抛出一个错误,提示开发者 setState 的参数必须是对象或者函数。这是为了确保开发者使用 setState 时提供了正确的参数类型,避免潜在的错误。
- 更新机制的抽象:updater 对象被用于处理组件的更新。在这个例子中,默认的 updater 是 ReactNoopUpdateQueue。在实际的 React 内部,这个 updater 对象可能会被替换成不同的实现,以实现不同的更新策略(例如批量更新、异步更新等)。这种抽象机制使得 React 内部的更新逻辑更加灵活可控。
// 源码位置:packages/react-reconciler/src/ReactFiberBeginWork.js
function finishClassComponent(current$$1, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime) {
...
// instance.render()返回当前组件的元素
var nextChildren = instance.render();
...
// 开始执行协调算法,返回下一个 Fiber 结点
reconcileChildren(current$$1, workInProgress, nextChildren, renderExpirationTime);
...
// 使用组件实例值来记忆当前 Fiber 结点状态,可用于后续 diff
workInProgress.memoizedState = instance.state;
}
这段源码是 React 内部的 Fiber 架构中,用于处理 Class 组件(class components)的更新逻辑。
finishClassComponent 函数的目的:该函数负责完成 Class 组件的工作,包括调用组件的 render 方法,获取新的子元素(nextChildren),并且在内部协调算法的帮助下,与之前的渲染结果进行比较,找出需要更新的部分。
instance.render() 的调用:首先,finishClassComponent 函数会调用当前 Class 组件的 render 方法,这个方法返回了组件的元素树(React 元素)。nextChildren 变量保存了这个返回的新元素树。
协调算法的执行:接下来,reconcileChildren 函数被调用,传入了之前渲染的 Fiber 节点(current1 中保存的子元素和 nextChildren 中的新子元素,找出需要更新、删除或者添加的部分。这个过程确保了 React 应用的高效更新。
workInProgress.memoizedState 的更新:最后,finishClassComponent 函数会将当前 Class 组件实例的状态(instance.state)保存到当前 Fiber 节点的 memoizedState 属性中。这个属性用于后续的比较,以决定组件是否需要更新。
总结来说,这段源码表示了 React 中 Class 组件的核心处理逻辑。它涉及到了组件的 render 方法的调用、新旧元素的比较、以及状态的更新。这些步骤确保了组件的渲染和更新在内部得到了高效且准确的处理。
// 源码位置:packages/react-reconciler/src/ReactFiberCommitWork.js
// 调用commit完成后的生命周期函数
function commitLifeCycles(finishedRoot, current$$1, finishedWork, committedExpirationTime) {
// tag标识了当前Fiber节点的类型,包括FunctionComponent,ClassComponent,HostComponent等
switch (finishedWork.tag) {
...
case ClassComponent:
...
instance.componentDidMount();
...
}
}
// 调用组件被卸载前的生命周期函数
var callComponentWillUnmountWithTimer = function (current$$1, instance) {
...
instance.componentWillUnmount();
...
};
...
commitLifeCycles 函数:该函数在组件完成渲染或更新后被调用,用于执行组件的生命周期函数。在这个函数中,根据 finishedWork.tag 的不同类型(比如 ClassComponent、FunctionComponent 等),不同的生命周期函数会被调用。
在 ClassComponent 的情况下,例如在 componentDidMount 函数中,表示组件已经成功被挂载到 DOM 树中,这时可以执行一些需要在组件被挂载后立即执行的操作。
callComponentWillUnmountWithTimer 函数:该函数用于在组件被卸载(即从 DOM 树中移除)之前,调用组件的 componentWillUnmount 生命周期函数。在 React 组件被销毁前,该函数会执行清理工作和资源释放操作,确保组件被卸载时不会产生内存泄漏等问题。
这些函数的目的是确保 React 组件在不同的生命周期阶段得到适时的调用,从而保证组件在挂载、更新、卸载等过程中能够执行开发者定义的逻辑。这种机制是 React 组件生命周期的基础,通过它,开发者可以控制组件在不同生命周期阶段的行为,使得应用的状态和逻辑得以正确管理和处理。
// 源码位置:packages/react-reconciler/src/ReactFiberClassComponent.js
// 应用程序首次渲染时会为组件实例绑定更新器
function adoptClassInstance(workInProgress, instance) {
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
}
// 组件更新器
var classComponentUpdater = {
enqueueSetState: function (inst, payload, callback) {
// 创建更新对象
var update = createUpdate(expirationTime, suspenseConfig);
// 为更新对象赋值更新内容
update.payload = payload;
...
// 将更新对象加入更新队列
enqueueUpdate(fiber, update);
// 开始(更新)调度工作
scheduleWork(fiber, expirationTime);
...
}
...
}
adoptClassInstance 函数:在应用程序首次渲染组件实例时,会为该组件实例绑定一个更新器。这个函数负责将 React Class 组件的实例(instance)和对应的 Fiber 节点(workInProgress)关联起来,同时设置组件实例的 updater。在 React 中,updater 是负责处理组件状态变化、更新队列等的核心模块。
classComponentUpdater 对象:classComponentUpdater 是一个组件更新器对象。该对象包含了类组件中的 enqueueSetState 方法,它的作用是将状态更新的请求(payload)转化为一个更新对象(update),并将这个更新对象加入到组件的更新队列中。更新队列是一种数据结构,用来按照一定顺序保存待更新的组件和状态变更,确保在适当的时候进行更新。
enqueueSetState 函数内部,首先会创建一个更新对象,这个对象包含了状态的变化(payload)和过期时间(expirationTime),expirationTime 表示该更新的截止时间,决定了更新的优先级。
接着,将创建的更新对象加入到组件的更新队列中,这是通过 enqueueUpdate(fiber, update) 实现的,将更新请求放到了组件的待处理队列中。
最后,调用 scheduleWork(fiber, expirationTime) 开始(更新)调度工作,这意味着 React 会根据更新队列的内容,按照一定的优先级和策略,决定何时进行组件的状态更新和重新渲染。
这段源码的核心作用是处理 Class 组件的状态更新请求,它通过创建更新对象和加入更新队列的方式,实现了 React 中 Class 组件状态更新的机制,确保状态的变化能够被及时响应和渲染。
总结一下本小节,
组件实例是运行时状态:每当一个 React 组件被实例化,就会创建一个组件实例。这个实例持有组件的属性(props),上下文(context),以及内部状态(state)。组件实例是在应用程序运行时动态生成的,每个组件在页面上都有对应的组件实例。
- 渲染 React 元素:当应用程序首次渲染时,React 会调用组件实例的 render 方法。这个方法的返回值通常是一个 React 元素(通过 JSX 语法转换得到)。React 元素是 React 中的基本构建块,用于描述你希望在屏幕上看到的内容。
- 状态更新流程:当应用程序状态发生变化(比如用户触发了某个事件),组件实例的状态可能会发生改变。此时,组件实例会调用自身的更新器(updater),通常是 setState 方法,来请求状态的变化。这个请求被包装成一个更新对象,然后被加入到更新队列中,等待后续的处理。
- 生命周期函数的调用:在应用程序的不同阶段,React 会调用组件实例的生命周期函数,例如 componentDidMount(在组件被挂载到 DOM 后调用)或者 componentDidUpdate(在组件更新后调用)。这些生命周期函数提供了机会,让开发者在组件不同的生命周期阶段执行特定的操作,比如数据获取、订阅事件、或者清理工作。
总的来说,组件实例在 React 应用程序中扮演着非常关键的角色。它们是组件的具体实现,负责处理组件的数据、状态、生命周期和事件等。React 的整个渲染和更新过程都围绕着组件实例展开,确保应用程序的状态和界面保持同步。
生命周期函数的细节
首先讨论React 中组件的生命周期函数 getDerivedStateFromProps 和 componentDidUpdate 的用法。
// 使用getDerivedStateFromProps替换componentWillReceiveProps
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.isLogin !== prevState.isLogin) {
// 注意这里的写法
return {
isLogin: nextProps.isLogin,
};
}
return null;
}
componentDidUpdate(prevProps, prevState) {
if (!prevState.isLogin && this.props.isLogin) {
// 这里this.props已经是最新的props,prevState是上一版本的state
this.handleClose();
}
}
getDerivedStateFromProps 函数:
getDerivedStateFromProps 是一个静态方法(static method),它在组件每次渲染前被调用。
当父组件传递给当前组件的 props 发生变化时,getDerivedStateFromProps 会被触发。
在这段代码中,getDerivedStateFromProps 用于替代旧版的生命周期函数 componentWillReceiveProps,它的目的是根据新的 props 和之前的 state 来计算并返回一个新的 state 对象。
如果 nextProps.isLogin(即最新的 props 中的 isLogin 属性)与之前的 state 中的 isLogin 不相等,那么说明 isLogin 属性发生了变化,返回一个新的 state 对象,将 isLogin 更新为 nextProps.isLogin。如果没有变化,返回 null。
componentDidUpdate 函数:
componentDidUpdate 是一个生命周期函数,在组件完成更新后被调用。
在这段代码中,componentDidUpdate 被用于处理组件更新后的逻辑。它接受两个参数 prevProps 和 prevState,分别表示之前的 props 和 state。
如果之前的 state 中的 isLogin 是 false,而当前最新的 props 中的 isLogin 是 true,说明用户已经登录,那么调用 handleClose 方法,这个方法可能用于关闭当前组件的某些 UI 或触发其他操作。
总的来说,getDerivedStateFromProps 用于在 props 变化时更新组件的内部状态,而 componentDidUpdate 用于处理组件更新后的逻辑。 这两个生命周期函数在组件的生命周期中的不同阶段被触发,允许开发者在这些阶段执行特定的操作。
React v16.3 之后的版本将组件的更新逻辑划分得更为清晰:getDerivedStateFromProps 用于根据新的 props 更新组件的 state,而 componentDidUpdate 用于处理组件更新后的逻辑,这种划分使得代码更易维护、更易理解。
class ScrollingList extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
return (
// 这里可以访问更新前的DOM元素属性
return this.rootNode.scrollHeight
);
}
componentDidUpdate(prevProps, prevState, snapshot) {
// snapshot值是在getSnapshotBeforeUpdate函数中返回
if (snapshot !== null) {
const curScrollTop= this.rootNode.scrollTop;
this.rootNode.scrollTop = curScrollTop + (this.rootNode.scrollHeight - snapshot);
}
}
render() {
return (
<div>
{/* ...contents... */}
</div>
);
}
}
接下来讨论一下getSnapshotBeforeUpdate 和 componentDidUpdate 这两个生命周期函数的使用。以下是对这段代码的详细解释:
getSnapshotBeforeUpdate(prevProps, prevState):
getSnapshotBeforeUpdate 是 React 组件生命周期函数,在组件即将更新(即 render 被调用前)时被触发。
prevProps 和 prevState 分别代表组件更新前的 props 和 state。
这个函数的返回值将会传递给 componentDidUpdate 函数作为第三个参数 snapshot。
在这个函数中,可以访问到组件更新前的 DOM 元素属性,例如 this.rootNode.scrollHeight,它返回的是更新前的 DOM 元素的滚动高度。
componentDidUpdate(prevProps, prevState, snapshot):
componentDidUpdate 是 React 组件生命周期函数,当组件更新完成后被调用。
prevProps 和 prevState 分别代表组件更新前的 props 和 state。
snapshot 是由 getSnapshotBeforeUpdate 返回的值,代表了组件更新前的某个状态(在这个例子中是滚动高度)。
在这个函数中,可以根据 snapshot 的值进行一些 DOM 操作。在这个例子中,它计算了更新后的滚动位置,确保滚动位置保持不变(即用户在滚动前和滚动后看到的内容相同)。
render():
render 函数是 React 组件的渲染函数,返回组件的 JSX 结构。
在这个例子中,返回了一个 div 元素,它包含了组件的内容。
综上所述,这段代码中的 getSnapshotBeforeUpdate 函数用于获取组件更新前的 DOM 元素的属性(这里是滚动高度),然后这个值在 componentDidUpdate 函数中被用来计算滚动位置,确保用户在页面滚动前后看到的内容保持一致。