React原始碼分析2 — 元件和物件的建立(createClass,createElement)
1 元件的建立
React受大家歡迎的一個重要原因就是可以自定義元件。這樣的一方面可以複用開發好的元件,實現一處開發,處處呼叫,另外也能使用別人開發好的元件,提高封裝性。另一方面使得程式碼結構很清晰,元件間耦合減少,方便維護。ES5建立元件時,呼叫React.createClass()即可. ES6中使用class myComponent extends React.Component, 其實內部還是呼叫createClass建立元件。
元件建立我們可以簡單類比為Java中ClassLoader載入class。下面來分析下createClass的原始碼,我們省去了開發階段錯誤提示的相關程式碼,如propType的檢查。(if (“development” !== ‘production’) {}程式碼段都不進行分析了,這些只在開發除錯階段呼叫)
createClass: function (spec) {
var Constructor = function (props, context, updater) {
// 觸發自動繫結
if (this.__reactAutoBindPairs.length) {
bindAutoBindMethods(this);
}
// 初始化引數
this.props = props;
this.context = context;
this.refs = emptyObject; // 本元件物件的引用,可以利用它來呼叫元件的方法
this.updater = updater || ReactNoopUpdateQueue;
// 呼叫getInitialState()來初始化state變數
this.state = null;
var initialState = this.getInitialState ? this.getInitialState() : null;
this.state = initialState;
};
// 繼承父類
Constructor.prototype = new ReactClassComponent();
Constructor.prototype.constructor = Constructor;
Constructor.prototype.__reactAutoBindPairs = [];
injectedMixins.forEach(mixSpecIntoComponent.bind(null , Constructor));
mixSpecIntoComponent(Constructor, spec);
// 呼叫getDefaultProps,並掛載到元件類上。defaultProps是類變數,使用ES6寫法時更清晰
if (Constructor.getDefaultProps) {
Constructor.defaultProps = Constructor.getDefaultProps();
}
// React中暴露給應用呼叫的方法,如render componentWillMount。
// 如果應用未設定,則將他們設為null
for (var methodName in ReactClassInterface) {
if (!Constructor.prototype[methodName]) {
Constructor.prototype[methodName] = null;
}
}
return Constructor;
},
createClass主要做的事情有
- 定義構造方法Constructor,構造方法中進行props,refs等的初始化,並呼叫getInitialState來初始化state
- 呼叫getDefaultProps,並放在defaultProps類變數上。這個變數不屬於某個單獨的物件。可理解為static 變數
- 將React中暴露給應用,但應用中沒有設定的方法,設定為null。
2 物件的建立
JSX中建立React元素最終會被babel轉譯為createElement(type, config, children), babel根據JSX中標籤的首字母來判斷是原生DOM元件,還是自定義React元件。如果首字母大寫,則為React元件。這也是為什麼ES6中React元件類名必須大寫的原因。如下面程式碼
<div className="title" ref="example">
<span>123</span> // 原生DOM元件,首字母小寫
<ErrorPage title='123456' desc={[]}/> // 自定義元件,首字母大寫
</div>
轉譯完後是
React.createElement(
'div', // type,標籤名,原生DOM物件為String
{
className: 'title',
ref: 'example'
}, // config,屬性
React.createElement('span', null, '123'), // children,子元素
React.createElement(
// type,標籤名,React自定義元件的type不為String.
// _errorPage2.default為從其他檔案中引入的React元件
_errorPage2.default,
{
title: '123456',
desc: []
}
) // children,子元素
)
下面來分析下createElement的原始碼
ReactElement.createElement = function (type, config, children) {
var propName;
// 初始化引數
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
// 從config中提取出內容,如ref key props
if (config != null) {
ref = config.ref === undefined ? null : config.ref;
key = config.key === undefined ? null : '' + config.key;
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 提取出config中的prop,放入props變數中
for (propName in config) {
if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// 處理children,掛到props的children屬性下
// 入參的前兩個為type和config,後面的就都是children引數了。故需要減2
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
// 只有一個引數時,直接掛到children屬性下,不是array的方式
props.children = children;
} else if (childrenLength > 1) {
// 不止一個時,放到array中,然後將array掛到children屬性下
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 取出元件類中的靜態變數defaultProps,並給未在JSX中設定值的屬性設定預設值
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 返回一個ReactElement物件
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
};
下面來看ReactElement原始碼
var ReactElement = function (type, key, ref, self, source, owner, props) {
var element = {
// This tag allow us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// ReactElement物件上的四個變數,特別關鍵
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner
};
return element;
}
可以看到僅僅是給ReactElement物件內的成員變數賦值而已,不在贅述。
流程如如下
3 元件物件初始化
在mountComponent()掛載元件中,會進行元件渲染,呼叫到instantiateReactComponent()方法。這個過程我們在React生命週期方法中再詳細講述,這裡有個大體瞭解即可。instantiateReactComponent()根據ReactElement中不同的type欄位,建立不同型別的元件物件。原始碼如下
// 初始化元件物件,node是一個ReactElement物件,是節點元素在React中的表示
function instantiateReactComponent(node) {
var instance;
var isEmpty = node === null || node === false;
if (isEmpty) {
// 空物件
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
// 元件物件,包括DOM原生的和React自定義元件
var element = node;
// 根據ReactElement中的type欄位區分
if (typeof element.type === 'string') {
// type為string則表示DOM原生物件,比如div span等。可以參看上面babel轉譯的那段程式碼
instance = ReactNativeComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
// 保留給以後版本使用,此處暫時不會涉及到
instance = new element.type(element);
} else {
// React自定義元件
instance = new ReactCompositeComponentWrapper(element);
}
} else if (typeof node === 'string' || typeof node === 'number') {
// 元素是一個string時,對應的比如<span>123</span> 中的123
// 本質上它不是一個ReactElement,但為了統一,也按照同樣流程處理,稱為ReactDOMTextComponent
instance = ReactNativeComponent.createInstanceForText(node);
} else {
// 無處理
}
// 初始化引數,這兩個引數是DOM diff時用到的
instance._mountIndex = 0;
instance._mountImage = null;
return instance;
}
故我們可以看到有四種建立元件元素的方式,同時對應四種ReactElement
- ReactEmptyComponent.create(), 建立空物件ReactDOMEmptyComponent
- ReactNativeComponent.createInternalComponent(), 建立DOM原生物件 ReactDOMComponent
- new ReactCompositeComponentWrapper(), 建立React自定義物件ReactCompositeComponent
- ReactNativeComponent.createInstanceForText(), 建立文字物件 ReactDOMTextComponent
下面分別分析這幾種物件,和建立他們的過程。
ReactDOMEmptyComponent
由ReactEmptyComponent.create()建立,最終生成ReactDOMEmptyComponent物件,原始碼如下
var emptyComponentFactory;
var ReactEmptyComponentInjection = {
injectEmptyComponentFactory: function (factory) {
emptyComponentFactory = factory;
}
};
var ReactEmptyComponent = {
create: function (instantiate) {
return emptyComponentFactory(instantiate);
}
};
ReactEmptyComponent.injection = ReactEmptyComponentInjection;
ReactInjection.EmptyComponent.injectEmptyComponentFactory(function (instantiate) {
// 前面比較繞,關鍵就是這句話,建立ReactDOMEmptyComponent物件
return new ReactDOMEmptyComponent(instantiate);
});
// 各種null,就不分析了
var ReactDOMEmptyComponent = function (instantiate) {
this._currentElement = null;
this._nativeNode = null;
this._nativeParent = null;
this._nativeContainerInfo = null;
this._domID = null;
};
ReactDOMComponent
由ReactNativeComponent.createInternalComponent()建立。這裡注意原生元件不代表是DOM元件,而是React封裝過的Virtual DOM物件。React並不直接操作原生DOM。
大家可以自己看ReactDOMComponent的原始碼。重點看下ReactDOMComponent.Mixin
ReactDOMComponent.Mixin = {
mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {},
_createOpenTagMarkupAndPutListeners: function (transaction, props){},
_createContentMarkup: function (transaction, props, context) {},
_createInitialChildren: function (transaction, props, context, lazyTree) {}
receiveComponent: function (nextElement, transaction, context) {},
updateComponent: function (transaction, prevElement, nextElement, context) {},
_updateDOMProperties: function (lastProps, nextProps, transaction) {},
_updateDOMChildren: function (lastProps, nextProps, transaction, context) {},
getNativeNode: function () {},
unmountComponent: function (safely) {},
getPublicInstance: function () {}
}
其中暴露給外部的比較關鍵的是mountComponent,receiveComponen, updateComponent,unmountComponent。他們會引發React生命週期方法的呼叫,下一節再講。
ReactCompositeComponent
由new ReactCompositeComponentWrapper()建立,重點看下ReactCompositeComponentMixin
var ReactCompositeComponentMixin = {
// new對應的方法,建立ReactCompositeComponent物件
construct: function(element) {},
mountComponent, // 初始掛載元件時被呼叫,僅一次
performInitialMountWithErrorHandling, // 和performInitialMount相近,只是多了錯誤處理
performInitialMount, // 執行mountComponent的渲染階段,會呼叫到instantiateReactComponent,從而進入初始化React元件的入口
getNativeNode,
unmountComponent, // 解除安裝元件,記憶體釋放等工作
receiveComponent,
performUpdateIfNecessary,
updateComponent, // setState後被呼叫,重新渲染元件
attachRef, // 將ref指向元件物件,這樣我們就可以利用它呼叫物件內的方法了
detachRef, // 將元件的引用從全域性物件refs中刪掉,這樣我們就不能利用ref找到元件物件了
instantiateReactComponent, // 初始化React元件的入口,在mountComponent時的渲染階段會被呼叫
}
ReactDOMTextComponent
由ReactNativeComponent.createInstanceForText()建立,我們也不細細分析了,主要入口程式碼如下,大家可以自行分析。
var ReactDOMTextComponent = function (text) {
this._currentElement = text;
this._stringText = '' + text;
};
_assign(ReactDOMTextComponent.prototype, {
mountComponent,
receiveComponent,
getNativeNode,
unmountComponent
}