React 首次渲染
界面更新本质上就是数据的变化。通过把所有会动的东西收敛到状态 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) 拷贝 children
到 props.children
, 3) 拷贝 type.defaultProps
到 props
;
…
// 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)
会把 type
和 props
原样透传给 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.child
的 ReactElement[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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论