導(dǎo)語(yǔ):
最近組內(nèi)打算對(duì)部分項(xiàng)目的前端進(jìn)行重構(gòu),新的前端框架打算拋棄傳統(tǒng)的JQuery,使用這幾年非?;鸬腞eact,于是從頭開(kāi)始學(xué)習(xí)React,寫(xiě)篇博客記錄下學(xué)習(xí)的過(guò)程,也可以加固對(duì)React的理解。本文主要是記錄入門(mén)的經(jīng)歷,所以側(cè)重于實(shí)踐開(kāi)發(fā),所以就不啰嗦介紹背景什么的了。主要介紹原理、優(yōu)勢(shì)和實(shí)際應(yīng)用。
Why React?
我們知道,瀏覽器通過(guò)Http請(qǐng)求從各個(gè)異構(gòu)服務(wù)器獲取到html文檔。會(huì)根據(jù)包含相關(guān)信息的請(qǐng)求頭和請(qǐng)求體,將其解析并構(gòu)建成一個(gè)DOM樹(shù)。同時(shí),根據(jù)文檔獲取到相關(guān)的css文檔,這些文檔里面包含了許許多多的CSSOM。最后,這顆DOM樹(shù)和這些CSSOM會(huì)在瀏覽器內(nèi)存中形成一個(gè)Render樹(shù),瀏覽器就是根據(jù)這個(gè)Render樹(shù)渲染出我們最后看到的頁(yè)面的。而這些過(guò)程都是發(fā)生在渲染引擎中的,這與負(fù)責(zé)執(zhí)行動(dòng)態(tài)邏輯的JavaScript引擎是相分離的。因此,為了JS能夠方便操作DOM結(jié)構(gòu),渲染引擎會(huì)暴露一些接口供JavaScript調(diào)用
問(wèn)題就在這里,雖然通過(guò)暴露的接口,JS可以操作到DOM樹(shù)中的節(jié)點(diǎn)。但是性能其實(shí)不是很高,特別是對(duì)于一些復(fù)雜的網(wǎng)頁(yè),添加刪除節(jié)點(diǎn)會(huì)導(dǎo)致DOM節(jié)點(diǎn)的更新,這個(gè)開(kāi)銷是很大的。在之前,普遍都是通過(guò)JQuery來(lái)和DOM進(jìn)行交互:

在網(wǎng)頁(yè)設(shè)計(jì)越來(lái)越豐富,邏輯交互越來(lái)越復(fù)雜的情況下,頻繁地進(jìn)行DOM操作組件逐漸成為了性能的瓶頸。而以直接操作DOM的JQuery也不再像之前那么大一統(tǒng)。許許多多前端框架如雨后春筍般涌現(xiàn),如AngularJS,React,Vue等。其中最火的當(dāng)屬React,它提供了一套不同的,高效的方案來(lái)更新DOM。這種全新的解決方案就是“Virtual DOM”:

