React 首次渲染

发布于 2022-10-26 12:45:08 字数 13438 浏览 123 评论 0

界面更新本质上就是数据的变化。通过把所有会动的东西收敛到状态 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 语句中看到,所以我会用 @ 标签在代码块中标注其对应的文件路径。

从 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)

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


var createElement = ReactElement.createElement;

var React = {

createElement: createElement,

};
module.exports = React;

React@isomorphic/React.js

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

然后 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树中。 这个过程留到下篇讨论。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
27 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文