之前介绍了React16.8版本的React公用API,本着学习最新版的React的想法,但是败在了Fiber的阵下,还有回过头来写搞明白React15的源码,毕竟从15到16是一次重大的更新。本文中React源码版本为 15.6.2 ,望各位看官找准版本号,不同的版本还是有着细微的区别的
值得一提的是,在阅读源码时,在Chrome中打断点是一个很好的操作,可以了解到函数的调用栈,变量的值,一步一步的调试还可以了解整个执行的流程,一边调试一边记录着流程一边在加以理解一边感慨这神乎其技的封装。
博客会同步到github上,这样也算是有了开源的项目。欢迎各位看官指教!
准备步骤
首先需要安装 [email protected], [email protected]
,其次搭建webpack打包,因为必不可少的需要console.log
啥的,另外需要babel
的配置,babel6 babel7
倒是无所谓,关键是可以解析我们的jsx
语法。
示例代码
import React from 'react';
import ReactDom from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: 'Hello World'
}
}
componentWillMount() {
console.log('component will mount');
this.setState({
name: 'Hello CHina'
})
}
componentDidMount() {
console.log('component did mount');
this.setState({
name: 'Hello CHange'
})
}
componentWillReceiveProps(nextProps) {
console.log('component will receive props');
}
componentWillUpdate(nextProps, nextState) {
console.log('component will updates');
}
componentDidUpdate(prevProps, prevState){
console.log('component Did Update');
};
render() {
console.log('render');
return (
{ this.state.name }
)
}
};
ReactDom.render(
Hello World
,
document.getElementById('root')
);
本片博客就是基于该代码进行调试的,将这段代码使用babel
转码之后的结果为
// 主要代码段
var App =
/*#__PURE__*/
function (_React$Component) {
_inherits(App, _React$Component);
function App(props) {
var _this;
_classCallCheck(this, App);
_this = _possibleConstructorReturn(this, _getPrototypeOf(App).call(this, props));
_this.state = {
name: 'Hello World'
};
return _this;
}
_createClass(App, [{
key: "componentWillMount",
value: function componentWillMount() {
console.log('component will mount');
this.setState({
name: 'Hello CHina'
});
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
console.log('component did mount');
this.setState({
name: 'Hello CHange'
});
}
}, {
key: "componentWillReceiveProps",
value: function componentWillReceiveProps(nextProps) {
console.log('component will receive props');
}
}, {
key: "componentWillUpdate",
value: function componentWillUpdate(nextProps, nextState) {
console.log('component will updates');
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
console.log('component Did Update');
}
}, {
key: "render",
value: function render() {
console.log('render');
return _react["default"].createElement("div", null, this.state.name);
}
}]);
return App;
}(_react["default"].Component);
;
_reactDom["default"].render(_react["default"].createElement(App, null, _react["default"].createElement("div", null, "Hello World")), document.getElementById('root'));
一个立即执行函数,返回一个名为App
的构造函数,内部的componentWillMount
render
等方法,等会通过Object.defineProperty
方法添加到App
的原型链中。之后使用React.createElement
将App
转换为ReactElement
对象传入到ReactDOM.render
中
看源码需要扎实的Js基础,原型链、闭包、this指向、模块化、Object.defineProperty等常用的方法都是必须提前掌握的。
ReactDOM.render
在引入ReactDOM.js
文件的时候,从上往下仔细看会发现有这么一行代码是在引入的时候被执行了ReactDefaultInjection.inject();
,这个ReactDefaultInjection
调用了其内部的一个inject
方法,主要目的是进行一次全局的依赖注入,本博主一开始光注意着研究ReactDOM.render
了,漏了这一句,导致后面有的东西很迷,所以在这提个醒,在引入一个文件时,文件内部有的函数是没有被导出的反而是在引入文件时直接执行的。这个inject
具体的代码后面用到时会进行详细的介绍。
下面就是ReactDOM
文件的代码了
/* 各种文件的引入 */
// 执行依赖注入
ReactDefaultInjection.inject();
// ReactDOM对象
var ReactDOM = {
findDOMNode: findDOMNode,
render: ReactMount.render,
unmountComponentAtNode: ReactMount.unmountComponentAtNode,
version: ReactVersion,
/* eslint-disable camelcase */
unstable_batchedUpdates: ReactUpdates.batchedUpdates,
unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
/* eslint-enable camelcase */
};
/* 杂七杂八的东西 */
那么实质上ReactDOM.render
方法就是ReactMount.render
方法,ReactMount
文件可以说是render的入口了,是一个极其重要的文件。当然ReactDOM
两万多行代码,重要的文件一大堆。。。。
ReactMount
还是一样的,从上往下看仔细看,不要去找关键词ReactMount
,一旦找关键词会错过很多细节。一旦错过了那么导致的结局就是卧槽,这个东西什么时候被赋值了,卧槽,这个属性哪里来的尴尬局面。所以再一次强调,打断点的好处。Chrome断点,
那么你会发现,有这么一个构造函数
/**
* Temporary (?) hack so that we can store all top-level pending updates on
* composites instead of having to worry about different types of components
* here.
*/
var topLevelRootCounter = 1;
var TopLevelWrapper = function () {
this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
if (process.env.NODE_ENV !== 'production') {
TopLevelWrapper.displayName = 'TopLevelWrapper';
}
TopLevelWrapper.prototype.render = function () {
return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;
这个TopLevelWrapper
就是整个组件的最顶层,我们调用ReactDOM.render
时,传递的参数被这个构造函数给包裹起来。
// ReactMount.js
/**
*
* @param {ReactElement} nextElement Component element to render.
* @param {DOMElement} container DOM element to render into.
* @param {?function} callback function triggered on completion
* @return {ReactComponent} Component instance rendered in `container`.
*/
render: function (nextElement, container, callback) {
return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
},
参数说明
nextElement: 这是React.createElement(App, null, React.createElement("div", null, "Hello World")))的结果,babel在解析jsx时,会调用React.createElement将我们写的组件变成一个 ReactElement
container: ReactDOM.render 的第二个参数,所需要挂载的节点,document.getElementById('root')
callback: 可选的回调函数,第三个参数
内部就一句话,关键代码还是ReactMount._renderSubtreeIntoContainer
函数
ReactMount._renderSubtreeIntoContainer
// ReactMount.js
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
// 校验 callback
ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
!React.isValidElement(nextElement) ? /**/
nextElement != null && nextElement.props !== undefined ? /**/
/**/
var nextWrappedElement = React.createElement(TopLevelWrapper, {
child: nextElement
});
var nextContext;
if (parentComponent) {
var parentInst = ReactInstanceMap.get(parentComponent);
nextContext = parentInst._processChildContext(parentInst._context);
} else {
nextContext = emptyObject;
}
var prevComponent = getTopLevelWrapperInContainer(container);
if (prevComponent) {
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props.child;
if (shouldUpdateReactComponent(prevElement, nextElement)) {
var publicInst = prevComponent._renderedComponent.getPublicInstance();
var updatedCallback = callback && function () {
callback.call(publicInst);
};
ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container, updatedCallback);
return publicInst;
} else {
ReactMount.unmountComponentAtNode(container);
}
}
var reactRootElement = getReactRootElementInContainer(container);
var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement); // false
// 如果DOM元素包含一个由React呈现但不是根元素R的直接子元素,则为True。
var containerHasNonRootReactChild = hasNonRootReactChild(container); // false
if (process.env.NODE_ENV !== 'production') {
/**/
}
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
if (callback) {
callback.call(component);
}
return component;
}
参数
流程:
首先检查callback
nextElement
是否是合法的,判断一下类型啥的,然后会使用React.createElement创建一个type
为TopLevelWrapper
的ReactElement
var nextWrappedElement = React.createElement(TopLevelWrapper, {
child: nextElement
});
我们传入的nextElement会变成nextWrapperElement
的一个props
;
之后对parentComponent
是否存在进行判断并对nextContext
赋值,当前为空赋值为一个空对象emptyObject
调用getTopLevelWrapperInContainer(container)
方法,这个方法主要是检查容器内部是否已经存在一个有ReactDOM直接渲染的节点,当前是无,我们的容器内部是空的
再往下执行var reactRootElement = getReactRootElementInContainer(container);
getReactRootElementInContainer
/**
* @param {DOMElement|DOMDocument} container DOM element that may contain
* a React component
* @return {?*} DOM element that may have the reactRoot ID, or null.
*/
function getReactRootElementInContainer(container) {
if (!container) {
return null;
}
// DOC_NODE_TYPE = 9
if (container.nodeType === DOC_NODE_TYPE) {
return container.documentElement;
} else {
return container.firstChild;
}
}
对container.nodeType做判断,nodeType
是html节点的一个属性,nodeType = 9 的话表明当前container是document节点,不是话返回内部的第一个子节点
接下来执行var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
这个标记变量containerHasReactMarkup
用来判断当前container是否具有React标记,当前值为 false
下一个var containerHasNonRootReactChild = hasNonRootReactChild(container);
如果DOM元素包含一个由React呈现但不是根元素R的直接子元素,则为True。当前为 false
下面根据以上的几个变量得出一个标记变量shouldReuseMarkup
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false
下面就是该函数的核心了
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
if (callback) {
callback.call(component);
}
return component;
执行ReactMount._renderNewRootComponent()._renderedComponent.getPublicInstance()
函数并将返回值返回出来,如果传入了callback
的话,在return之前在调用一下callback。
那么先看ReactMount._renderNewRootComponent()
方法
ReactMount._renderNewRootComponent
传入的参数为
nextWrappedElement: nextWrappedElement // 对 TopLevelWrapper调用React.createElement的结果
container: document.getElementById('root')
shouldReuseMarkup: false
nextContext: {}
_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
process.env.NODE_ENV !== 'production' /**/
!isValidContainer(container) /**/
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
var componentInstance = instantiateReactComponent(nextElement, false);
// 初始render是同步的,但是在render期间发生的任何更新,在componentWillMount或componentDidMount中,都将根据当前的批处理策略进行批处理。
ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
},
流程如下:
对 container 进行验证
调用ReactBrowserEventEmitter.ensureScrollValueMonitoring()
确保监听浏览器滚动,在React15中渲染时应该是不会管页面中高性能事件的,所以在React16中引入的fiber架构。
调用instantiateReactComponent
方法实例化一个ReactComponent,这个方法也是ReactDOM的一个重点,在下篇会说
调用ReactUpdates.batchedUpdates();
开始执行批量更新,这当中会用到一开始注入的ReactDefaultBatchingStrategy
外部存储一下当前实例instancesByReactRootID[wrapperID] = componentInstance
,对象instancesByReactRootID
外部闭包的一个Object,key值为实例的rootID,value值为当前实例化出来的实例
最后return出这个实例。
当前流程图
本篇留坑
instantiateReactComponent方法
ReactUpdates 文件