React源码 ReactDOM.render
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: 'jokcy', } 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
这个 demo 非常简单,一个 Input 一个 List ,Input 里面有个 state ,属性有 name,然后 render 里面有个 input 标签,这个标签,输入的时候去改变这个 name 。
import React from 'react' import ReactDOM from 'react-dom' import './index.css' import App from './App'; ReactDOM.render(<App />, document.getElementById('root'))
我们这里传入的是 <App /> ,实际上我们传入的是 React.createElement。我们传进去的是 App 这个类,但是并没有创建这个实例,这个时候还什么东西都没有,因为我们只得到了一个 createElement,最终要形成一个页面渲染出来的过程,这就是 ReactDOM.render 要做的事情,然后看下这个源码,打开 ReactDOM.js。 react-dom 下面有不同的包,有 client , 有 server . 还有 shared , shared 是在 cilent 和 server 里面都会用到的。对应的就是渲染的平台不一样,server 就是在 Nodejs 下面进行渲染的一个工具包,我们主要了解的是 client 下面的东西。在 ReactDOM.js 下面,我们先找到定义 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, ], }, };
这段就是定义 ReactDOM 的地方。ReactDOM 是个对象,这个对象里面有 render 方法。这个 render 方法我们可以看到他接收的有三个参数。一个是 element ,就是 react element . 第二个是 container ,就是我们要挂载到哪一个节点上面。第三个是 callback ,callback 就是说这个应用渲染结束之后,他会调用这个 callback 。
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); }
这个就是 legacyRenderSubtreeIntoContainer 这个方法,第一个参数 null ,他对应的是 parentComponent 这个参数
let root: Root = (container._reactRootContainer: any);
然后下面他获取了 root 这么一个变量,他通过 container , container 就是 DOMContainer ,就是 ReactDOM.render 里面的第二个参数,就是挂载的节点,它上面有没有 _reactRootContainer 这个属性,第一次渲染的时候,这个属性值肯定是没有的,接下来没有的话,他就会去创建这个属性值,通过调用 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); }
我们看到这个方法,他接收两个参数,第一个是 DOMContainer , 第二个是 forceHydrate ,这个 forceHydrate 就是传入的第四个参数,也就是 render 方法里面的第四个参数,是 false , render 方法里面是写死的,是不是代表着永远都是 false 呢,这个主要对应的是另外一种情况,就是 hydrate 这个方法,hydrate 跟 render 本质是一样的,唯一的一个区别就是是否会调和 container 里面的 html 节点,是否要复用这些节点。主要是有服务端渲染的时候会使用 hydrate 这个 api。因为服务端渲染 和 客户端渲染 第一次得到的节点是一样的。这个时候如果可以复用这些节点,可以提高一定的性能。所以他两唯一的区别就是第四个参数是 true 还是 false 。
function shouldHydrateDueToLegacyHeuristic(container) { const rootElement = getReactRootElementInContainer(container); return !!( rootElement && rootElement.nodeType === ELEMENT_NODE && rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME) ); }
这个方法里面 return 的是 rootElement 。rootElement 是通过调用 getReactRootElementInContainer 得到的
function getReactRootElementInContainer(container: any) { if (!container) { return null; } if (container.nodeType === DOCUMENT_NODE) { return container.documentElement; } else { return container.firstChild; } }
这个方法先判断一下这个 container 是否存在,如果不存在, return,如果存在,再继续判断这个节点是否等于 DOCUMENT_NODE ,就是 document ,就是 window.document ,是否是等于的。如果是等于,就直接返回这个 document 。 如果不是等于,就返回这个 container 的子节点。再回到 shouldHydrateDueToLegacyHeuristic ,判断完他是否是个节点,而且是 ROOT_ATTRIBUTE_NAME,然后返回,这个不是特点重要,跟 react 整体更新没有多大关系,再回到 legacyCreateRootFromDOMContainer 。当这个节点是 false 的情况下,执行了一个 for 循环,这个 for 循环是干嘛的呢?就是把 container 传进来的所有子节点都删掉,因为我们认为 shouldHydrate 为 false 的情况下,在渲染出来后,这些子节点已经不能用了,因为不需要合并他。最后return 一个 new ReactRoot。他传进去的是 container, isConcurrent, shouldHydrate 。 isConcurrent 我们看到直接是 false,已经注明了 root 节点不应该是 async 的。接下来我们看一下 new 一个 root 的过程
function ReactRoot( container: Container, isConcurrent: boolean, hydrate: boolean, ) { const root = DOMRenderer.createContainer(container, isConcurrent, hydrate); this._internalRoot = root; } 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; }; ReactRoot.prototype.unmount = function(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(null, root, null, work._onCommit); return work; }; ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function( parentComponent: ?React$Component<any, any>, 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, parentComponent, work._onCommit); return work; }; ReactRoot.prototype.createBatch = function(): Batch { const batch = new ReactBatch(this); const expirationTime = batch._expirationTime; const internalRoot = this._internalRoot; const firstBatch = internalRoot.firstBatch; if (firstBatch === null) { internalRoot.firstBatch = batch; batch._next = null; } else { // Insert sorted by expiration time then insertion order let insertAfter = null; let insertBefore = firstBatch; while ( insertBefore !== null && insertBefore._expirationTime <= expirationTime ) { insertAfter = insertBefore; insertBefore = insertBefore._next; } batch._next = insertBefore; if (insertAfter !== null) { insertAfter._next = batch; } } return batch; };
ReactRoot 方法里面通过 DOMRenderer.createContainer(container, isConcurrent, hydrate) 创建了 root 节点。那么 DOMRenderer 是什么东西呢?
import * as DOMRenderer from 'react-reconciler/inline.dom';
DOMRenderer 其实就是 react-reconciler 下面的 inline.dom 。 inline.dom 里面只引用了 ReactFiberReconciler.js 。打开这个js 之后,找到 createContainer。
export function createContainer( containerInfo: Container, isConcurrent: boolean, hydrate: boolean, ): OpaqueRoot { return createFiberRoot(containerInfo, isConcurrent, hydrate); }
发现这个方法里面创建了一个 createFiberRoot 。返回到 ReactRoot ,创建完之赋值给 this._internalRoot。然后再赋值给了 container._reactRootContainer ,最后,调用到了 root.render,就是原型链上的 render 方法。这里创建了一个 reactWork,这个不是特别重要,就跳过了。最终调用了 DOMRenderer.updateContainer。
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, ); }
updateContainer 他接收了 element,container,parentComponent。这个方法里面有三个变量。一个 current, 一个 currentTime。一个 expirationTime 。 expirationTime 是通过computeExpirationForFiber 获得的, expirationTime是非常重要的。然后调用 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); }
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; }
这里面创建了 createUpdate。update 是用来标记,react 中需要标记更新的地点的,这个方法就是设置了一些update 相关的属性。最后调用 enqueueUpdate 。后面调用了 scheduleWork ,调度,为什么有调度呢,因为有任务优先级的概念。