如上圖所所示,React會(huì)在內(nèi)存中根據(jù)DOM創(chuàng)建一個(gè)虛擬的DOM樹(shù)?;赗eact的開(kāi)發(fā)并不直接操作DOM,而是通過(guò)操作這棵虛擬DOM進(jìn)行的,每當(dāng)數(shù)據(jù)變化的時(shí)候,React會(huì)重新構(gòu)建整個(gè)DOM樹(shù),然后將當(dāng)前DOM樹(shù)和上個(gè)DOM樹(shù)進(jìn)行對(duì)比,得到DOM結(jié)構(gòu)的區(qū)別,然后僅僅將需要變化的部分進(jìn)行實(shí)際的瀏覽器DOM更新。既然最后還是會(huì)通過(guò)React來(lái)進(jìn)行對(duì)DOM的更新,那為何還會(huì)有性能的提升呢?原因在于React并不總是馬上對(duì)DOM樹(shù)所做的更改進(jìn)行更新,換而言之,就是你在虛擬DOM樹(shù)上做的操作,不保證馬上會(huì)產(chǎn)生實(shí)際的效果,它只會(huì)在你需要產(chǎn)生DOM樹(shù)更新的時(shí)候進(jìn)行更新。這樣的一個(gè)機(jī)制就使得React能夠等到一個(gè)事件循環(huán)的結(jié)尾,將若干個(gè)由數(shù)據(jù)影響的節(jié)點(diǎn)合并在一起,和實(shí)際DOM進(jìn)行比較,只操作Diff部分,而不是像傳統(tǒng)的js那樣需要更新DOM操作,就更新DOM樹(shù)一次,因而能達(dá)到提高性能的目的。同時(shí),在保證性能的同時(shí),React通過(guò)組件化的抽象概念,讓開(kāi)發(fā)者將不需要關(guān)注某個(gè)數(shù)據(jù)的變化該如何體現(xiàn)在DOM樹(shù)上,只需要關(guān)系某個(gè)數(shù)據(jù)更新時(shí),頁(yè)面是如何Render的。
React 使用
本文的所有例子均來(lái)自于React官方教程,不過(guò)做了些許改動(dòng),可以不需要搭設(shè)服務(wù)器即可運(yùn)行,所以也不需要引入JQuery。
需要引入的文件
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core5.6.16/browser.js"></script>
上面一共調(diào)用了三個(gè)庫(kù):react.js,react-dom.js,browser.js,它們必須優(yōu)先加載。其中react.js是react的核心庫(kù),react-dom.js 提供與DOM相關(guān)的功能,browser.js的作用實(shí)際是一種運(yùn)行時(shí)的編譯,當(dāng)執(zhí)行的代碼是jsx的語(yǔ)法,其實(shí)瀏覽器是無(wú)法識(shí)別然后報(bào)錯(cuò)的,但是引入了它以后就可以解析了,這也是為什么有時(shí)候并不需要將jsx的語(yǔ)法轉(zhuǎn)成js語(yǔ)法也能直接在瀏覽器中運(yùn)行。不過(guò)這個(gè)文件本身挺大的,而且jsx在客戶端解析成js語(yǔ)法需要一段時(shí)間,并且造成不必要的性能損耗。所以其實(shí)這個(gè)過(guò)程應(yīng)該由服務(wù)端完成。即我們?cè)陂_(kāi)發(fā)完成后應(yīng)該用gulp或者webpack這些工具將其解析打包后才發(fā)到生產(chǎn),這樣就不再需要引入browser.js這個(gè)文件了。
JSX語(yǔ)法
使用JSX語(yǔ)法,可以定義簡(jiǎn)潔而且較為熟知的樹(shù)狀語(yǔ)法結(jié)構(gòu)。其實(shí)它的基本語(yǔ)法規(guī)則也很簡(jiǎn)單:遇到HTML標(biāo)簽(<開(kāi)頭,并且第一個(gè)字母是小寫(xiě),如<div>),就用HTML規(guī)則解析;遇到代碼塊(以{開(kāi)口)就用JavaScript規(guī)則解析,遇到組件(<開(kāi)頭,并且第一個(gè)字母是大寫(xiě),如<Comment>),就是我們的React組件的類名了,所以寫(xiě)組件類的時(shí)候,別忘了類名以大寫(xiě)字母開(kāi)頭。
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
Components
Component就是其實(shí)就是React的核心思想,它通過(guò)把代碼封裝成組件的形式,然后每調(diào)用一次就會(huì)通過(guò)React的工廠方法來(lái)生成這個(gè)組件類的實(shí)例,并且根據(jù)注入的props或者state的值來(lái)輸出組件。
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
Hello, world! I am a CommentBox.
</div>
);
}
});
ReactDOM.render(
<CommentBox />,
document.getElementById('content')
);
ReactDOM.render是React的最基本用法,用于將模版轉(zhuǎn)為HTML語(yǔ)言,并插入指定的DOM節(jié)點(diǎn)中。下面兩小節(jié)代碼將展示如何將值傳到組件中,組件如何獲取。
Props
通過(guò)Props屬性,組件能夠讀取到從父組件傳遞過(guò)來(lái)的數(shù)據(jù),然后通過(guò)這些標(biāo)記渲染一些標(biāo)記,所有在父組件中傳過(guò)來(lái)的屬性都可以通過(guò)this.props.propertyName來(lái)獲取到,其中有一個(gè)特殊的屬性this.props.children,通過(guò)它你可以獲取到組件的所有子節(jié)點(diǎn),如下例所示:
var data = [
{author: "Pete Hunt", text: "This is one comment"},
{author: "Jordan Walke", text: "This is *another* comment"}
];
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h2 className="commentAuthor">
{this.props.author}
</h2>
{this.props.children}
</div>
);
}
});
var CommentList = React.createClass({
render: function() {
return (
<div className="commentList">
<Comment author="Pete Hunt">This is one comment</Comment>
<Comment author="Jordan Walke">This is *another* comment</Comment>
</div>
);
}
});
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
</div>
);
}
});
ReactDOM.render(
<CommentBox data={data} />,
document.getElementById('content')
);

