全栈猎人
首发于全栈猎人

深入理解React源码 - 首次渲染(简单组件) I

Photo by Gerrie van der Walt on Unsplash

本文也同时发表在我的博客HACKERNOON

深入理解React源码 - 首次渲染 I (本篇)

深入理解React源码 - 首次渲染 II

深入理解React源码 - 首次渲染 III

深入理解React源码 - 首次渲染 IV

深入理解React源码 - 首次渲染 V


界面更新本质上就是数据的变化。通过把所有会动的东西收敛到状态(state),React提供了一个非常直观的前端框架。我也比较喜欢review基于React代码,因为我一般都是从数据结构开始看,这样可以在钻到细节代码之前建立对整个逻辑的初步理解。我也经常会好奇React的实现方式,然后就有了这篇文章。

我一直认为项目的可控离不开对底层库的理解。熟悉底层实现以后,无论是魔改,贡献代码,还是日常升级都能更加得心应手。

这篇会通过渲染一个简单的组件来打通React的一条关键路径。(组合组件,界面更新等其他主题会在后续文章中讨论)

本文用到的文件:

isomorphic/React.js: ReactElement.createElement()的入口

isomorphic/classic/element/ReactElement.js:ReactElement.createElement()的具体实现

renderers/dom/ReactDOM.js: ReactDOM.render()的入口

renderers/dom/client/ReactMount.js: ReactDom.render()的具体实现

renderers/shared/stack/reconciler/instantiateReactComponent.js: 基于元素类型创建组件 (ReactComponents)

renderers/shared/stack/reconciler/ReactCompositeComponent.js: 顶级元素的ReactComponents 包装

调用栈里用到的标签

- 函数调用

= 别名

~ 间接调用

由于React对组件进行了扁平化处理,文件的位置不太容易从import语句中看到,所以我会用@标签在代码块中标注其对应的文件路径。

本文基于React 15.6.2

从JSX到React.createElement()

JSX是在编译的时候由Babel转译成React.createElement()调用的。举例来说,create-react-app 自带的App.js:

import React, { Component } from ‘react’;
import logo from ‘./logo.svg’;
import ‘./App.css’;
class App extends Component {
  render() {
    return (
      <div className=”App”>
        <header className=”App-header”>
          <img src={logo} className=”App-logo” alt=”logo” />
          <h1 className=”App-title”>Welcome to React</h1>
        </header>
        <p className=”App-intro”>
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}
export default App;

会被转译成:

import React, { Component } from ‘react’;
import logo from ‘./logo.svg’;
import ‘./App.css’;
class App extends Component {
  render() {
    return React.createElement(
      ‘div’,
      { className: ‘App’ },
      React.createElement(
        ‘header’,
        { className: ‘App-header’ },
        React.createElement(‘img’, { src: logo, className: ‘App-logo’, alt: ‘logo’ }),
        React.createElement(
          ‘h1’,
          { className: ‘App-title’ },
          ‘Welcome to React’
        )
      ),
      React.createElement(
        ‘p’,
        { className: ‘App-intro’ },
        ‘To get started, edit ‘,
        React.createElement(
          ‘code’,
          null,
          ‘src/App.js’
        ),
        ‘ and save to reload.’
      )
    );
  }
}
export default App;

然后这个函数返回的ReactElement 会在应用层的"index.js"渲染:

ReactDOM.render(
  <App />,
  document.getElementById(‘root’)
);

(这个过程应该都知道了)

上面的这个组件树对于入门来说有点复杂了,所以最好先从简单一点的?开始来撬开React的实现。

…
ReactDOM.render(
  <h1 style={{“color”:”blue”}}>hello world</h1>,
  document.getElementById(‘root’)
);
…

转译后:

…
ReactDOM.render(React.createElement(
  ‘h1’,
  { style: { “color”: “blue” } },
  ‘hello world’
), document.getElementById(‘root’));
…

React.createElement() - 创建一个 ReactElement

第一步其实没做啥。仅仅是实例化一个ReactElement,再用传入的参数初始化它。这一步的目标结构是:

这一步的调用栈:

React.createElement
|=ReactElement.createElement(type, config, children)
   |-ReactElement(type,…, props)

1. React.createElement(type, config, children) 仅仅是 ReactElement.createElement()的一个别名;

…
var createElement = ReactElement.createElement;
…
var React = {
…
  createElement: createElement,
…
};
module.exports = React;

React@isomorphic/React.js

2. ReactElement.createElement(type, config, children) 做了三件事: 1) 把 config里的数据一项一项拷入props, 2) 拷贝 childrenprops.children, 3) 拷贝 type.defaultPropsprops;

…
  // 1)
  if (config != null) {
    …extracting not interesting properties from config…
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }
 // 2)
  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  var childrenLength = arguments.length — 2;
  if (childrenLength === 1) {
    props.children = children; // scr: one child is stored as object
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2]; // scr: multiple children are stored as array
    }
   props.children = childArray;
  }
 // 3)
  // Resolve default props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
 return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
…

ReactElement.createElement@isomorphic/classic/element/ReactElement.js

3. 然后 ReactElement(type,…, props) 会把 typeprops 原样透传给 ReactElement 的构造函数,并返回新构造的实例.

…
var ReactElement = function(type, key, ref, self, source, owner, props) {
  // This tag allow us to uniquely identify this as a React Element
  $$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
  type: // scr: --------------> ‘h1’
  key:  // scr: --------------> not of interest for now
  ref:  // scr: --------------> not of interest for now
  props: {
    children:     // scr: --------------> ‘hello world’
    …other props: // scr: --------------> style: { “color”: “blue” }
  },
// Record the component responsible for creating this element.
  _owner:  // scr: --------------> null
};
…

