1 )概述
ReactRoot
,这是一个包含react它整个应用的一个最顶点的一个对象FiberRoot
和 RootFiber
setState
还是 ReactDOM.render
2 )Demo 示例
App.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
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './demos/lazy'
ReactDOM.render(<App />, document.getElementById('root'))
Input
,一个是 List
ReactDOM.render( , document.getElementById('root'))
这个 React App 小程序通过 ReactElement 形成一个树结构
App
|
render() return
|
div
/ \
/ \
children[0] children[1]
/ \
/ \
/ \
Input List
| \
render() return render() return
| \
input (span span span button)
3 )特别说明
ReactDOM.render( , document.getElementById('root'))
这个写法中的
React.createElement
传递进去的是 App 这个类,但并没有去创建它的一个实例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 对象
const 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
, 就是说这个应用它渲染结束之后,它会调用这个callbackrender
方法最终 return 了一个 legacyRenderSubtreeIntoContainer
方法
null
, element
, container
, false
, callback
四个方法null
和 第四个 false
现在定位到 legacyRenderSubtreeIntoContainer
这个方法
function 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
这个属性如果root不存在,则进行创建
// Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
这个方法 legacyCreateRootFromDOMContainer
我们也要注意,它接受两个参数
container
: DOMContainerforceHydrate
: boolean
forceHydrate
是一个 false
,这是一开始就写死的hydrate
方法的第四个参数传递的是 true
hydrate
跟render
方法本质是一样的,唯一的一个区别,就是是否会调和原来存在于这个dom节点container
里面的它的HTML的节点, 是否要复用这些节点hydrate
这个APIhydrate
跟render
内部的唯一的区别就是传的第四个参数是true
或false
再回到 legacyCreateRootFromDOMContainer
这个函数
function 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
来得到是否应该进行 Hydrateconst shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
function 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
属性shouldHydrate
在下面的一个 if (!shouldHydrate) {}
判断中,没有服务端渲染,这里是false
是会进入判断的return new ReactRoot(container, isConcurrent, shouldHydrate);
接下来,进入 new ReactRoot
的过程
function 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';
export * from './src/ReactFiberReconciler';
createContainer
export function createContainer(
containerInfo: Container,
isConcurrent: boolean,
hydrate: boolean,
): OpaqueRoot {
return createFiberRoot(containerInfo, isConcurrent, hydrate);
}
ReactRoot
, 它这边挂载了一个 _internalRoot, this._internalRoot = root;
退回到 legacyCreateRootFromDOMContainer
它最终返回了一个 ReactRoot
再退回到调用 legacyCreateRootFromDOMContainer
的 legacyRenderSubtreeIntoContainer
函数中
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
callback
, 没有则对其进行简单的封装处理if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
originalCallback.call(instance);
};
}
DOMRenderer.unbatchedUpdates
// 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
批量更新,这里先跳过
unbatchedUpdates
里面的回调直接被执行,里面直接走 else,也就是执行 root.render(children, callback);
root.render
方法, 实际上是ReactRoot.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 中的export 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
的第一个参数,还可以向上继续溯源ConcurrentMode
进行一个优先级的任务更新computeExpirationForFiber
涉及一个非常复杂的计算过程,先跳过updateContainerAtExpirationTime
来返回结果,进入这个方法export 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);
}
const context = getContextForSubtree(parentComponent);
这个先忽略,因为 parentComponent 是 nullcontainer.context
和 container.pendingContext
都不存在scheduleRootUpdate
作为返回值,进入这个方法function 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;
}
const update = createUpdate(expirationTime);
enqueueUpdate
, 是把 update 加入到我们这个Fiber对象上面对应的 updateQueue
里面batchUpdates
是有一定的关系的scheduleWork
就是开始任务调度,告诉 react 有更新产生了,要进行更新了,也要开始调度了
ReactDOM.render
过程当中,创建了一个 ReactRoot
ReactRoot
创建的过程中创建了 FiberRoot
FiberRoot
在创建的过程中也会自动去初始化一个 Fiber 对象(上面暂没有涉及)expirationTime