綠色框是由Comment組件負(fù)責(zé)生成的,它被它的父組件CommentList(圖中的藍(lán)色框)調(diào)用了兩次,所以根據(jù)props中獲取的不同的數(shù)據(jù)實(shí)例化了兩次。接著組件CommentList又被頂層組件CommentBox所包括(圖中的紅色框)。React就是通過(guò)這樣的方式,將組件與組件之間的關(guān)系建立起來(lái)的,通過(guò)組合,可以做出各式各樣的我們需要的頁(yè)面。同時(shí),由于這些模塊化的組件,使得我們可以只關(guān)注傳入組件和改變組件的數(shù)據(jù),基本數(shù)據(jù)對(duì)了,組件對(duì)數(shù)據(jù)的渲染也就對(duì)了。同時(shí)我們也可以在后續(xù)的開(kāi)發(fā)中,將一些通用的組件抽出來(lái),代碼結(jié)構(gòu)清晰有調(diào)理。隨著開(kāi)發(fā)的不斷深入和代碼的不斷累積,這種優(yōu)勢(shì)就會(huì)越來(lái)越明顯。
State
在上一節(jié)的例子中,在組件CommentList中傳給Comment組件是寫(xiě)死的。我們知道,可以通過(guò)父組設(shè)置屬性,然后子組件中通過(guò)props獲取。但是如果子組件中的數(shù)據(jù)會(huì)不斷地改變(或者通過(guò)定時(shí)器,或者通過(guò)回調(diào),或者通過(guò)Ajax),子組件如何通過(guò)數(shù)據(jù)的變化來(lái)不斷地重新渲染呢?答案是State。
state和props一樣,都是用來(lái)描述組件的特性。只不過(guò)不同的是,對(duì)于props屬性,組件只會(huì)在對(duì)象實(shí)例的時(shí)候渲染并返回render函數(shù),而對(duì)state的設(shè)置則在組件的生命周期內(nèi)都有效,只要setState了,組件就會(huì)重新渲染并返回render。換而言之,就是如果你在組件實(shí)例以后再對(duì)props進(jìn)行更新,react并不能保證你的更新會(huì)反應(yīng)到VDOM甚至DOM上,而setState就可以。所以我們一般將哪些定義了以后就不再改變的特性放在props中,而隨著用戶交互或者定時(shí)觸發(fā)產(chǎn)生變化的一些特性,那放在state中將是更好的選擇。
現(xiàn)在,我們添加一個(gè)可以供用戶輸入的兩個(gè)輸入框和一個(gè)按鈕,讓用戶來(lái)輸入自己的名字和評(píng)論內(nèi)容,點(diǎn)擊提交后頁(yè)面將會(huì)顯示他們的評(píng)論。
//修改CommentList
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function (comment) {
return (
<Comment author={comment.author}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
//創(chuàng)建CommentForm組件,用于用戶輸入提交
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var author = this.refs.author.value.trim();
var text = this.refs.text.value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.refs.author.value = "";
this.refs.text.value = "";
alert("Submit!");
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
var data = [
{author: "YYQ", text: "這是一條評(píng)論"},
{author: "wuqke", text: "這是另外一條評(píng)論"}
];
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h3 className="commentAuthor">
{this.props.author}說(shuō):
</h3>
<span>{this.props.children}</span>
</div>
);
}
});
var CommentList = React.createClass({
render: function() {
var commentNodes = this.props.data.map(function (comment) {
return (
<Comment author={comment.author}>
{comment.text}
</Comment>
);
});
return (
<div className="commentList">
{commentNodes}
</div>
);
}
});
var CommentForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var author = this.refs.author.value.trim();
var text = this.refs.text.value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({author: author, text: text});
this.refs.author.value = "";
this.refs.text.value = "";
alert("Submit!");
return;
},
render: function() {
return (
<form className="commentForm" onSubmit={this.handleSubmit}>
<input type="text" placeholder="Your name" ref="author" />
<input type="text" placeholder="Say something..." ref="text" />
<input type="submit" value="Post" />
</form>
);
}
});
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},handleCommentSubmit: function(comment) {
var ndata = this.state.data;
ndata.push(comment);
this.setState({data:ndata});
},
componentDidMount: function() {
this.setState({data:this.props.data})
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit}/>
</div>
);
}
});
ReactDOM.render(
<CommentBox data={data} />,
document.getElementById('content')
);
//修改CommentBox組件,當(dāng)回調(diào)函數(shù)被觸發(fā)的時(shí)候,將comment添加到data中并且setState更新data
var CommentBox = React.createClass({
getInitialState: function() {
return {data: []};
},handleCommentSubmit: function(comment) {
var ndata = this.state.data;
ndata.push(comment);
this.setState({data:ndata});
},
componentDidMount: function() {
this.setState({data:this.props.datas})
},
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit}/>
</div>
);
}
});

