導語:
最近組內打算對部分項目的前端進行重構,新的前端框架打算拋棄傳統的JQuery,使用這幾年非常火的React,于是從頭開始學習React,寫篇博客記錄下學習的過程,也可以加固對React的理解。本文主要是記錄入門的經歷,所以側重于實踐開發,所以就不啰嗦介紹背景什么的了。主要介紹原理、優勢和實際應用。
Why React?
我們知道,瀏覽器通過Http請求從各個異構服務器獲取到html文檔。會根據包含相關信息的請求頭和請求體,將其解析并構建成一個DOM樹。同時,根據文檔獲取到相關的css文檔,這些文檔里面包含了許許多多的CSSOM。最后,這顆DOM樹和這些CSSOM會在瀏覽器內存中形成一個Render樹,瀏覽器就是根據這個Render樹渲染出我們最后看到的頁面的。而這些過程都是發生在渲染引擎中的,這與負責執行動態邏輯的JavaScript引擎是相分離的。因此,為了JS能夠方便操作DOM結構,渲染引擎會暴露一些接口供JavaScript調用
問題就在這里,雖然通過暴露的接口,JS可以操作到DOM樹中的節點。但是性能其實不是很高,特別是對于一些復雜的網頁,添加刪除節點會導致DOM節點的更新,這個開銷是很大的。在之前,普遍都是通過JQuery來和DOM進行交互:
在網頁設計越來越豐富,邏輯交互越來越復雜的情況下,頻繁地進行DOM操作組件逐漸成為了性能的瓶頸。而以直接操作DOM的JQuery也不再像之前那么大一統。許許多多前端框架如雨后春筍般涌現,如AngularJS,React,Vue等。其中最火的當屬React,它提供了一套不同的,高效的方案來更新DOM。這種全新的解決方案就是“Virtual DOM”:
如上圖所所示,React會在內存中根據DOM創建一個虛擬的DOM樹。基于React的開發并不直接操作DOM,而是通過操作這棵虛擬DOM進行的,每當數據變化的時候,React會重新構建整個DOM樹,然后將當前DOM樹和上個DOM樹進行對比,得到DOM結構的區別,然后僅僅將需要變化的部分進行實際的瀏覽器DOM更新。既然最后還是會通過React來進行對DOM的更新,那為何還會有性能的提升呢?原因在于React并不總是馬上對DOM樹所做的更改進行更新,換而言之,就是你在虛擬DOM樹上做的操作,不保證馬上會產生實際的效果,它只會在你需要產生DOM樹更新的時候進行更新。這樣的一個機制就使得React能夠等到一個事件循環的結尾,將若干個由數據影響的節點合并在一起,和實際DOM進行比較,只操作Diff部分,而不是像傳統的js那樣需要更新DOM操作,就更新DOM樹一次,因而能達到提高性能的目的。同時,在保證性能的同時,React通過組件化的抽象概念,讓開發者將不需要關注某個數據的變化該如何體現在DOM樹上,只需要關系某個數據更新時,頁面是如何Render的。
React 使用
本文的所有例子均來自于React官方教程,不過做了些許改動,可以不需要搭設服務器即可運行,所以也不需要引入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>
上面一共調用了三個庫:react.js,react-dom.js,browser.js,它們必須優先加載。其中react.js是react的核心庫,react-dom.js 提供與DOM相關的功能,browser.js的作用實際是一種運行時的編譯,當執行的代碼是jsx的語法,其實瀏覽器是無法識別然后報錯的,但是引入了它以后就可以解析了,這也是為什么有時候并不需要將jsx的語法轉成js語法也能直接在瀏覽器中運行。不過這個文件本身挺大的,而且jsx在客戶端解析成js語法需要一段時間,并且造成不必要的性能損耗。所以其實這個過程應該由服務端完成。即我們在開發完成后應該用gulp或者webpack這些工具將其解析打包后才發到生產,這樣就不再需要引入browser.js這個文件了。
JSX語法
使用JSX語法,可以定義簡潔而且較為熟知的樹狀語法結構。其實它的基本語法規則也很簡單:遇到HTML標簽(<開頭,并且第一個字母是小寫,如<div>),就用HTML規則解析;遇到代碼塊(以{開口)就用JavaScript規則解析,遇到組件(<開頭,并且第一個字母是大寫,如<Comment>),就是我們的React組件的類名了,所以寫組件類的時候,別忘了類名以大寫字母開頭。
var CommentBox = React.createClass({
render: function() {
return (
<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>
);
}
});
Components
Component就是其實就是React的核心思想,它通過把代碼封裝成組件的形式,然后每調用一次就會通過React的工廠方法來生成這個組件類的實例,并且根據注入的props或者state的值來輸出組件。
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的最基本用法,用于將模版轉為HTML語言,并插入指定的DOM節點中。下面兩小節代碼將展示如何將值傳到組件中,組件如何獲取。
Props
通過Props屬性,組件能夠讀取到從父組件傳遞過來的數據,然后通過這些標記渲染一些標記,所有在父組件中傳過來的屬性都可以通過this.props.propertyName來獲取到,其中有一個特殊的屬性this.props.children,通過它你可以獲取到組件的所有子節點,如下例所示:
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組件負責生成的,它被它的父組件CommentList(圖中的藍色框)調用了兩次,所以根據props中獲取的不同的數據實例化了兩次。接著組件CommentList又被頂層組件CommentBox所包括(圖中的紅色框)。React就是通過這樣的方式,將組件與組件之間的關系建立起來的,通過組合,可以做出各式各樣的我們需要的頁面。同時,由于這些模塊化的組件,使得我們可以只關注傳入組件和改變組件的數據,基本數據對了,組件對數據的渲染也就對了。同時我們也可以在后續的開發中,將一些通用的組件抽出來,代碼結構清晰有調理。隨著開發的不斷深入和代碼的不斷累積,這種優勢就會越來越明顯。
State
在上一節的例子中,在組件CommentList中傳給Comment組件是寫死的。我們知道,可以通過父組設置屬性,然后子組件中通過props獲取。但是如果子組件中的數據會不斷地改變(或者通過定時器,或者通過回調,或者通過Ajax),子組件如何通過數據的變化來不斷地重新渲染呢?答案是State。
state和props一樣,都是用來描述組件的特性。只不過不同的是,對于props屬性,組件只會在對象實例的時候渲染并返回render函數,而對state的設置則在組件的生命周期內都有效,只要setState了,組件就會重新渲染并返回render。換而言之,就是如果你在組件實例以后再對props進行更新,react并不能保證你的更新會反應到VDOM甚至DOM上,而setState就可以。所以我們一般將哪些定義了以后就不再改變的特性放在props中,而隨著用戶交互或者定時觸發產生變化的一些特性,那放在state中將是更好的選擇。
現在,我們添加一個可以供用戶輸入的兩個輸入框和一個按鈕,讓用戶來輸入自己的名字和評論內容,點擊提交后頁面將會顯示他們的評論。
//修改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>
);
}
});
//創建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: "這是一條評論"},
{author: "wuqke", text: "這是另外一條評論"}
];
var Comment = React.createClass({
render: function() {
return (
<div className="comment">
<h3 className="commentAuthor">
{this.props.author}說:
</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組件,當回調函數被觸發的時候,將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>
);
}
});
----------樸實無華的前后效果分割線---------------
上面的例子,在CommentBox的render中添加了一個CommentForm組件,用于獲取用戶的輸入,同時添加了一個函數handleCommentSubmit(comment),函數接收comment參數,做的事情很簡單,就是和原來的數據data合并,并通過setState()更新數據data,該組件發現state變化以后,就會去重新渲染組件,最后在執行render函數,最終將變化反映在VDOM和DOM上。這讓我們可以只關注數據的變化,而不必去考慮太多DOM節點是否被更新的問題。
組件的生命周期
React為其組件定義了生命周期的三個狀態。針對這三個狀態提供了7種鉤子函數,方便使用者在不同狀態之前或者之后設置一些事件監聽或者邏輯處理。
- Mounting:組件正在被插入到DOM節點中
- Updating:組件正在被重新渲染,是否被更新取決于該組件是否有改變
- Unmouting:組件正在從DOM節點中移出
針對以上三個狀態,都分別提供了兩種鉤子函數,用于在進入這個狀態之前(will函數),活著離開這個狀態之后(did函數)調用,理解了上面的狀態,就會非常容易明白函數名和函數的調用時機了:
Mounting:
- componentWillMount()
- componentDidMount()
Updating:
- shouldComponentUpdate(object nextProps, object nextState)
- componentWillReceiveProps(object nextProps)
- componentWillUpdate(object nextProps, object nextState)
- componentDidUpdate(object prevProps, object prevState)
Unmouting:
- componentWillUnmount
總結
在理解React的思想和相關的一些概念后,其實很容易就可以使用React開始開發。個人總結了一下,只要了解好React會在內存中創建一個Virtual DOM,所有的React組件都是在更新這棵VDOM(通過ref獲得DOM節點更新除外),然后React才會根據這棵VDOM和DOM運用一個加速Diff算法,做一個差異覆蓋。
接著,可以通過Props和State來傳遞數據(但是數據的傳遞是單向的)。其中,setState會使得組件重新計算并執行render函數,從而做到組件隨著數據變化渲染。最后,理解了組件的生命周期的三個狀態,我們就可以在這三個狀態之前或者之后調用的鉤子函數中綁定事件,處理相關邏輯,也可以從父組件中傳入回調函數,在子組件中調用該函數,做到子組件和父組件的通信,解決單數流單向傳遞的一些問題。
感覺真正難的,是如果使用React及周邊生態搭建起一套行之有效的架構。雖然React入門比較簡單,但是真正開發起來,其實是一個漫長的過程,不僅僅是思維的轉變,整個技術棧可能也要配合著學習。但這也是許多的前端開發者相信正是因為這些,它可能是未來前端的方向。
參考資料
- 書籍:《React:引領未來的用戶界面開發框架》 電子工業出版社
- 阮一峰老師的 React入門實例教程
- 本文的例子絕大部分來自 React官方文檔 并作了小量的修改。