ReactElement@isomorphic/classic/element/ReactElement.js

这个新构建的ReactElement一会会在ReactMount.instantiateReactComponent() 函数中用到。因为下一步也会构建一个ReactElement我们先把这一步生成的对象命名为ReactElement[1]

ReactDom.render() -  开始渲染

_renderSubtreeIntoContainer()  - 给ReactElement[1] 加上TopLevelWrapper

下一步的目标是把ReactElement[1]包装到另外一个ReactElement,(我们叫它[2]吧),然后把ReactElement.type赋值为TopLevelWrapper。这个TopLevelWrapper的名字很能说明问题了-(传入render()函数的)顶级元素的包装:

这里的TopLevelWrapper定义很重要,所以我在这里打上三个星号***,方便你以后会到这篇文章时搜索。

…
var TopLevelWrapper = function() {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
TopLevelWrapper.prototype.render = function() { 
// scr: this function will be used to strip the wrapper later in the // rendering process
 return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;
…

TopLevelWrapper@renderers/dom/client/ReactMount.js 

废话一句,传入ReactElement.type的是一个类型(TopLevelWrapper)。这个类型会在接下来的渲染过程中被实例化。而render()函数则是用于提取包含在this.props.childReactElement[1]

这一步的调用栈:

ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer(
   parentComponent, // scr: --------------> null
   nextElement,     // scr: -------------->  ReactElement[1]
   container,// scr: --------------> document.getElementById(‘root’)
   callback’ // scr: --------------> undefined
)

对于首次渲染,ReactMount._renderSubtreeIntoContainer()其实比它看起来简单很多,因为大部分的分支都被跳过了。这个阶段函数中唯一有效的代码是:

…
  var nextWrappedElement = React.createElement(TopLevelWrapper, {
    child: nextElement,
  });
…

_renderSubtreeIntoContainer@renderers/dom/client/ReactMount.js 

我们刚看过React.createElement(),这一步的构建过程应该很好理解。这里就不赘述了。

instantiateReactComponent()  -  用 ReactElement[2]创建一个 ReactCompositeComponent

这一步会为顶级组件创建一个初始的ReactCompositeComponent

调用栈:

ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer()
  |-ReactMount._renderNewRootComponent(
      nextWrappedElement, // scr: ------> ReactElement[2]
      container, // scr: ------> document.getElementById(‘root’)
      shouldReuseMarkup, // scr: null from ReactDom.render()
      nextContext, // scr: emptyObject from ReactDom.render()
    )
    |-instantiateReactComponent(
        node, // scr: ------> ReactElement[2]
        shouldHaveDebugID /* false */
      )
      |-ReactCompositeComponentWrapper(
          element // scr: ------> ReactElement[2]
      );
      |=ReactCompositeComponent.construct(element)

instantiateReactComponent是唯一一个比较复杂的函数。在这次的上下文中,这个函数会根据这个字段ReactElement[2].type 的值(TopLevelWrapper),然后创建一个ReactCompositeComponent

function instantiateReactComponent(node, shouldHaveDebugID) {
  var instance;
…
  } else if (typeof node === ‘object’) {
    var element = node;
    var type = element.type;
…
   // Special case string values
    if (typeof element.type === ‘string’) {
…
    } else if (isInternalComponentType(element.type)) {
…
    } else {
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === ‘string’ || typeof node === ‘number’) {
…
  } else {
…
  }
…
  return instance;
}

instantiateReactComponent@renderers/shared/stack/reconciler/instantiateReactComponent.js 

这里比较值得注意的是new ReactCompositeComponentWrapper()

…
// To avoid a cyclic dependency, we create the final class in this module
var ReactCompositeComponentWrapper = function(element) {
  this.construct(element);
};
…
…
Object.assign(
  ReactCompositeComponentWrapper.prototype,
  ReactCompositeComponent,
  {
    _instantiateReactComponent: instantiateReactComponent,
  },
);
…

ReactCompositeComponentWrapper@renderers/shared/stack/reconciler/instantiateReactComponent.js 

实际会直接调用ReactCompositeComponent的构造函数:

construct: function(element /* scr: ------> ReactElement[2] */) {
  this._currentElement = element;
  this._rootNodeID = 0;
  this._compositeType = null;
  this._instance = null;
  this._hostParent = null;
  this._hostContainerInfo = null;
// See ReactUpdateQueue
  this._updateBatchNumber = null;
  this._pendingElement = null;
  this._pendingStateQueue = null;
  this._pendingReplaceState = false;
  this._pendingForceUpdate = false;
this._renderedNodeType = null;
  this._renderedComponent = null;
  this._context = null;
  this._mountOrder = 0;
  this._topLevelWrapper = null;
// See ReactUpdates and ReactUpdateQueue.
  this._pendingCallbacks = null;
// ComponentWillUnmount shall only be called once
  this._calledComponentWillUnmount = false;
},

ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

在后续的步骤里ReactCompositeComponent还会被instantiateReactComponent()创建, 所以我们把这一步生成的对象命名为ReactCompositeComponent[T] (T 代表 top)。

ReactCompositeComponent[T] 创建以后, 下一步React会调用 batchedMountComponentIntoNode, 来初始化这个组件对象,然后渲染它并插入DOM树中。 这个过程留到下篇讨论。


今天先写到这。如果您觉得这篇不错,可以点赞或关注这个专栏。

感谢阅读!?


Originally published at

Understanding The React Source Code - Initial Rendering (Simple Component) Iholmeshe.me

.

文章被以下专栏收录