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

1,174 阅读7分钟
原文链接: zhuanlan.zhihu.com

Photo by Joshua Sortino on Unsplash

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

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

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

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

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

咱们在上几篇讲完了简单组件的渲染过程。这次我们用一个自定义组件(我们平时开发用的那种)来探索渲染流程的更多支线。

本篇涉及的文件:第一篇还有第二篇一样

如果本篇涉及的函数和代码在之前详细讨论过,我会用{}来做引用

App非常像我在最开始给出来的的那个组件,我们上次觉得这个组件太难了(所以转头讨论的简单组件渲染)。但经历完前面几次的代码阅读后我们也升了不少级。现在,对于我们来说这个原先的boss变成小怪也就难免了。

import React, { Component } from ‘react’;
import logo from ‘./logo.svg’;
import ‘./App.css’;

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      desc: 'start',
    };
  }

render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src="main.jpg" className="App-logo" alt="logo" />
          <h1> "Welcom to React" </h1>
        </div>
        <p className="App-intro">
          { this.state.desc }
        </p>
      </div>
    );
  }
}

export default App;

App@App.js

之前提到过,这个组件会被这么来渲染:

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

再来上babel后的代码:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      desc: 'start',
    };
  }

  render() {
    return React.createElement(
      'div',
      { className: 'App' },
      React.createElement(
        'div',
        { className: 'App-header' },
        React.createElement(
          'img',
          { src: "main.jpg", className: 'App-logo', alt: 'logo' }
        ),
        React.createElement(
          'h1',
          null,
          ' "Welcom to React" '
        )
      ),
      React.createElement(
        'p',
        { className: 'App-intro' },
        this.state.desc
      )
    );
  }
}

export default App;

...

ReactDOM.render(React.createElement(App, null), document.getElementById('root'));

这里我们先把Component看成一个普通的基类,因为她的其它成员方法暂时还用不到。

对于和简单组件重复的路径我们这次可以过快点。

构建顶层包装组件`ReactCompositeComponent[T]`

目标数据结构:

这一步和简单组件是一样的,所以就简单说一下:

1)创建ReactElement.createElement(type, config, children)创建ReactElement[1](这次App传给了type,而configchildren则是null);

2)在_renderSubtreeIntoContainer()里创建ReactElement[2]

3)用instantiateReactComponent()创建我们需要的包装元素。

ReactElement.createElement(type,    // scr: -------------> App
                           config,  // scr: -------------> null
                           children // scr: -------------> null
) // scr: ------------------------------------------------------> 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
) // scr: ------------------------------------------------------> 2)
  |-instantiateReactComponent( // scr: -------------------------> 3)
      node, // scr: ------> ReactElement[2]
      shouldHaveDebugID /* false */
    )
    |-ReactCompositeComponentWrapper(
        element // scr: ------> ReactElement[2]
      );
    |=ReactCompositeComponent.construct(element /* same */)

这个是我们在{第一篇}里面讲的。

初始化`ReactCompositeComponent[T]`

目标数据结构:

这一步也是一样的:

1)ReactDOMContainerInfo[ins]代表用document.getElementById(‘root’)创建的DOM元素;

2)TopLevelWrapper会被实例化,然后被设置给ReactCompositeComponent[T]._currentElement,同时其它的属性也会被初始化;

3)mountComponentIntoNode是表里层的交叉口,在这个函数里面ReactCompositeComponent[T].mountComponent会返回一个完整的DOMLazyTree,最后被里层函数ReactMount._mountImageIntoNode渲染成DOM元素。

ReactDOM.render                                           ___
|=ReactMount.render(nextElement, container, callback)      |
|=ReactMount._renderSubtreeIntoContainer()                 |
  |-ReactMount._renderNewRootComponent()                   |
  |-instantiateReactComponent()                            |
    |~batchedMountComponentIntoNode()                 upper half
      |~mountComponentIntoNode()                (platform agnostic)
        |-ReactReconciler.mountComponent() // scr-----> 1) |
          |-ReactCompositeComponent[T].mountComponent() scr:> 2)3)                    
            ...                                           _|_
               ...                                    lower half
        |-_mountImageIntoNode()                 (HTML DOM specific) 

这个是我们在{第二篇}里面讲的。

除了一下传参的细小差别外,其实顶层包装类的操作是和简单组件几本一致的。这些操作完成后我们就到了自定义组件特有的第一个叉道。

`ReactCompositeComponent[T].performInitialMount()` —用`ReactElement[1]`创建`ReactCompositeComponent`

这一步主要来去除顶层包装类,然后创建另一个ReactCompositeComponent来代表我们自定义的App组件。

目标数据结构:

调用栈:

