React源码 ReactDOM.render

在 react 当中,主要创建更新的有三种方式
1、ReactDOM.render || hydrate 
这两个api都是要把这个应用第一次渲染到我们页面上面,展现出来整个应用的样子的过程,这就是初始渲染
2、setState
3、forceUpdate

 

ReactDOM.render 需要做哪些事情呢?
1、创建 ReactRoot
2、创建 FiberRoot 和 RootFiber
3、创建更新

 

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 。

 

第二个组件是 List ,这个 List 里面有三个 span ,三个 span 里面分别显示 state 的属性 a,b,c ,然后点击 click ,a,b,c 以乘以自己的方式改变值。这就是这个 demo 的样子。 然后我们看一下最终要渲染这个应用,会调用 ReactDOM.render ,然后传入这个 App ,第二个是这个应用会挂载到的节点上

 

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 。

 

然后里面直接调用了一个方法叫 legacyRenderSubtreeIntoContainer,然后传入了 null ,element,container,false,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 。

 

我们再回到 legacyCreateRootFromDOMContainer ,对于 render 来说,这个 forceHydrate 是 false。那么如果是 false 的情况,会去调用这个方法 shouldHydrateDueToLegacyHeuristic。

 

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);
}

 

 

这里的 context 是没有的,最后调用了 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;
}

这里面创建了 createUpdate。update 是用来标记,react 中需要标记更新的地点的,这个方法就是设置了一些update 相关的属性。最后调用 enqueueUpdate 。后面调用了 scheduleWork ,调度,为什么有调度呢,因为有任务优先级的概念。

 

posted @ 2019-12-01 15:02  wzndkj  阅读(656)  评论(0编辑  收藏  举报