Photo by Gerrie van der Walt on Unsplash
界面更新本质上就是数据的变化。通过把所有会动的东西收敛到状态(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)
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) 拷贝 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
3. 然后 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树中。 这个过程留到下篇讨论。
今天先写到这。如果您觉得这篇不错,可以点赞或关注这个专栏。
感谢阅读!👋
Originally published at holmeshe.me.