...
|~mountComponentIntoNode()                                    |
  |-ReactReconciler.mountComponent()                          |
    |-ReactCompositeComponent[T].mountComponent()             |
      /* we are here */                                       |
      |-ReactCompositeComponent[T].performInitialMount(       |
          renderedElement,   // scr: -------> undefined       |
          hostParent,        // scr: -------> null        upper half
          hostContainerInfo, // scr: ------->                 | ReactDOMContainerInfo[ins]                                    |
          transaction,       // scr: -------> not of interest |
          context,           // scr: -------> not of interest |
        )                                                     |

这一步和在{第二篇}中讨论的performInitialMount() 非常类似。其中唯一的区别是_instantiateReactComponent会基于ReactElement[1]创建 代表AppReactCompositeComponent,而不是ReactDOMComponent。简单来说:

1)调用_renderValidatedComponent(),紧跟着在这个函数里再调用 TopLevelWrapper.render()来解出ReactElement[1];2)用_instantiateReactComponent实例化 ReactCompositeComponent[ins]);和3)通过ReactReconciler(递归)调用ReactCompositeComponent[ins].mountComponent ,然后我们到下一步。

performInitialMount: function (
  renderedElement,
  hostParent,
  hostContainerInfo,
  transaction,
  context) 
{
  var inst = this._instance;

...

  if (inst.componentWillMount) {
... // scr: we did not define componentWillMount() in App
  }

// If not a stateless component, we now render
  if (renderedElement === undefined) {
    renderedElement = this._renderValidatedComponent(); // scr: > 1)
  }

  var nodeType = ReactNodeTypes.getType(renderedElement); // scr: -> the type is ReactNodeTypes.Composite this time

  this._renderedNodeType = nodeType;

  var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
  );      // scr: ----------------------------------------------> 2)

  this._renderedComponent = child;

  var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID); // scr: ----------------------------------------------> 3)

...// scr: DEV code

  return markup;
},

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

`ReactCompositeComponent[1].mountComponent()` — initialize `ReactCompositeComponent[1]`

目标数据结构:

调用栈:

...
|~mountComponentIntoNode()                                    |
  |-ReactReconciler.mountComponent()                          |
    |-ReactCompositeComponent[T].mountComponent()             |
      |-ReactCompositeComponent[T].performInitialMount()  upper half
        |-ReactReconciler.mountComponent()                    |
          /* we are here */                                   |
          |-ReactCompositeComponent[1].mountComponent(same)   |

和{第二篇}里一样,ReactCompositeComponent[T].mountComponent()里面最重要的一步是用ReactCompositeComponent[ins]._currentElementReactElement[1])实例化App

把这个活干了的关键行是:

...
var inst = this._constructComponent(
    doConstruct,
    publicProps,
    publicContext,
    updateQueue,
  );
...

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

在这个函数里App的构造函数会被调用。

...
  constructor(props) {
    super(props);
    this.state = {
      desc: 'start',
    };
  }
...

// 从开头拷的

然后(我们叫她)App[ins]被赋值给ReactCompositeComponent[ins]._instance,并且ReactInstanceMap被用来做反向链接。

其它操作包括:1)App[ins].props引用ReactElement[1].props;和2)自增全局变量nextMountIDReactCompositeComponent[ins]._mountOrder设置成2。

这里很重要的一点是App[ins].render()这个在文章开头定义的另一个函数。不像TopLevelWrapper[ins].render()会返回一个实在的ReactElement对象,App[ins].render()会在被调用时依赖React.createElement() 来动态创建ReactElement。这个我们马上就会看到了。

之于这一步和{第二篇}讨论的ReactCompositeComponent[T]初始化非常类似,我们只用大致浏览一遍这个函数的实现就可以了。

mountComponent: function(
 transaction,
 hostParent,
 hostContainerInfo,
 context,
// scr: this ------> ReactCompositeComponent[T]
 ) {

...

this._mountOrder = nextMountID++; // scr: ----------------------> 2)

...

var publicProps = this._currentElement.props; // scr: ------------> { child: ReactElement[1] }

...

// scr: -------------------------------------------------> critical)
  var inst = this._constructComponent(
    doConstruct,
    publicProps,
    publicContext,
    updateQueue,
  ); // scr: ----------> call TopLevelWrapper’s constructor

var renderedElement;

...

// These should be set up in the constructor, but as a convenience   
// for simpler class abstractions, we set them up after the fact.
  // scr: --------------------------------------------------------> 1)
  inst.props = publicProps; // scr: ----> { child: ReactElement[1] }
…

// scr: -------------------------------------------------> critical)
  this._instance = inst; // scr: ---------------------------------> link the ReactCompositeComponent[T] to the TopLevelWrapper instance

// Store a reference from the instance back to the internal representation
  ReactInstanceMap.set(inst, this); // scr: ----------------------> link the TopLevelWrapper instance back to ReactCompositeComponent[T]

…

var markup;
  if (inst.unstable_handleError) { // scr: -----------------------> false, TopLevelWrapper.prototype.unstable_handleError is not defined
…
  } else {

// scr: -----------------------------------------------------> next)
    markup = this.performInitialMount( // scr: a initial at the end?
      renderedElement,
      hostParent,
      hostContainerInfo,
      transaction,
      context,
    );
  }

…

return markup;
}

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