----------樸實(shí)無(wú)華的前后效果分割線---------------

上面的例子,在CommentBox的render中添加了一個(gè)CommentForm組件,用于獲取用戶的輸入,同時(shí)添加了一個(gè)函數(shù)handleCommentSubmit(comment),函數(shù)接收comment參數(shù),做的事情很簡(jiǎn)單,就是和原來(lái)的數(shù)據(jù)data合并,并通過(guò)setState()更新數(shù)據(jù)data,該組件發(fā)現(xiàn)state變化以后,就會(huì)去重新渲染組件,最后在執(zhí)行render函數(shù),最終將變化反映在VDOM和DOM上。這讓我們可以只關(guān)注數(shù)據(jù)的變化,而不必去考慮太多DOM節(jié)點(diǎn)是否被更新的問(wèn)題。
組件的生命周期
React為其組件定義了生命周期的三個(gè)狀態(tài)。針對(duì)這三個(gè)狀態(tài)提供了7種鉤子函數(shù),方便使用者在不同狀態(tài)之前或者之后設(shè)置一些事件監(jiān)聽(tīng)或者邏輯處理。
- Mounting:組件正在被插入到DOM節(jié)點(diǎn)中
- Updating:組件正在被重新渲染,是否被更新取決于該組件是否有改變
- Unmouting:組件正在從DOM節(jié)點(diǎn)中移出
針對(duì)以上三個(gè)狀態(tài),都分別提供了兩種鉤子函數(shù),用于在進(jìn)入這個(gè)狀態(tài)之前(will函數(shù)),活著離開(kāi)這個(gè)狀態(tài)之后(did函數(shù))調(diào)用,理解了上面的狀態(tài),就會(huì)非常容易明白函數(shù)名和函數(shù)的調(diào)用時(shí)機(jī)了:
Mounting:
- componentWillMount()
- componentDidMount()
Updating:
- shouldComponentUpdate(object nextProps, object nextState)
- componentWillReceiveProps(object nextProps)
- componentWillUpdate(object nextProps, object nextState)
- componentDidUpdate(object prevProps, object prevState)
Unmouting:
- componentWillUnmount
總結(jié)
在理解React的思想和相關(guān)的一些概念后,其實(shí)很容易就可以使用React開(kāi)始開(kāi)發(fā)。個(gè)人總結(jié)了一下,只要了解好React會(huì)在內(nèi)存中創(chuàng)建一個(gè)Virtual DOM,所有的React組件都是在更新這棵VDOM(通過(guò)ref獲得DOM節(jié)點(diǎn)更新除外),然后React才會(huì)根據(jù)這棵VDOM和DOM運(yùn)用一個(gè)加速Diff算法,做一個(gè)差異覆蓋。
接著,可以通過(guò)Props和State來(lái)傳遞數(shù)據(jù)(但是數(shù)據(jù)的傳遞是單向的)。其中,setState會(huì)使得組件重新計(jì)算并執(zhí)行render函數(shù),從而做到組件隨著數(shù)據(jù)變化渲染。最后,理解了組件的生命周期的三個(gè)狀態(tài),我們就可以在這三個(gè)狀態(tài)之前或者之后調(diào)用的鉤子函數(shù)中綁定事件,處理相關(guān)邏輯,也可以從父組件中傳入回調(diào)函數(shù),在子組件中調(diào)用該函數(shù),做到子組件和父組件的通信,解決單數(shù)流單向傳遞的一些問(wèn)題。
感覺(jué)真正難的,是如果使用React及周邊生態(tài)搭建起一套行之有效的架構(gòu)。雖然React入門(mén)比較簡(jiǎn)單,但是真正開(kāi)發(fā)起來(lái),其實(shí)是一個(gè)漫長(zhǎng)的過(guò)程,不僅僅是思維的轉(zhuǎn)變,整個(gè)技術(shù)??赡芤惨浜现鴮W(xué)習(xí)。但這也是許多的前端開(kāi)發(fā)者相信正是因?yàn)檫@些,它可能是未來(lái)前端的方向。
參考資料
- 書(shū)籍:《React:引領(lǐng)未來(lái)的用戶界面開(kāi)發(fā)框架》 電子工業(yè)出版社
- 阮一峰老師的 React入門(mén)實(shí)例教程
- 本文的例子絕大部分來(lái)自 React官方文檔 并作了小量的修改。