本文研究的版本爲reactv0.8.0javascript
在v0.8.0版本中,react組件初始掛載實現是相對簡單的。整體能夠劃分爲兩部分來闡述:組件實例化和(真正的)組件掛載。由於先理解組件實例化,再理解組件掛載會比較好。因此,我先介紹組件實例化,後介紹(真正的)組件掛載流程。html
在搞懂什麼是react組件以前,咱們不妨先了解一下「組件」的定義。顯然,「組件」這個概念並非軟件編程界所獨有的,它應該是來源於工程學。前端
卡耐基梅隆大學給「組件」下過這樣的定義:java
一個不透明的功能實體,可以被第三方組裝,且符合一個構件模型。node
計算機百科全書是這樣說的:react
是軟件系統中具備相對獨立功能、接口由契約指定、和語境有明顯依賴關係、可獨立部署、可組裝的軟件實體。jquery
軟件構件著做中如是定義:算法
是一個組裝單元,它具備約定式規範的接口,以及明確的依賴環境。構建能夠被獨立的部署,由第三方組裝。編程
不一樣的上下文中,「組件」的定義是略有不一樣的。可是,共同點是有的。「功能獨立,向外提供接口,可組裝/組合」就是組件定義的基本要素。咱們拿這三點對照一下後,會發現react組件是符合這三個基本要素的。canvas
const A_Component = React.createClass({
render(){
return (
<B_Component>
<C_Component />
</B_Component>
)
}
})
複製代碼
這種示例下,咱們能夠清晰地看到A_Component是由B_Component和C_Component組合而成的。而A_Component組件又能夠參與別的組件的組合。
從最後的實現結果來看,react組件是符合這三個組件定義的基本要素的。那麼,迴歸到「react」這個語景中,什麼是「react」組件呢? 我的理解是這樣的:
若是非要扯上點代碼,那麼咱們能夠說,由React.createClass()和React.DOM.XXX()返回的就是react組件。
以上是我的對react組件定義的理解。那麼官方是怎麼定義的呢?其實,官方也沒有太嚴謹地,鄭重其事地給它下定義,只是簡短的一句話(見官網):
React components are small, reusable pieces of code that return a React element to be rendered to the page.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
複製代碼
那什麼又是「react element」呢?官方是這麼說的:
React elements are the building blocks of React applications. One might confuse elements with a more widely known concept of 「components」. An element describes what you want to see on the screen. React elements are immutable.
const element = <h1>Hello, world</h1>;
複製代碼
Typically, elements are not used directly, but get returned from components.
從代碼層面來看,官方已經把「react組件」和「react element」區分得很清楚了。就拿上面列舉的代碼來講明的話,Welcome
這個變量是組件,<Welcome />
這個jsx就是「react element」。
可是有一個問題,在reactv0.8.0中,並無引入「react element」的概念(react在v0.12.0的時候才引入「react element」的概念)。那此時的「react 組件」是怎麼理解的呢?其實,此時的「react組件實例」大體至關於上面所提到的「react element」,咱們只須要把上面所提到的「react element」替換爲「react組件實例」就好。
當咱們不寫jsx的時候,每每能把react的諸多概念看得更透徹些。回到reactv0.8.0中,若是有如下代碼片斷的的話:
const Welcome = React.createClass({
render(){
return React.DOM.h1({},'Hello, world');
}
})
複製代碼
咱們會說變量Welcome
是一個「react組件」,而Welcome()
(用jsx表示的話就是<Welcome />
)就是一個「react組件實例」。
在reactv0.8.0中,組件分爲三個大的類別:
類是一個抽象的存在,那麼咱們不妨經過實例來具像化這三種類型組件的感知。因而,咱們能夠在控制檯把它們都打印出來看看。
若是咱們有ReactCompositeComponent組件以下:
const RCC = React.createClass({
render(){
return 'ReactCompositeComponent';
}
})
複製代碼
那麼它的組件實例是這樣的:
咱們隨便建立個ReactDOMComponent組件實例以下:
const DIV= React.DOM.div({},'ReactDOMComponent')
複製代碼
把它打印出來看看
最後,咱們來看看ReactTextComponent組件實例長什麼樣:
const TEXT= new React.__internals.TextComponent('ReactTextComponent');
複製代碼
眼尖的同窗可能都看到了,這幾個類型組件的實例都經過__proto__屬性告知咱們,這些實例對象都是經過原型繼承來繼承了某些方法的。由於沒有時間用UML畫更具體的類關係圖,我畫了個簡單粗略的關係圖:
在react源碼中,採用mixin模式實現了原型繼承,而且很好地複用了代碼。下面看看實現mixin模式的關鍵方法:/**
* Simply copies properties to the prototype.
*/
var mixInto = function(constructor, methodBag) {
var methodName;
for (methodName in methodBag) {
if (!methodBag.hasOwnProperty(methodName)) {
continue;
}
constructor.prototype[methodName] = methodBag[methodName];
}
};
複製代碼
從代碼中,咱們看到了mixInto經過遍歷傳遞進來的methodBag,把它身上的方法逐個逐個地掛載在constructor的原型對象上來實現了原型繼承和mixin模式的結合的。
因此,咱們在探究react組件初始掛載過程當中,定位某個方法的源碼時,只要沿着原型鏈傻上找就好。好了,組件類型就講到這裏。下面,咱們探索一下各類類型組件的具體實例化過程。
ReactTextComponent構造函數是掛在React.__internals上的,只供內部使用,所以組件實例化也是由內部代碼來完成的。這一節,咱們主要是討論ReactCompositeComponent和ReactDOMComponent的實例化過程。ReactTextComponent的實例化過程比較簡單,咱們放在最後講。
由於源碼實現的緣故,ReactCompositeComponent和ReactDOMComponent的實例化都是通過兩次函數調用才完成的。而這麼作的緣由,值得咱們深究。
由於React.createClass方法引用的就是ReactCompositeComponent.createClass方法,因此,咱們就直奔ReactCompositeComponent.js看看:
var ReactCompositeComponent = {
// ......
/**
* Creates a composite component class given a class specification.
*
* @param {object} spec Class specification (which must define `render`).
* @return {function} Component constructor function.
* @public
*/
createClass: function(spec) {
// 這裏不妨這麼寫,可以幫助讀者更清楚梳理各個「類」之間的關係
// 那就是:var Constructor = function ReactCompositeComponent(){}
var Constructor = function() {};
Constructor.prototype = new ReactCompositeComponentBase();
Constructor.prototype.constructor = Constructor;
mixSpecIntoComponent(Constructor, spec);
("production" !== process.env.NODE_ENV ? invariant(
Constructor.prototype.render,
'createClass(...): Class specification must implement a `render` method.'
) : invariant(Constructor.prototype.render));
if ("production" !== process.env.NODE_ENV) {
if (Constructor.prototype.componentShouldUpdate) {
console.warn(
(spec.displayName || 'A component') + ' has a method called ' +
'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +
'The name is phrased as a question because the function is ' +
'expected to return a value.'
);
}
}
// Reduce time spent doing lookups by setting these on the prototype.
for (var methodName in ReactCompositeComponentInterface) {
if (!Constructor.prototype[methodName]) {
Constructor.prototype[methodName] = null;
}
}
var ConvenienceConstructor = function(props, children) {
var instance = new Constructor();
instance.construct.apply(instance, arguments);
return instance;
};
ConvenienceConstructor.componentConstructor = Constructor;
ConvenienceConstructor.originalSpec = spec;
return ConvenienceConstructor;
},
// ......
}
複製代碼
能夠看出,createClass方法的源碼框架是這樣的:
createClass: function(spec) {
var Constructor = function() {};
var ConvenienceConstructor = function(props, children) {
var instance = new Constructor();
return instance;
};
return ConvenienceConstructor;
}
複製代碼
第一次調用是由用戶完成的,像這樣:
const SomeComponent = React.createClass({
render(){
return React.DOM.div({},'SomeComponent');
}
})
複製代碼
而對照上面的源碼框架,咱們能夠知道,其實SomeComponent就是構造函數。再囉嗦點講其實就是一個自定義類型。對的,組件本質上就是一個自定義類型。
而後,通常狀況下,咱們會用jsx的方式去消費SomeComponent
:
const AnotherComponent = React.createClass({
render(){
return <SomeComponent />;
}
})
複製代碼
咱們你們都知道jsx<SomeComponent />
會被編譯爲一個普通的函數調用模樣:SomeComponent()
。也就是看似聲明式的jsx本質是一個命令式的函數調用,就像react的提供給用戶的是函數式的開發風格下實際上是面向對象式的實現同樣的道理:真相每每是dirty的。而這個函數調用就是咱們上面所提兩次調用裏面的最後一次了。SomeComponent()
返回的是什麼呢?是組件實例。什麼?都沒有new操做符,如何實例化的呢?客官稍安勿躁,待我娓娓道來。
React.createClass方法調用以後返回的是一個構造函數,表明着一類組件,這個相信你們都有認識了。從源碼看,上面的SomeComponent其實就是ConvenienceConstructor
函數。如今咱們聚焦一下ConvenienceConstructor
函數的具體實現:
var ConvenienceConstructor = function(props, children) {
var instance = new Constructor();
instance.construct.apply(instance, arguments);
return instance;
};
複製代碼
相信你們看到了SomeComponent組件實例其實就是裏面的instance,而instance就是經過new Constructor來返回的。也就是說,咱們的ReactCompositeComponent組件實例的構造函數就是這個Constructor。雖然組件實例的構造函數是它,可是實際的實例化工做並非它來完成的。它只是一個「空殼公司」,啥事也沒幹。兩處代碼可證:
// 函數聲明
var Constructor = function() {};
// 實際的實例化
instance.construct.apply(instance, arguments);
複製代碼
咱們能夠看到,Constructor函數只作了聲明,並無具體的實現代碼。它最後在閉包裏面,把實例化的工做交給了實例對象的construct方法。而new出來的實例對象[自身屬性]上根本沒有該方法,因而乎,咱們就得往原型鏈上去找這個方法了。
在createClass方法的源碼的開頭處,咱們能夠看到有兩個地方是往構造函數的原型對象上掛載方法的。
第一個:
Constructor.prototype = new ReactCompositeComponentBase();
複製代碼
第二個:
mixSpecIntoComponent(Constructor, spec);
複製代碼
顯然,咱們傳入的spec對象裏面並無construct方法,那確定是在ReactCompositeComponentBase類裏面了。一番代碼導航追溯下來,咱們發現了這個construct方法是ReactCompositeComponentMixin.construct:
construct: function(initialProps, children) {
// Children can be either an array or more than one argument
ReactComponent.Mixin.construct.apply(this, arguments);
this.state = null;
this._pendingState = null;
this._compositeLifeCycleState = null;
},
複製代碼
而方法的主體實際上是由ReactComponent.Mixin.construct方法來充當的:
/**
* Base constructor for all React component.
*
* Subclasses that override this method should make sure to invoke
* `ReactComponent.Mixin.construct.call(this, ...)`.
*
* @param {?object} initialProps
* @param {*} children
* @internal
*/
construct: function(initialProps, children) {
this.props = initialProps || {};
// Record the component responsible for creating this component.
this.props.__owner__ = ReactCurrentOwner.current;
// All components start unmounted.
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
this._pendingProps = null;
this._pendingCallbacks = null;
// Children can be more than one argument
// 從這段代碼能夠看出,this.props.children值的類型是:對象 或者 對象組成的數組
var childrenLength = arguments.length - 1;
if (childrenLength === 1) {
if ("production" !== process.env.NODE_ENV) {
validateChildKeys(children);
}
this.props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
if ("production" !== process.env.NODE_ENV) {
validateChildKeys(arguments[i + 1]);
}
childArray[i] = arguments[i + 1];
}
this.props.children = childArray;
}
}
複製代碼
由於在createClass方法裏面Constructor構造函數和ReactCompositeComponentBase構造函數都是個空函數,因此咱們能夠用僞代碼作個總結一下ReactCompositeComponent組件的實例化過程就是:
ReactCompositeComponent實例化 = ReactCompositeComponentMixin.construct() + ReactComponent.Mixin.construct()
複製代碼
具體的實例化細節,我在這裏就不深刻講述了。不過,有一點咱們卻是能夠再看看,那就ReactComponent.Mixin.construct方法的註釋:
Base constructor for all React component.
複製代碼
咱們能夠看出,react中全部類型組件的實例化接口都是同樣的,都是:
(initialProps, children) => componentInstance
複製代碼
ReactCompositeComponent組件的實例化過程所涉及的兩個函數調用都是由用戶來完成的。若是,從用戶角度來看,ReactDOMComponent組件的實例化過程就不是這樣了。由於react幫咱們作了第一次調用,而咱們只須要作第二次調用。下面來看看。
ReactDOMComponent實例化構造函數的構造過程大致跟ReactCompositeComponent是相同的。它也是有一個createDOMComponentClass的方法用於建立同一類別DOM組件所須要的constructor:
/**
* Creates a new React class that is idempotent and capable of containing other
* React components. It accepts event listeners and DOM properties that are
* valid according to `DOMProperty`.
*
* - Event listeners: `onClick`, `onMouseDown`, etc.
* - DOM properties: `className`, `name`, `title`, etc.
*
* The `style` property functions differently from the DOM API. It accepts an
* object mapping of style properties to values.
*
* @param {string} tag Tag name (e.g. `div`).
* @param {boolean} omitClose True if the close tag should be omitted.
* @private
*/
function createDOMComponentClass(tag, omitClose) {
var Constructor = function() {};
Constructor.prototype = new ReactDOMComponent(tag, omitClose);
Constructor.prototype.constructor = Constructor;
Constructor.displayName = tag;
var ConvenienceConstructor = function(props, children) {
var instance = new Constructor();
instance.construct.apply(instance, arguments);
return instance;
};
ConvenienceConstructor.componentConstructor = Constructor;
return ConvenienceConstructor;
}
複製代碼
不一樣的是,ReactDOMComponent這個構造函數是有具體實現的,而construct方法是全權指向ReactComponent.Mixin.construct。因此,針對ReactDOMComponent實例化過程,咱們有如下的總結:
ReactDOMComponent實例化 = new ReactDOMComponent() + ReactComponent.Mixin.construct()
複製代碼
上以小節也提到了,ReactDOMComponent的實例化所須要的第一次函數調用實際上是react幫咱們作了。怎麼作法呢?其實就是使用上面提到的那個createDOMComponentClass方法:
/**
* Creates a mapping from supported HTML tags to `ReactDOMComponent` classes.
* This is also accessible via `React.DOM`.
*
* @public
*/
var ReactDOM = objMapKeyVal({
a: false,
abbr: false,
address: false,
area: false,
article: false,
aside: false,
audio: false,
b: false,
base: false,
bdi: false,
bdo: false,
big: false,
blockquote: false,
body: false,
br: true,
button: false,
canvas: false,
caption: false,
cite: false,
code: false,
col: true,
colgroup: false,
data: false,
datalist: false,
dd: false,
del: false,
details: false,
dfn: false,
div: false,
dl: false,
dt: false,
em: false,
embed: true,
fieldset: false,
figcaption: false,
figure: false,
footer: false,
form: false, // NOTE: Injected, see `ReactDOMForm`.
h1: false,
h2: false,
h3: false,
h4: false,
h5: false,
h6: false,
head: false,
header: false,
hr: true,
html: false,
i: false,
iframe: false,
img: true,
input: true,
ins: false,
kbd: false,
keygen: true,
label: false,
legend: false,
li: false,
link: false,
main: false,
map: false,
mark: false,
menu: false,
menuitem: false, // NOTE: Close tag should be omitted, but causes problems.
meta: true,
meter: false,
nav: false,
noscript: false,
object: false,
ol: false,
optgroup: false,
option: false,
output: false,
p: false,
param: true,
pre: false,
progress: false,
q: false,
rp: false,
rt: false,
ruby: false,
s: false,
samp: false,
script: false,
section: false,
select: false,
small: false,
source: false,
span: false,
strong: false,
style: false,
sub: false,
summary: false,
sup: false,
table: false,
tbody: false,
td: false,
textarea: false, // NOTE: Injected, see `ReactDOMTextarea`.
tfoot: false,
th: false,
thead: false,
time: false,
title: false,
tr: false,
track: true,
u: false,
ul: false,
'var': false,
video: false,
wbr: false,
// SVG
circle: false,
g: false,
line: false,
path: false,
polyline: false,
rect: false,
svg: false,
text: false
}, createDOMComponentClass);
複製代碼
更加具體的函數調用操做是在objMapKeyVal方法的實現代碼裏面:
function objMapKeyVal(obj, func, context) {
if (!obj) {
return null;
}
var i = 0;
var ret = {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = func.call(context, key, obj[key], i++);
}
}
return ret;
}
複製代碼
看到沒?ret[key] = func.call(context, key, obj[key], i++);
是也。
通過objMapKeyVal的一番調用,掛載在React.DOM引用所指向的對象實際上是這樣的:
React.DOM = {
a: createDOMComponentClass('a', false),
abbr: createDOMComponentClass('abbr', false),
// ......
}
複製代碼
到了這裏,咱們也看清楚了爲何在實例化ReactDOMComponent組件的過程當中,咱們並不須要像實例化ReactCompositeComponent組件那樣,先構造好構造函數,再進行實例化。那是由於HTML標籤就這麼幾個,react爲了咱們使用方便,內部已經幫咱們作了。咱們經過React.DOM.xxx訪問到的其實就是xxx這個標籤所對應的構造函數了。因而乎,咱們只須要直接實例化就好,好比:
const div = React.DOM.div({onClick:()=> {}}, '我是一個div組件的實例')
複製代碼
好,到這裏ReactDOMComponent組件的實例化過程已經講完了。下面咱們簡單講講ReactTextComponent的實例化過程。
上面提到了,其實ReactTextComponent是供給react內部使用的,用來把字符串wrap成組件。具體點講,react會把咱們傳遞進去的字符串用span包裹起來。 好比咱們傳進入的是「Parent count 1 times」,那麼最終生成是這樣的HTML片斷:
<span data-reactid=".r[2tqvw].[0]">Parent count 1 times</span>
複製代碼
wrap的這個動做是在ReactTextComponent的mountComponent方法裏面完成的:
/**
* Creates the markup for this text node. This node is not intended to have
* any features besides containing text content.
*
* @param {string} rootID DOM ID of the root node.
* @param {ReactReconcileTransaction} transaction
* @param {number} mountDepth number of components in the owner hierarchy
* @return {string} Markup for this text node.
* @internal
*/
mountComponent: function(rootID, transaction, mountDepth) {
ReactComponent.Mixin.mountComponent.call(
this,
rootID,
transaction,
mountDepth
);
return (
'<span ' + ReactMount.ATTR_NAME + '="' + escapeTextForBrowser(rootID) + '">' +
escapeTextForBrowser(this.props.text) +
'</span>'
);
},
複製代碼
那如今就只有一個問題值得探究了,那就是react內部在哪裏調用ReactTextComponent構造函數去作實例化呢?答曰:是在traverseAllChildren.js模塊裏面。
var traverseAllChildrenImpl =
function(children, nameSoFar, indexSoFar, callback, traverseContext) {
// 省略其餘代碼
else if (type === 'string') {
var normalizedText = new ReactTextComponent(children);
callback(traverseContext, normalizedText, storageName, indexSoFar);
subtreeCount += 1;
} else if (type === 'number') {
var normalizedNumber = new ReactTextComponent('' + children);
callback(traverseContext, normalizedNumber, storageName, indexSoFar);
subtreeCount += 1;
}
// 省略其餘代碼
};
複製代碼
看到醒目的new ReactTextComponent()
沒?
traverseAllChildrenImpl方法會在組件初次掛載的時候調用,對數字類型和字符串類型的值進行包裝,使得它們也能融入到react的組件體系裏面。
使用span對傳入的數字類型和字符串類型的值進行包裹這一特性會在reactv15.0.0 版本去掉,有changlog爲證:
No more extra
<span>
s. ReactDOM will now render plain text nodes interspersed with comment nodes that are used for demarcation. This gives us the same ability to update individual pieces of text, without creating extra nested nodes. If you were targeting these s in your CSS, you will need to adjust accordingly. You can always render them explicitly in your components. (@mwiencek in #5753)
好,ReactTextComponent組件的實例化簡單介紹完畢。
三種類型組件的實例化已經介紹完了。react組件實例化所涉及的源碼,無非包含如下幾個知識點
這些都是javascript比較基礎且重要的知識,這裏就不展開討論。那咱們是否是真的結束了該小節的探討了呢?不,源碼還留下了一個問題給咱們去思考。什麼問題呢?那就是ReactCompositeComponent和ReactDOMComponent組件實例化所須要的構造函數爲何採用層層包裹的方式來實現呢?。就拿ReactCompositeComponent組件來講,相比於當前的實現,createClass方法的實現爲何不是這麼寫呢:
createClass: function(spec) {
mixSpecIntoComponent(ReactCompositeComponentBase, spec);
return ReactCompositeComponentBase;
}
複製代碼
那是由於這種寫法的話,使用createClass方法建立的全部自定義組件都歸屬於同一種類型了。而源碼中用來判斷兩個組件實例是否屬於同一種類型的判斷是這樣的:
if (currentComponent.constructor === nextComponent.constructor) {
currentComponent.receiveComponent(nextComponent, transaction);
} else {
// 此處省略了代碼
}
複製代碼
假如按照咱們上面的的實現,咱們自定義了兩種類型的組件:
const Component = React.createClass({
render() { return 'Component';}
});
const AnotherComponent = React.createClass({
render() { return 'AnotherComponent';}
});
複製代碼
那麼上面的if語句的判斷條件永遠都是爲true。由於全部自定義了類型的組件都公用一個constructor:ReactCompositeComponentBase。並且,這種寫法還有一個問題就是,咱們的傳進去的spec掛載在prototype上後會存在相互覆蓋的風險,生命週期函數和render函數老是被後者覆蓋。
那是否是包一層就行了呢?像下面那樣:
createClass: function(spec) {
var Constructor = function(props, children) {}
this.construct.(props, children);
;
Constructor.prototype = new ReactCompositeComponentBase();
Constructor.prototype.constructor = Constructor;
mixSpecIntoComponent(Constructor, spec);
return Constructor;
}
複製代碼
我感受是能夠的,可是某位同窗在他的博文中說這樣寫的話會致使同以類型組件的全部實例均可以去篡改原型上的東西。是的,這種寫法是會的,可是包了兩層的寫法同樣會這樣的。因此我以爲不是這個理由。不信,咱們能夠試一試。假設咱們有如下代碼:
const Child = React.createClass({
render(){
return React.DOM.button({}, '我是子組件')
}
});
const Parent = React.createClass({
render(){
return React.DOM.div({},Child())
}
})
const test = Child();
const testPortotype = Object.getPrototypeOf(test);
testPortotype.render = function () {
return React.DOM.div({},'Child組件全部的實例的render都被我改了')
};
React.renderComponent(Parent({}), document.getElementById('app-root'));
複製代碼
正常狀況是這樣的:
篡改後是這樣的:
因此說,那個同窗的說法是有誤的。由於包了兩層的寫法(也就是react正式版本所採用的寫法)所建立的自定義組件全部的實例仍是指向同一個原型對象。
那麼爲何要採用包了兩層的寫法呢?真正的緣由,我目前沒法追溯到了。我猜真正的緣由是如第二個構造函數的名字ConvenienceConstructor所言,是爲了方便。爲了什麼方便呢?是爲了實例化的時候不用採用new操做符(這也是jsx目前的編譯目標,即講jsx標籤編譯爲普通的函數調用)。也就是說,相比於將<Component />
編譯爲new Component()
,react團隊更想把它編譯爲Component()
。
也許你的腦殼靈光一閃,爲什麼不這樣寫:
createClass: function(spec) {
var Constructor = function(props, children) {}
this.construct.(props, children);
return new Constructor();
;
Constructor.prototype = new ReactCompositeComponentBase();
Constructor.prototype.constructor = Constructor;
mixSpecIntoComponent(Constructor, spec);
return Constructor;
}
複製代碼
哈哈,這個想法太「天才」了。實際上,它會形成call stack溢出的。
好了,咱們自我折騰到這就差很少了。如需折騰,請自行探索吧。
組件如何實例化講完了,那麼咱們乘熱打鐵吧-趕忙在此基礎上,看看react組件是如何進行初始掛載的。
注意,從源碼實現來看(源碼中命名也多有不統一和不夠嚴謹之處),「組件掛載」準確地來說應該是「組件實例的掛載」。本文爲了闡述過程當中的簡便,採用「組件掛載」的說法。
在應用比較普遍的版本,好比reactv15.0.0中,組件初始掛載的入口函數叫render,相信是人盡皆知的。可能不多人知道這個render函數之前是叫renderComponent。那react是在哪一個版本中作了這個change呢?咱們看看changelog就知道了:
咱們能夠從上面截圖看出,入口函數名變動是從v0.12.0開始的。
咱們從renderComponent這個入口函數開始梳理的話,咱們能夠獲得如下的關於react組件初始掛載的大體流程圖:
從上面流程能夠看出,react組件初始掛載能夠劃分爲兩個步驟:
container.innerHTML = markup;
這種簡單的方式將markup插入到文檔流中。顯然,要想理解第一個步驟的現實原理,對遞歸的理解是十分必要的。咱們不妨看看百度百科對「遞歸算法」的釋義:
遞歸算法(英語:recursion algorithm)在計算機科學中是指一種經過重複將問題分解爲同類的子問題而解決問題的方法。遞歸式方法能夠被用於解決不少的計算機科學問題,所以它是計算機科學中十分重要的一個概念。絕大多數編程語言支持函數的自調用,在這些語言中函數能夠經過調用自身來進行遞歸。計算理論能夠證實遞歸的做用能夠徹底取代循環,所以在不少函數編程語言(如Scheme)中習慣用遞歸來實現循環。
關於對遞歸的理解,這篇博文值得一看。裏面也提到遞歸算法的應用場景:
思考一下,在react的組件初始掛載顯然是符合第1和第3點。
首先,react的組件系統的結構是遞歸的。父compositeComponent能夠由子compositeComponent,子DOMComponent 和子TextComponent組成,而子DOMComponent成爲父組件的時候,它又能夠由子compositeComponent組成。而遞歸結構裏面又有終結「遞去」的數據結構存在,那就是沒有子組件的DOMComponent和TextComponent,這是由當前上下文(DOM文檔樹)所決定的。這二者是組件系統結構的最底層的數據。因此,react組件存在使用遞歸算法的充分條件。
其次,由於reactv0.8.0中,react對「掛載(mount)某個組件」的定義是計算該組件實例所對應的HTML markup。因此,react對「掛載react組件」的這個問題的定義也是按照遞歸來定義的:要想掛載父組件,必須先掛載子組件,而子組件又有子組件......直到到了沒有子組件的DOMComponent和TextComponent這一層。
綜上兩個條件的吻合,react很巧妙地用上了遞歸算法。
上面的流程圖已經畫得很明顯了。遞歸開始是在this.mountComponent()
方法調用的時候。由於「this」有可能指向compositeComponent,DOMComponent和TextComponent,因此,流程開始分化。即便組件樹足夠深與廣,可是最終的最終確定會走到樹的末端節點,也就是沒有子組件的DOMComponent和TextComponent。從上面流程圖,咱們也能夠看出,每個類型組件的mountComponent方法的職責就是計算markup。總的來看,有去有回,咱們經過一個完整的遞歸過程完成了整一顆組件樹所對應的markup計算。
這篇博文提到了兩個遞歸模型:
我以爲react組件掛載過程是符合「在歸來的過程當中解決問題 」這個模型的。咱們不妨用僞代碼實現一下:
function mountComponent(componentInstance){
let markup = '';
if (typeof componentInstance === 'TextComponent'){
return markup += TextComponent所對應的markup;
}else if(typeof componentInstance === 'DOMComponent'){
if(componentInstance沒有子組件){
return markup += DOMComponent所對應的markup;
}else {
return markup +=
DOMComponent的開標籤 +
for(let child of DOMComponent.children){
return mountComponent(child);// 遞去,遞到最深處後,不斷地歸來;
} +
DOMComponent的閉標籤;
}
}else { // compositeComponent
const currentComponent = compositeComponent.render(); // 遞去
mountComponent(currentComponent); // 遞到最深處後,不斷地歸來
}
return markup;
}
複製代碼
通過一番闡述,咱們能夠看出,在reactv0.8.0這個版本中,所謂的「mount」一個組件就是一個純粹的字符串計算過程。期間,不涉及到任何的DOM操做。最後,react才把計算出來的字符串採用簡單的DOM操做插入到container中,簡單而粗暴。具體操做是發生在在mountImageIntoNode方法中:
/**
* @param {string} markup Markup string to place into the DOM Element.
* @param {DOMElement} container DOM Element to insert markup into.
* @param {boolean} shouldReuseMarkup Should reuse the existing markup in the
* container if possible.
*/
mountImageIntoNode: function(markup, container, shouldReuseMarkup) {
// 以上省略了不少代碼
// Asynchronously inject markup by ensuring that the container is not in
// the document when settings its `innerHTML`.
var parent = container.parentNode;
if (parent) {
var next = container.nextSibling;
parent.removeChild(container);
container.innerHTML = markup;
if (next) {
parent.insertBefore(container, next);
} else {
parent.appendChild(container);
}
} else {
container.innerHTML = markup;
}
}
複製代碼
把「mount component」定義爲「純字符串計算」能夠算是事關react的架構的事了。不過,在後期版本中,react廢棄了這種定義,採用了新的定義。這個變動具體是發生在react的v15.0.0的這個大版本中:
這種變動到底會產生多大的不一樣呢?這值得咱們撰寫另外一個篇章來闡述。
到了這裏,react組件初始掛載過程的探索幾乎完成得差很少了。不過,可能閱讀得比較細緻的同窗會有疑惑:「那上個小節所提到組件實例化有啥用呢?」。不急,咱們不妨把目光放在ReactCompositeComponent.js中的mountComponent方法的這行代碼:
mountComponent(rootID, transaction, mountDepth) {
// ......
this._renderedComponent = this._renderValidatedComponent();
// ......
}
複製代碼
而_renderValidatedComponent方法的實現是怎樣的呢:
_renderValidatedComponent: function() {
var renderedComponent;
ReactCurrentOwner.current = this;
try {
renderedComponent = this.render();
} catch (error) {
// IE8 requires `catch` in order to use `finally`.
throw error;
} finally {
ReactCurrentOwner.current = null;
}
("production" !== process.env.NODE_ENV ? invariant(
ReactComponent.isValidComponent(renderedComponent),
'%s.render(): A valid ReactComponent must be returned. You may have ' +
'returned null, undefined, an array, or some other invalid object.',
this.constructor.displayName || 'ReactCompositeComponent'
) : invariant(ReactComponent.isValidComponent(renderedComponent)));
return renderedComponent;
},
複製代碼
能夠看到在_renderValidatedComponent方法裏面,調用了組件實例的render方法。這個方法的實現就是咱們建立自定義組件時傳入的spec對象的render方法。而render用純js是怎麼寫的呢?咱們來複習一下:
render(){
return SomeComponent(
{},
Child(
{},
React.DOM.div({},'我是div啦,你暈了沒?')
)
)
}
複製代碼
咱們上面提到過,組件的實例化會經歷兩個函數調用,而最後一個函數調用纔是真正意義上的「實例化」(由於裏面涉及到了new操做符)。現在,這一連串的函數調用被延遲執行地包裹在render方法裏面。可是,一旦render函數被調用,一連串的組件實例化就發生了。
綜上所述,組件的實例化是發生了遞歸計算組件樹markup的過程當中的。幾乎能夠這麼說初始掛載過程當中,實例化組件就是爲了調用該實例的mountComponent方法。其實,從本質來看,這個方法名能夠更名爲「calculateComponentMarkup」更爲貼切。
好了,到了這裏,正文算是結束了。最後,咱們作個小總結。
在沒有引入react element, internalInstance, publicInstance等概念的reactv0.8.0中,react組件的初始掛載是簡單而粗暴的。整個過程的實現能夠劃分爲兩個步驟:
container.innerHTML = markup;
將計算出來的總的markup字符串插入到文檔流中。大師 L. Peter Deutsch 說過:To Iterate is Human, to Recurse, Divine.翻譯過來能夠是這樣:「循環迭代是人類的循環迭代,遞歸是神的遞歸」。可想而知,遞歸算法的應用須要多麼優雅而巧妙的思惟啊。
理解react組件初始掛載的實現的關鍵是理解三種不一樣組件類型的設計,組合和遞歸算法,尤爲是遞歸算法。react組件初始掛載也算是將遞歸算法應用得淋漓盡致了。我在想,這淋漓盡致的背後未嘗不是創建在將react組件系統設計得精密而其巧妙才行呢?