? ?? ?最近開始了React的學(xué)習(xí)之旅,感覺這些框架都是一個(gè)套路莲组。早先有學(xué)過VUE诊胞,它就像是在寫模板,大部分是HTML锹杈。而React是在寫模塊撵孤,大部分是寫js。
? ?? ?React中有一個(gè)神奇的技術(shù)就是JSX竭望,VUE2.0中引入也了React的JSX技術(shù)邪码,但是React為什么要使用JSX技術(shù)呢?我們先來看官方的觀點(diǎn):
JSX is not required to use React, but itmakes code more readable, and writing it feels like writing HTML.
? ?? ?React并不是必須使用JSX咬清,但是JSX可以增加代碼的可讀性闭专,并且寫起來就像寫HTML一樣。
? ?? ?JSX為什么可以增加代碼的可讀性呢?
??其實(shí)是這樣的旧烧。React中有一個(gè)核心的機(jī)制影钉,虛擬DOM。虛擬DOM是一個(gè)原生的JavaScript對象掘剪,并且具有真實(shí)DOM信息的一些屬性平委。它在APP與DOM之間建立了一個(gè)抽象層,當(dāng)數(shù)據(jù)和狀態(tài)發(fā)生改變時(shí)杖小,只需要在虛擬DOM上進(jìn)行操作肆汹,最后再同步到真實(shí)的DOM中。由此可見予权,虛擬DOM可以減少直接操作DOM的次數(shù)昂勉,減少不必要的[repaint和reflow],提高性能扫腺。
??虛擬DOM是如何更新的呢岗照?
??當(dāng)狀態(tài)和數(shù)據(jù)發(fā)生變化時(shí),React會(huì)生成新的虛擬DOM樹笆环,然后將新的虛擬DOM樹與舊虛擬DOM樹進(jìn)行對比攒至,這用到了diff算法。diff算法的核心是對樹進(jìn)行分層比較躁劣,并且只對兩棵樹的同層進(jìn)行比較迫吐。React會(huì)對新舊兩棵樹進(jìn)行一個(gè)深度優(yōu)先的遍歷,每遍歷到一個(gè)節(jié)點(diǎn)就把該節(jié)點(diǎn)和新的樹進(jìn)行對比账忘。當(dāng)發(fā)現(xiàn)節(jié)點(diǎn)已經(jīng)不存在志膀,則該節(jié)點(diǎn)及其子節(jié)點(diǎn)會(huì)被完全刪除掉熙宇,不會(huì)用于進(jìn)一步的比較。這樣只需要對樹進(jìn)行一次遍歷溉浙,便能完成整個(gè)DOM樹的比較烫止。
??如果節(jié)點(diǎn)發(fā)生了跨級移動(dòng),則被移動(dòng)的節(jié)點(diǎn)會(huì)直接被銷毀戳稽,并且會(huì)創(chuàng)建新節(jié)點(diǎn)掛載到馆蠕,目標(biāo)DOM上。由此可見惊奇,在編寫代碼是要加強(qiáng)DOM結(jié)構(gòu)的穩(wěn)定性互躬,如果有必要更改,可以利用CSS來隱藏或顯示某些節(jié)點(diǎn)颂郎,而不是真的移除或添加DOM節(jié)點(diǎn)吨铸。其實(shí)一旦接受了React的寫法,就會(huì)發(fā)現(xiàn)前面所說的那種移動(dòng)的寫法幾乎不會(huì)被考慮祖秒,這里可以說是React限制了某些寫法诞吱,不過遵守這些實(shí)踐確實(shí)會(huì)使得React有更好的渲染性能。
??在React源碼中竭缝,diff算法的核心部分為_updateChildren方法房维,它是ReactMultiChild.js內(nèi)部的一個(gè)方法。
_updateChildren: function (nextNestedChildrenElements, transaction, context) {
var prevChildren = this._renderedChildren;
var removedNodes = {};
var mountImages = [];
var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, mountImages, removedNodes, transaction, context);
if (!nextChildren && !prevChildren) {
return;
}
var updates = null;
var name;
// `nextIndex` will increment for each child in `nextChildren`, but
// `lastIndex` will be the last index visited in `prevChildren`.
var nextIndex = 0;
var lastIndex = 0;
// `nextMountIndex` will increment for each newly mounted child.
var nextMountIndex = 0;
var lastPlacedNode = null;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
// 同一個(gè)引用抬纸,說明是使用的同一個(gè)component,所以我們需要移動(dòng)已有的子節(jié)點(diǎn)
updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
// The `removedNodes` loop below will actually remove the child.
}
// The child must be instantiated before it's mounted.
updates = enqueue(updates, this._mountChildAtIndex(nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context));
nextMountIndex++;
}
nextIndex++;
lastPlacedNode = ReactReconciler.getHostNode(nextChild);
}
// Remove children that are no longer present.
for (name in removedNodes) {
if (removedNodes.hasOwnProperty(name)) {
// 添加新的子節(jié)點(diǎn)在指定的位置上
updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
}
}
if (updates) {
processQueue(this, updates);
}
this._renderedChildren = nextChildren;
if (process.env.NODE_ENV !== 'production') {
setChildrenForInstrumentation.call(this, nextChildren);
}
}
關(guān)于react的渲染和更新機(jī)制的具體操作將在下一篇文章中介紹咙俩,現(xiàn)在我們回歸最前面的問題,JSX在React中有什么作用呢湿故?
??如果不使用JSX阿趁,官方提供了React.createElement()機(jī)制,如下所示:
var slider = React.createElement(
'div',
{className: 'sidebar'},
null
)
我們來看一看React.createElement的源碼:
ReactElement.createElement = function (type, config, children) {
var propName;
//初始化
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
// 提取參數(shù)
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坛猪,并進(jìn)行存儲(chǔ)
for (propName in config) {
if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// 提取child信息
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
// only one child
props.children = children;
} else if (childrenLength > 1) {
// multiple child
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 提取defaultProps脖阵,對于沒有的屬性設(shè)置默認(rèn)值
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 返回一個(gè)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,
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner
};
return element;
}
由此可見,一個(gè) ReactElement 實(shí)例具有DOM的4個(gè)必要屬性墅茉,分別是type命黔、key、ref就斤、props悍募。并且它沒有方法,原型也沒有任何屬性和方法洋机。ReacrtElement的實(shí)例對象就是我們前面所說的虛擬的DOM對象坠宴。
??因此,完全可以用JavaScript構(gòu)建完整的界面DOM樹绷旗,就像可以用JavaScript創(chuàng)建真實(shí)DOM喜鼓。但這樣的代碼可讀性并不好忧设,于是就出現(xiàn)了JSX。它是javaScript的擴(kuò)展颠通,并且提供了語義化標(biāo)簽,使用熟悉的語法定義HTML樹膀懈,并且程序結(jié)構(gòu)更加直觀化顿锰。將上面的代碼用JSX改寫后如下:
var slider =∑袈А(<div className="sidebar" />)
XML語法直接加入到JavaScript代碼中硼控,讓你能夠高效的通過代碼而不是模板來定義界面。之后JSX通過翻譯器轉(zhuǎn)換到純JavaScript再由瀏覽器執(zhí)行胳赌±魏常可以使用react-tools和babel將將jsx轉(zhuǎn)碼成js,但是前者已經(jīng)被放棄了疑苫,下面我們就介紹babel的轉(zhuǎn)碼原理熏版。
??babel是一個(gè)轉(zhuǎn)碼器,但是它更像是一個(gè)編譯器捍掺,將源代碼編譯成適合開發(fā)環(huán)境的另一種語言代碼撼短。我們知道編譯過程一般有5個(gè)階段:詞法分析;語法分析挺勿;語義分析與中間代碼產(chǎn)生曲横;優(yōu)化;目標(biāo)代碼生成不瓶。
??詞法分析是以詞法規(guī)則為依據(jù)對輸入的源程序進(jìn)行單詞及其屬性的識別禾嫉,識別出一個(gè)個(gè)單詞符號;
??語法分析是根據(jù)語言的語法規(guī)則蚊丐,把單詞流組成各類語法單位熙参,如:短語、句子麦备、過程尊惰、程序。
??語義分析是將檢查程序的語義正確性泥兰,以保證程序各部分能有意義的結(jié)合在一起弄屡,為以后的代碼生成階段收集類型信息;
??中間代碼是不依賴于機(jī)器但是又便于生成依賴于機(jī)器的目標(biāo)代碼的一種結(jié)構(gòu)簡單鞋诗、含義明確的記號系統(tǒng)膀捷;
??代碼優(yōu)化是對前面產(chǎn)生的中間代碼進(jìn)行加工變換,以期在最后階段能產(chǎn)生更為高效的目標(biāo)代碼削彬;
??目標(biāo)代碼生成是把經(jīng)過優(yōu)化的中間代碼轉(zhuǎn)化成特定 機(jī)器上的低級語言代碼全庸。
??babel的工作過程分為三個(gè)階段:
??Step1:解析秀仲,將代碼字符串解析成抽象語法樹;
??Step2:遍歷做轉(zhuǎn)換壶笼,遍歷抽象語法樹神僵,并將需要變化的地方直接在該語法樹對象上做修改;
??Step3:生成新代碼覆劈,將變換后的新抽象語法樹生成對應(yīng)的字符串代碼保礼。
??其中,在第Step1解析階段包含詞法分析责语、語法分析和語義分析炮障,驗(yàn)證語法和語義的正確性。同時(shí)坤候,由字符串代碼生成抽象語法樹胁赢,它是源代碼抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式。
??總結(jié):
??React中引入了虛擬DOM白筹,它是一個(gè)原生的JavaScript對象智末,相對于DOM對象來說更易于處理和操作。為了使代碼結(jié)構(gòu)更加清晰徒河,React又引入了JSX語法吹害,它是JavaScript擴(kuò)展語法,同時(shí)也是帶屬性的樹狀結(jié)構(gòu)語法虚青,增加了代碼的可讀性它呀。使用JSX語法必須使用轉(zhuǎn)碼器,將JSX轉(zhuǎn)換為JS棒厘,代碼才可以在瀏覽器上執(zhí)行纵穿。
? ?? ?參考資料:
? ?? ?https://github.com/livoras/blog/issues/13
? ?? ?http://www.infoq.com/cn/articles/react-jsx-and-component