`ReactCompositeComponent[ins].performInitialMount()` — create a `ReactDOMComponent`

...
|~mountComponentIntoNode()                                    |
  |-ReactReconciler.mountComponent()                          |
    |-ReactCompositeComponent[T].mountComponent()             |
      |-ReactCompositeComponent[T].performInitialMount()  upper half
        |-ReactReconciler.mountComponent()                    |
          /* we are here */                                   |
          |-ReactCompositeComponent[1].mountComponent()       |
            |-this.performInitialMount()                      |
              |-this._renderValidatedComponent()              |
              |-instantiateReactComponent()                  _|_ 
              |-ReactDOMComponent[6].mountComponent()     lower half

我们又到这个函数了:

performInitialMount: function (
  renderedElement,
  hostParent,
  hostContainerInfo,
  transaction,
  context) 
{
  var inst = this._instance;

...

  if (inst.componentWillMount) {
... // scr: we did not define componentWillMount() in App
  }

// If not a stateless component, we now render
  if (renderedElement === undefined) {
    renderedElement = this._renderValidatedComponent(); // scr: > 1)
  }

  var nodeType = ReactNodeTypes.getType(renderedElement); // scr: -> the type is ReactNodeTypes.Host this time

  this._renderedNodeType = nodeType;

  var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */
  );      // scr: ----------------------------------------------> 2)

  this._renderedComponent = child;

  var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID); // scr: ----------------------------------------------> 3)

...// scr: DEV code

  return markup;
},

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

ReactDOMComponent(我们知道这个类用来直接操作DOM)被创建之前 ,我们需要先把在App[ins]里定义的ReactElement们解出来。这里还是用App[ins].render()。下一行函数在 {第二篇}中讨论过。

...
renderedElement = this._renderValidatedComponent();
...

然后,这个App[ins].render()会触发

1) 连锁调用React.createElement()

为了理解ReactElement树的创建过程,我们先重新看一下App.render()的实现:

render() {
    return React.createElement(           // scr: -----------> 5)
      'div',
      { className: 'App' },
      React.createElement(                // scr: -----------> 3)
        'div',
        { className: 'App-header' },
        React.createElement(              // scr: -----------> 1)
          'img',
          { src: "main.jpg", className: 'App-logo', alt: 'logo' }
        ),
        React.createElement(              // scr: -----------> 2)
          'h1',
          null,
          ' "Welcom to React" '
        )
      ),
      React.createElement(                // scr: -----------> 4)
        'p',
        { className: 'App-intro' },
        this.state.desc
      )
    );
  }

// 从开头拷的

在这段代码片段里我还给出了createElement()们的调用顺序。这个顺序遵循一个非常简单的原则:先(用createElement())拿参数,再调(createElement())函数。

然后再看每个ReactElement的创建{第一篇}就有谱了。

React.createElement( // scr: --------------------------------> 1)
  ‘img’,
  { src: "main.jpg", className: ‘App-logo’, alt: ‘logo’ }
),

创建ReactElement[2]:

;然后

React.createElement( // scr: --------------------------------> 2)
  ‘h1’,
  null,
  ‘Welcome to React’
)

创建ReactElement[3]:

(现在 3)要的两个参数好了)

;然后

React.createElement(                // scr: -----------> 3)
  'div',
  ReactElement[2],
  ReactElement[3]
),

创建ReactElement[4]:

;然后

React.createElement(                // scr: -----------> 4)
  'p',
  { className: 'App-intro' },
  this.state.desc
)

创建ReactElement[5]:

(现在 5)要的两个参数也好了)

;然后

return React.createElement(           // scr: -----------> 5)
  'div',
  { className: 'App' },
  ReactElement[4],
  ReactElement[5]
)

创建ReactElement[6]:

合在一起:

ReactElement树就是这么建立的。这棵树的根节点会被赋值给renderedElement

2)`ReactCompositeComponent[ins]._instantiateReactComponent()` — Create `ReactDOMComponent[6]`

目标数据结构:

上一步建立的element树在下面这行代码被用于创建ReactDOMComponent[6](在_instantiateReactComponent()里){第二篇}

var child = this._instantiateReactComponent(
  renderedElement,
  nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
);

3)ReactReconciler.mountComponent()会调用ReactDOMComponent[6].mountComponent() 然后逻辑处理到了里层。

待续...

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

感谢阅读!👋


Originally published at

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

.