之前介紹了React16.8版本的React公用API徒探,本著學(xué)習(xí)最新版的React的想法,但是敗在了Fiber的陣下喂窟,還有回過頭來寫搞明白React15的源碼测暗,畢竟從15到16是一次重大的更新。本文中React源碼版本為 15.6.2 磨澡,望各位看官找準(zhǔn)版本號碗啄,不同的版本還是有著細微的區(qū)別的
值得一提的是,在閱讀源碼時稳摄,在Chrome中打斷點是一個很好的操作稚字,可以了解到函數(shù)的調(diào)用棧,變量的值厦酬,一步一步的調(diào)試還可以了解整個執(zhí)行的流程胆描,一邊調(diào)試一邊記錄著流程一邊在加以理解一邊感慨這神乎其技的封裝。
博客會同步到github上弃锐,這樣也算是有了開源的項目袄友。歡迎各位看官指教!
準(zhǔn)備步驟
首先需要安裝 React@15.6.2霹菊, ReactDOM@15.6.2
,其次搭建webpack打包支竹,因為必不可少的需要console.log
啥的旋廷,另外需要babel
的配置,babel6 babel7
倒是無所謂礼搁,關(guān)鍵是可以解析我們的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 (
<div>
{ this.state.name }
</div>
)
}
};
ReactDom.render(
<App>
<div>Hello World</div>
</App>,
document.getElementById('root')
);
本片博客就是基于該代碼進行調(diào)試的,將這段代碼使用babel
轉(zhuǎn)碼之后的結(jié)果為
// 主要代碼段
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'));
一個立即執(zhí)行函數(shù)碍岔,返回一個名為App
的構(gòu)造函數(shù)弥锄,內(nèi)部的componentWillMount
render
等方法,等會通過Object.defineProperty
方法添加到App
的原型鏈中闲孤。之后使用React.createElement
將App
轉(zhuǎn)換為ReactElement
對象傳入到ReactDOM.render
中
看源碼需要扎實的Js基礎(chǔ)豪治,原型鏈洞拨、閉包、this指向负拟、模塊化烦衣、Object.defineProperty等常用的方法都是必須提前掌握的。
ReactDOM.render
在引入ReactDOM.js
文件的時候掩浙,從上往下仔細看會發(fā)現(xiàn)有這么一行代碼是在引入的時候被執(zhí)行了ReactDefaultInjection.inject();
花吟,這個ReactDefaultInjection
調(diào)用了其內(nèi)部的一個inject
方法,主要目的是進行一次全局的依賴注入厨姚,本博主一開始光注意著研究ReactDOM.render
了衅澈,漏了這一句,導(dǎo)致后面有的東西很迷谬墙,所以在這提個醒今布,在引入一個文件時,文件內(nèi)部有的函數(shù)是沒有被導(dǎo)出的反而是在引入文件時直接執(zhí)行的芭梯。這個inject
具體的代碼后面用到時會進行詳細的介紹险耀。
下面就是ReactDOM
文件的代碼了
/* 各種文件的引入 */
// 執(zhí)行依賴注入
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 */
};
/* 雜七雜八的東西 */
那么實質(zhì)上ReactDOM.render
方法就是ReactMount.render
方法,ReactMount
文件可以說是render的入口了玖喘,是一個極其重要的文件甩牺。當(dāng)然ReactDOM
兩萬多行代碼,重要的文件一大堆累奈。贬派。。澎媒。
ReactMount
還是一樣的搞乏,從上往下看仔細看,不要去找關(guān)鍵詞ReactMount
戒努,一旦找關(guān)鍵詞會錯過很多細節(jié)请敦。一旦錯過了那么導(dǎo)致的結(jié)局就是臥槽,這個東西什么時候被賦值了储玫,臥槽侍筛,這個屬性哪里來的尷尬局面。所以再一次強調(diào)撒穷,打斷點的好處匣椰。Chrome斷點,??
那么你會發(fā)現(xiàn)端礼,有這么一個構(gòu)造函數(shù)
/**
* 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
就是整個組件的最頂層禽笑,我們調(diào)用ReactDOM.render
時入录,傳遞的參數(shù)被這個構(gòu)造函數(shù)給包裹起來。
// 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);
},
參數(shù)說明
nextElement: 這是React.createElement(App, null, React.createElement("div", null, "Hello World")))的結(jié)果佳镜,babel在解析jsx時僚稿,會調(diào)用React.createElement將我們寫的組件變成一個 ReactElement
container: ReactDOM.render 的第二個參數(shù),所需要掛載的節(jié)點邀杏,document.getElementById('root')
callback: 可選的回調(diào)函數(shù)贫奠,第三個參數(shù)
內(nèi)部就一句話,關(guān)鍵代碼還是ReactMount._renderSubtreeIntoContainer
函數(shù)
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呈現(xiàn)但不是根元素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;
}
參數(shù)
流程:
首先檢查callback
nextElement
是否是合法的,判斷一下類型啥的脖律,然后會使用React.createElement創(chuàng)建一個type
為TopLevelWrapper
的ReactElement
var nextWrappedElement = React.createElement(TopLevelWrapper, {
child: nextElement
});
我們傳入的nextElement會變成nextWrapperElement
的一個props
;
之后對parentComponent
是否存在進行判斷并對nextContext
賦值谢肾,當(dāng)前為空賦值為一個空對象emptyObject
調(diào)用getTopLevelWrapperInContainer(container)
方法,這個方法主要是檢查容器內(nèi)部是否已經(jīng)存在一個有ReactDOM直接渲染的節(jié)點小泉,當(dāng)前是無芦疏,我們的容器內(nèi)部是空的
再往下執(zhí)行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節(jié)點的一個屬性微姊,nodeType = 9 的話表明當(dāng)前container是document節(jié)點酸茴,不是話返回內(nèi)部的第一個子節(jié)點
接下來執(zhí)行var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
這個標(biāo)記變量containerHasReactMarkup
用來判斷當(dāng)前container是否具有React標(biāo)記,當(dāng)前值為 false
下一個var containerHasNonRootReactChild = hasNonRootReactChild(container);
如果DOM元素包含一個由React呈現(xiàn)但不是根元素R的直接子元素兢交,則為True薪捍。當(dāng)前為 false
下面根據(jù)以上的幾個變量得出一個標(biāo)記變量shouldReuseMarkup
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false
下面就是該函數(shù)的核心了
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
if (callback) {
callback.call(component);
}
return component;
執(zhí)行ReactMount._renderNewRootComponent()._renderedComponent.getPublicInstance()
函數(shù)并將返回值返回出來,如果傳入了callback
的話配喳,在return之前在調(diào)用一下callback酪穿。
那么先看ReactMount._renderNewRootComponent()
方法
ReactMount._renderNewRootComponent
傳入的參數(shù)為
nextWrappedElement: nextWrappedElement // 對 TopLevelWrapper調(diào)用React.createElement的結(jié)果
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期間發(fā)生的任何更新晴裹,在componentWillMount或componentDidMount中被济,都將根據(jù)當(dāng)前的批處理策略進行批處理。
ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
},
流程如下:
對 container 進行驗證
調(diào)用ReactBrowserEventEmitter.ensureScrollValueMonitoring()
確保監(jiān)聽瀏覽器滾動涧团,在React15中渲染時應(yīng)該是不會管頁面中高性能事件的只磷,所以在React16中引入的fiber架構(gòu)。
調(diào)用instantiateReactComponent
方法實例化一個ReactComponent泌绣,這個方法也是ReactDOM的一個重點喳瓣,在下篇會說
調(diào)用ReactUpdates.batchedUpdates();
開始執(zhí)行批量更新,這當(dāng)中會用到一開始注入的ReactDefaultBatchingStrategy
外部存儲一下當(dāng)前實例instancesByReactRootID[wrapperID] = componentInstance
赞别,對象instancesByReactRootID
外部閉包的一個Object,key值為實例的rootID,value值為當(dāng)前實例化出來的實例
最后return出這個實例配乓。
當(dāng)前流程圖
本篇留坑
instantiateReactComponent方法
ReactUpdates 文件