React当中创建更新的主要方式
- ReactDOM.render || hydrate
- 这两个API都是我们要把整个应用第一次进行渲染到我们的页面上面
- 能够展现出来我们整个应用的样子的一个过程
- 这是初次渲染
- setState
- 后续更新应用
- forceUpdate
- 后续更新应用
- replaceState
- 在后续被舍弃
关于 ReactDOM.render
1 )概述
- 它先要去创建一个
ReactRoot
,这是一个包含react它整个应用的一个最顶点的一个对象 - 之后是创建
FiberRoot
和RootFiber
- 第三步,就是要创建一个更新,创建更新之后,应用就可以进入到一个更新调度的阶段
- 在进入调度之后,不管是通过
setState
还是ReactDOM.render
- 它们后续的操作都是由调度器去管理的,跟我们实际调用的API就已经没有任何关系了
- 也就是后面都交给内部调度器来掌控全局,这块先不涉及调度相关内容
- 在进入调度之后,不管是通过
2 )Demo 示例
App.js
js
import React, { Component } from 'react'
import './App.css'
class List extends Component {
state = {
a: 1,
b: 2,
c: 3,
}
handleClick = () => {
this.setState(oldState => {
const { a, b, c } = oldState
return {
a: a * a,
b: b * b,
c: c * c,
}
})
}
render() {
const { a, b, c } = this.state
return [
<span key="a">{a}</span>,
<span key="b">{b}</span>,
<span key="c">{c}</span>,
<button key="button" onClick={this.handleClick}>
click me
</button>,
]
}
}
class Input extends Component {
state = {
name: 'wang',
}
handleChange = e => {
// 这里如果使用方法设置`state`
// 那么需要现在外面读取`e.target.value`
// 因为在React走完整个事件之后会重置event对象
// 以复用event对象,如果等到方法被调用的时候再读取`e.target.value`
// 那时`e.target`是`null`
this.setState({
name: e.target.value,
})
}
render() {
return (
<input
type="text"
style={{ color: 'red' }}
onChange={this.handleChange}
value={this.state.name}
/>
)
}
}
class App extends Component {
render() {
return (
<div className="main">
<Input />
<List />
</div>
)
}
}
export default App
index.js
js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './demos/lazy'
ReactDOM.render(<App />, document.getElementById('root'))
- 这个demo非常的简单, 它首先有一个App, 它 render 了两个子节点,一个是
Input
,一个是List
- 最终渲染应用
ReactDOM.render(<App />, document.getElementById('root'))
- 应用渲染出来之后,会挂载到这个root的Dom节点上面
这个 React App 小程序通过 ReactElement 形成一个树结构
tree
App
|
render() return
|
div
/ \
/ \
children[0] children[1]
/ \
/ \
/ \
Input List
| \
render() return render() return
| \
input (span span span button)
- 我们传入的App是一个class component,它调用render之后会得到一个div标签
- 这个div标签就是它的children, 也是它 return 的唯一一个节点
- 这个节点它有两个children,一个是Input组件,另外一个是List组件
- 这个Input组件它调用render之后会返回一个input,就是原生的input标签
- 这个List组件它调用render之后 return 的是一个数组
- 这个数组里面有三个span标签和一个button标签
- 这就是整个树结构,我们完全可以通过一些特定的方式去获取到它的相应的子节点,一层一层下来
3 )特别说明
ReactDOM.render(<App />, document.getElementById('root'))
这个写法中的<App />
- 实际上是调用
React.createElement
传递进去的是 App 这个类,但并没有去创建它的一个实例 - 这个时候,我们还什么东西都没有,因为我们只得到了一个ReactElement
- 最终我们要形成一个把页面渲染出来的过程是
ReactDOM.render
这个方法,它接下去要做的事情
4 )源码解析
-
ReactDOM.js 链接: https://github.com/facebook/react/blob/v16.6.0/packages/react-dom/src/client/ReactDOM.js
-
在react-dom 下面会有很多不同的包,比如 client, server, shared, 对应的就是渲染平台不一样
-
server是在 nodejs 平台进行渲染的它的一个工具包, 我们把精力放在 client 下面
-
在react-dom里面先找到定义的 ReactDOM 对象
jsconst ReactDOM: Object = { createPortal, findDOMNode( componentOrElement: Element | ?React$Component<any, any>, ): null | Element | Text { if (__DEV__) { let owner = (ReactCurrentOwner.current: any); if (owner !== null && owner.stateNode !== null) { const warnedAboutRefsInRender = owner.stateNode._warnedAboutRefsInRender; warningWithoutStack( warnedAboutRefsInRender, '%s is accessing findDOMNode inside its render(). ' + 'render() should be a pure function of props and state. It should ' + 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', getComponentName(owner.type) || 'A component', ); owner.stateNode._warnedAboutRefsInRender = true; } } if (componentOrElement == null) { return null; } if ((componentOrElement: any).nodeType === ELEMENT_NODE) { return (componentOrElement: any); } if (__DEV__) { return DOMRenderer.findHostInstanceWithWarning( componentOrElement, 'findDOMNode', ); } return DOMRenderer.findHostInstance(componentOrElement); }, hydrate(element: React$Node, container: DOMContainer, callback: ?Function) { // TODO: throw or warn if we couldn't hydrate? return legacyRenderSubtreeIntoContainer( null, element, container, true, callback, ); }, render( element: React$Element<any>, container: DOMContainer, callback: ?Function, ) { return legacyRenderSubtreeIntoContainer( null, element, container, false, callback, ); }, unstable_renderSubtreeIntoContainer( parentComponent: React$Component<any, any>, element: React$Element<any>, containerNode: DOMContainer, callback: ?Function, ) { invariant( parentComponent != null && ReactInstanceMap.has(parentComponent), 'parentComponent must be a valid React Component', ); return legacyRenderSubtreeIntoContainer( parentComponent, element, containerNode, false, callback, ); }, unmountComponentAtNode(container: DOMContainer) { invariant( isValidContainer(container), 'unmountComponentAtNode(...): Target container is not a DOM element.', ); if (container._reactRootContainer) { if (__DEV__) { const rootEl = getReactRootElementInContainer(container); const renderedByDifferentReact = rootEl && !ReactDOMComponentTree.getInstanceFromNode(rootEl); warningWithoutStack( !renderedByDifferentReact, "unmountComponentAtNode(): The node you're attempting to unmount " + 'was rendered by another copy of React.', ); } // Unmount should not be batched. DOMRenderer.unbatchedUpdates(() => { legacyRenderSubtreeIntoContainer(null, null, container, false, () => { container._reactRootContainer = null; }); }); // If you call unmountComponentAtNode twice in quick succession, you'll // get `true` twice. That's probably fine? return true; } else { if (__DEV__) { const rootEl = getReactRootElementInContainer(container); const hasNonRootReactChild = !!( rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl) ); // Check if the container itself is a React root node. const isContainerReactRoot = container.nodeType === ELEMENT_NODE && isValidContainer(container.parentNode) && !!container.parentNode._reactRootContainer; warningWithoutStack( !hasNonRootReactChild, "unmountComponentAtNode(): The node you're attempting to unmount " + 'was rendered by React and is not a top-level container. %s', isContainerReactRoot ? 'You may have accidentally passed in a React root node instead ' + 'of its container.' : 'Instead, have the parent component update its state and ' + 'rerender in order to remove this component.', ); } return false; } }, // Temporary alias since we already shipped React 16 RC with it. // TODO: remove in React 17. unstable_createPortal(...args) { if (!didWarnAboutUnstableCreatePortal) { didWarnAboutUnstableCreatePortal = true; lowPriorityWarning( false, 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + 'and will be removed in React 17+. Update your code to use ' + 'ReactDOM.createPortal() instead. It has the exact same API, ' + 'but without the "unstable_" prefix.', ); } return createPortal(...args); }, unstable_batchedUpdates: DOMRenderer.batchedUpdates, unstable_interactiveUpdates: DOMRenderer.interactiveUpdates, flushSync: DOMRenderer.flushSync, unstable_flushControlled: DOMRenderer.flushControlled, __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { // Keep in sync with ReactDOMUnstableNativeDependencies.js // and ReactTestUtils.js. This is an array for better minification. Events: [ ReactDOMComponentTree.getInstanceFromNode, ReactDOMComponentTree.getNodeFromInstance, ReactDOMComponentTree.getFiberCurrentPropsFromNode, EventPluginHub.injection.injectEventPluginsByName, EventPluginRegistry.eventNameDispatchConfigs, EventPropagators.accumulateTwoPhaseDispatches, EventPropagators.accumulateDirectDispatches, ReactControlledComponent.enqueueStateRestore, ReactControlledComponent.restoreStateIfNeeded, ReactDOMEventListener.dispatchEvent, EventPluginHub.runEventsInBatch, ], }, };
-
这个对象里面,有一个
render
方法,它接收3个参数- 一个是
element
,本质是 React$Element 对象 - 第二个是
container
,就是我们要挂载到哪个dom节点上面 - 第三个是
callback
, 就是说这个应用它渲染结束之后,它会调用这个callback
- 一个是
-
render
方法最终 return 了一个legacyRenderSubtreeIntoContainer
方法- 传入了
null
,element
,container
,false
,callback
四个方法 - 主要看下 第一个
null
和 第四个false
- 传入了
-
现在定位到
legacyRenderSubtreeIntoContainer
这个方法jsfunction legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, children: ReactNodeList, container: DOMContainer, forceHydrate: boolean, callback: ?Function, ) { // TODO: Ensure all entry points contain this check invariant( isValidContainer(container), 'Target container is not a DOM element.', ); if (__DEV__) { topLevelUpdateWarnings(container); } // TODO: Without `any` type, Flow says "Property cannot be accessed on any // member of intersection type." Whyyyyyy. let root: Root = (container._reactRootContainer: any); if (!root) { // Initial mount root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = DOMRenderer.getPublicRootInstance(root._internalRoot); originalCallback.call(instance); }; } // Initial mount should not be batched. DOMRenderer.unbatchedUpdates(() => { if (parentComponent != null) { root.legacy_renderSubtreeIntoContainer( parentComponent, children, callback, ); } else { root.render(children, callback); } }); } else { if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = DOMRenderer.getPublicRootInstance(root._internalRoot); originalCallback.call(instance); }; } // Update if (parentComponent != null) { root.legacy_renderSubtreeIntoContainer( parentComponent, children, callback, ); } else { root.render(children, callback); } } return DOMRenderer.getPublicRootInstance(root._internalRoot); }
-
传进来的第一个参数
null
它对应的是叫做parentComponent
这么一个参数 -
接着往下,它定义一个
root
, 即:let root: Root = (container._reactRootContainer: any);
- 获取是否有
_reactRootContainer
这个属性 - 正常来讲,一个普通的dom对象,肯定不会有这种属性在上面的
- 所以第一次渲染的时候,它肯定是不存在的
- 所以我们主要关心的就是下面if里面的这个条件满足的root不存在的情况
- 获取是否有
-
如果root不存在,则进行创建
js// Initial mount root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, );
-
这个方法
legacyCreateRootFromDOMContainer
我们也要注意,它接受两个参数container
: DOMContainerforceHydrate
: boolean- 我们在函数调用栈向上溯源,传进去的
forceHydrate
是一个false
,这是一开始就写死的 - 我们在最顶层对比,可知,在 626 行的
hydrate
方法的第四个参数传递的是true
- 因为
hydrate
跟render
方法本质是一样的,唯一的一个区别,就是是否会调和原来存在于这个dom节点 - 就是我们
container
里面的它的HTML的节点, 是否要复用这些节点 - 它主要是在有服务端渲染的情况下会使用
hydrate
这个API - 因为服务端渲染出来的情况,它里面的dom节点应该是跟客户端渲染的时候,第一次渲染,它得到的节点是一模一样的
- 这个时候如果可以复用这些dom节点,可以提高一定的性能
- 所以
hydrate
跟render
内部的唯一的区别就是传的第四个参数是true
或false
- 我们在函数调用栈向上溯源,传进去的
-
再回到
legacyCreateRootFromDOMContainer
这个函数jsfunction legacyCreateRootFromDOMContainer( container: DOMContainer, forceHydrate: boolean, ): Root { const shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // First clear any existing content. if (!shouldHydrate) { let warned = false; let rootSibling; while ((rootSibling = container.lastChild)) { if (__DEV__) { if ( !warned && rootSibling.nodeType === ELEMENT_NODE && (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME) ) { warned = true; warningWithoutStack( false, 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.', ); } } container.removeChild(rootSibling); } } if (__DEV__) { if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) { warnedAboutHydrateAPI = true; lowPriorityWarning( false, 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + 'will stop working in React v17. Replace the ReactDOM.render() call ' + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', ); } } // Legacy roots are not async by default. const isConcurrent = false; return new ReactRoot(container, isConcurrent, shouldHydrate); }
-
在
render
函数中,进入了这个函数,forceHydrate
参数的值就是false
-
这边定义了一个
shouldHydrate
来得到是否应该进行 Hydrate -
const shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
jsfunction shouldHydrateDueToLegacyHeuristic(container) { const rootElement = getReactRootElementInContainer(container); return !!( rootElement && rootElement.nodeType === ELEMENT_NODE && rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME) // 这里的 ROOT_ATTRIBUTE_NAME 是 'data-reactroot' 老版本服务端渲染,会在第一个节点上加上这个标识 ); } function getReactRootElementInContainer(container: any) { if (!container) { return null; } // 判断节点类型是否是 DOCUMENT_NODE 类型 if (container.nodeType === DOCUMENT_NODE) { return container.documentElement; } else { // 否则,返回第一个孩子节点 return container.firstChild; } }
-
通过判断有
rootElement
这个节点,并且它有这个ROOT_ATTRIBUTE_NAME
属性 -
来判断它是否需要进行一个合并: 老的html节点和我们客户端第一次渲染出来的应用的所有节点进行合并的一个过程
-
因为是跟服务端渲染相关的,跟整体的更新流程没有特别大的关系,所以就不是特别重要
-
再回到
shouldHydrate
在下面的一个if (!shouldHydrate) {}
判断中,没有服务端渲染,这里是false
是会进入判断的 -
执行了一个 while循环,也就是循环删除 container 下面的所有子节点
-
因为这些子节点里面的东西不是在我们整个reactApp 渲染出来之后,还可以用的节点
-
因为我们不需要去合并它,所以就把这些节点全部删了
-
忽略下面 DEV 的判断,最终返回了一个 ReactRoot:
return new ReactRoot(container, isConcurrent, shouldHydrate);
-
-
接下来,进入
new ReactRoot
的过程jsfunction ReactRoot( container: Container, isConcurrent: boolean, hydrate: boolean, ) { const root = DOMRenderer.createContainer(container, isConcurrent, hydrate); this._internalRoot = root; }
- 通过
const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
创建了一个 root 节点 - 而
DOMRenderer
是在import * as DOMRenderer from 'react-reconciler/inline.dom';
-
在 react-reconciler 这个包中的函数
-
react-reconciler 是在 react 中非常重要的一个模块
-
它处理和平台无关的节点的调和操作和任务调度的操作
-
react-reconciler 中的代码比 react-dom中的还要复杂
-
在 react-reconciler/inline.dom 文件下只有一行代码
export * from './src/ReactFiberReconciler';
-
打开这个 js 文件,找到
createContainer
jsexport function createContainer( containerInfo: Container, isConcurrent: boolean, hydrate: boolean, ): OpaqueRoot { return createFiberRoot(containerInfo, isConcurrent, hydrate); }
-
最终它创建了一个 FiberRoot, 这里先不展开
-
- 回到
ReactRoot
, 它这边挂载了一个 _internalRoot,this._internalRoot = root;
- 通过
-
退回到
legacyCreateRootFromDOMContainer
它最终返回了一个ReactRoot
-
再退回到调用
legacyCreateRootFromDOMContainer
的legacyRenderSubtreeIntoContainer
函数中-
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
-
接下来,判断是否有
callback
, 没有则对其进行简单的封装处理jsif (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = DOMRenderer.getPublicRootInstance(root._internalRoot); originalCallback.call(instance); }; }
-
接着进入核心环节
DOMRenderer.unbatchedUpdates
js// Initial mount should not be batched. DOMRenderer.unbatchedUpdates(() => { if (parentComponent != null) { root.legacy_renderSubtreeIntoContainer( parentComponent, children, callback, ); } else { root.render(children, callback); } });
-
unbatchedUpdates
涉及到 react 中的一个概念batchedUpdates
批量更新,这里先跳过- 可以理解为 改了 scheduler 里的一个全局变量,可以暂时忽略,这里涉及到更新的过程
-
然后
unbatchedUpdates
里面的回调直接被执行,里面直接走 else,也就是执行root.render(children, callback);
-
这里的
root.render
方法, 实际上是jsReactRoot.prototype.render = function( children: ReactNodeList, callback: ?() => mixed, ): Work { const root = this._internalRoot; const work = new ReactWork(); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'render'); } if (callback !== null) { work.then(callback); } DOMRenderer.updateContainer(children, root, null, work._onCommit); return work; };
-
这里创建了一个
ReactWork
, 最终调用了DOMRenderer.updateContainer
-
这里的
updateContainer
是在 ReactFiberReconciler.js 中的jsexport function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, callback: ?Function, ): ExpirationTime { const current = container.current; const currentTime = requestCurrentTime(); const expirationTime = computeExpirationForFiber(currentTime, current); return updateContainerAtExpirationTime( element, container, parentComponent, expirationTime, callback, ); }
-
这里的第一个参数
element
是上层ReactRoot.prototype.render
的第一个参数,还可以向上继续溯源 -
这里最核心的是, 计算 expirationTime, 这里是React 16让我们使用
ConcurrentMode
进行一个优先级的任务更新 -
这里
computeExpirationForFiber
涉及一个非常复杂的计算过程,先跳过 -
最后调用
updateContainerAtExpirationTime
来返回结果,进入这个方法jsexport function updateContainerAtExpirationTime( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, expirationTime: ExpirationTime, callback: ?Function, ) { // TODO: If this is a nested container, this won't be the root. const current = container.current; if (__DEV__) { if (ReactFiberInstrumentation.debugTool) { if (current.alternate === null) { ReactFiberInstrumentation.debugTool.onMountContainer(container); } else if (element === null) { ReactFiberInstrumentation.debugTool.onUnmountContainer(container); } else { ReactFiberInstrumentation.debugTool.onUpdateContainer(container); } } } const context = getContextForSubtree(parentComponent); if (container.context === null) { container.context = context; } else { container.pendingContext = context; } return scheduleRootUpdate(current, element, expirationTime, callback); }
-
它获取了一个 context,
const context = getContextForSubtree(parentComponent);
这个先忽略,因为 parentComponent 是 null -
下面的 if else 先忽略,简单认为
container.context
和container.pendingContext
都不存在 -
在 react-dom 的 api 中没有任何方法可以在root节点上提供context的入口,先忽略它们
-
最终
scheduleRootUpdate
作为返回值,进入这个方法jsfunction scheduleRootUpdate( current: Fiber, element: ReactNodeList, expirationTime: ExpirationTime, callback: ?Function, ) { if (__DEV__) { if ( ReactCurrentFiber.phase === 'render' && ReactCurrentFiber.current !== null && !didWarnAboutNestedUpdates ) { didWarnAboutNestedUpdates = true; warningWithoutStack( false, 'Render methods should be a pure function of props and state; ' + 'triggering nested component updates from render is not allowed. ' + 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + 'Check the render method of %s.', getComponentName(ReactCurrentFiber.current.type) || 'Unknown', ); } } const update = createUpdate(expirationTime); // Caution: React DevTools currently depends on this property // being called "element". update.payload = {element}; callback = callback === undefined ? null : callback; if (callback !== null) { warningWithoutStack( typeof callback === 'function', 'render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback, ); update.callback = callback; } enqueueUpdate(current, update); scheduleWork(current, expirationTime); return expirationTime; }
- 跳过里面的 DEV 判断的代码,它创建了一个 update
const update = createUpdate(expirationTime);
- update 是用来标记 react 应用当中需要更新的地点的
- 接着设置 update 的一些属性,如:payload, callback
- 最后调用
enqueueUpdate
, 是把 update 加入到我们这个Fiber对象上面对应的updateQueue
里面 - update它是可以在一次更新当中,这个节点上面有多个更新的,
- 就是一个整体的react应用的更新过程当中会有很多次更新在某一个节点上产生
- 这跟
batchUpdates
是有一定的关系的 - 最终调用
scheduleWork
就是开始任务调度,告诉 react 有更新产生了,要进行更新了,也要开始调度了- 为何要调度?
- react 16之后,提供了一个任务优先级的概念, 因为有可能在同一时间, 有各种优先级的任务在应用里面
- 就需要有个调度器来进行指挥调度,按照优先级,先执行优先级高的任务,再执行优先级低的任务
- 这才是react更新中,最复杂的逻辑
- 跳过里面的 DEV 判断的代码,它创建了一个 update
-
-
简单总结
- 在
ReactDOM.render
过程当中,创建了一个ReactRoot
- 同时在
ReactRoot
创建的过程中创建了FiberRoot
FiberRoot
在创建的过程中也会自动去初始化一个 Fiber 对象(上面暂没有涉及)- 然后又在这个 root 上面去创建了一个
expirationTime
- 之后又创建了一个 update 这个更新的对象,然后把这个更新的对象放到我们的 root 的节点上面
- 之后就进入了一个更新的过程, 这就是一个创建更新的过程
- 创建完更新, 再去实际的调度整个任务的更新