HTML模版
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../build/browse.min.js"></script>
<div id="example"></div>
<script type="text/babel">
//** Our code goes here! **
</script>
之后出現的React代碼嵌套入模版中。
1. Hello world
ReactDOM.render{
<h1>Hello world</h1>,
document.getElementById('root')
};
這段代碼將一個一級標題插入到指定的DOM節點中。
javaScript筆記
React是一個JS庫,學習前請首先確保擁有JS基礎。在之后的例子中會使用ES6的語法,但并不多因為相對較新。但鼓勵大家對arrow functions,classes,template literals,let,const statements等熟悉起來。
2. JSX介紹
const element = <h1>Hello,world</h1>;
這段看起來既像是字符串,又像是HTML的代碼就是JSX。JSX是對JavaScript的語法擴展,它非常強大。
JSX的意義
“表達”邏輯和UI邏輯天生一對兒,React天生就是來撮合它們的,比如產生的“事件“如何觸發?頁面的狀態如何隨時間不斷改變?數據如何時刻準備著在頁面上顯示?
與人為的將邏輯處理和標記放置在獨立的文件中不同(.js文件和.html文件),React能夠將“交合” 的組件??用不同的規則解析(js規則和html規則)。在后續章節會詳細介紹組件,但現在如果你還不適應在JS代碼中寫 html代碼,就快點適應??。
React開發不一定必須寫JSX,但好處多多。
在JSX文件中的嵌入語句
在JSX文件的html標記中,可以直接嵌入JS語句,當然需要用大括號進行轉義。語句無論運算表達式、函數表達式、引用等,只要它合法就行。例如:
2+2;
user.firstname;
formatName(user);
const user = {
firstName : 'Harper',
lastName : 'Perez'
};
const element = (
<h1>
hello, {formatName(user)}!
</h1>
);
function formatName(user){
return user.firstName + ' ' + user.lastName;
}
ReactDOM.render(
element,
document.getElementById('root')
);
JSX也是一種表達式
從底層上講,JSX編譯后會變成js函數調用或js對象。因此,允許在if語句、loop語句、賦值語句中使用JSX,甚至可以將它們作為參數、函數返回值使用。例如:
function getGreeting(user){
if(user){
return <h1>Hello,{formatName(user)}!</h1>;
}
return <h1>Hello,Stranger.</h1>;
}
JSX的屬性
可以使用指定字符串+引號作為JSX的屬性:
const element = <div tabIndex = "0"></div>;
也可以在大括號中嵌入js語句來作為屬性:
const element = <img src = {user.avatarUrl}></img>
當嵌入js語句作為參數時,千萬不要在大括號外加引號。你要么在字符串外加引號,要么在js語句外加大括號,不要同時用。
警告
*JSX語法和javaScript語法更接近,React DOM使用的是駱駝命名法而不是HTML屬性的命名法.
例如:在JSX中類名是className,因此tabindex命名為“tabIndex”.*
JSX的子標簽
JSX中,如果一個標簽為空,你可以用/>立刻關閉它,就像XML語法一樣。而不必須使用<></>。
const element = <img src = {user.avataUrl}/>;
JSX的標簽也可以包含子標簽:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX 防止注入攻擊
JSX嵌入用戶輸入是很安全的:
const title = response.potentiallyMaliciousInput;
//This is safe;
const element = <h1>{title}</h1>;
默認情況下, 在渲染之前, React DOM 會格式化(escapes) JSX中的所有值。從而保證用戶無法注入任何應用之外的代碼。在被渲染之前,所有的數據都被轉義成為了字符串處理。 以避免XSS(跨站腳本) 攻擊。
JSX表示對象
Babel將JSX編譯成React.createElement() 調用。
下面兩個例子是完全相同的:
const element = (
<h1 className = "greeting">
Hello,world!
</h1>
);
const element = React.createElement(
'h1',
{className:'greeting'},
'Hello,world!'
);
React.createElement()會執行一些檢查來幫助你寫無Bug的代碼,但基本上它會創建如下所示的對象:
const element = {
type:'h1',
props:{
className:'greeting',
children:'Hello,world'
}
};
這些對象叫做React元素。你可以把它們想象成你想在屏幕上看到的東西的一種描述。React讀取這些對象,使用它們構建DOM節點并保持數據的更新。
下一節我們將介紹如何將React元素渲染到DOM節點中。
提示:
我們推薦在你所用的編輯器中加入Babel插件,這樣ES6和JSX代碼就可以顯示高亮。
3. 渲染元素
元素是React應用的最小構建單元
元素描述如下所示:
const element = <h1>Hello, world</h1>;
和瀏覽器的DOM元素不同,React元素是純對象,并且創建起來耗費更低。React DOM所關心的是DOM節點與React元素的匹配。
注意
大家可能會講元素概念與組件概念混淆。我們會在下一章節介紹組件。元素和組件的關系是組成被組成的關系,簡而言之,組件是由元素組成的。
在DOM節點中渲染元素
在HTML文件中我們處處能見到<div>:
<div id = "root"></div>
我們稱之為根DOM節點,因為其中的一切都是由React DOM管理。
React構建的應用通常只有一個根節點。如果你將React繼承入已存在的應用中,如你所愿,你可能有很多個相互獨立的根節點。
我們使用ReactDOM.render()來將React元素渲染到根DOM節點中(或稱作“插入”),代碼如下:
const element = <h1>Hello , world</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
這段代碼在頁面中顯示hello world。
更新被渲染元素
React元素是不可改變的(immutable)。一個元素一旦被創建,你就不能再改變它的子節點和屬性。一個元素很像電影中一幀:它表示UI在某一個時刻內的展示。
到目前為止我們所了解的知識,更新UI只有一條路:創建新的元素,然后通過ReactDOM.render()渲染它。
請看下面的時鐘例子代碼:
function tick(){
const element = (
<div>
<h1>Hello world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick,1000);
tick()函數每一秒被setInterval回調一次,每一次回調都要調用ReactDOM.render()函數。
注意
在實踐中,大多數React應用只調用ReactDOM.render()一次。在下一節中我們將學習怎樣將這樣的代碼封裝在狀態化的組件中。
React僅僅更新那些有必要更新的內容
React DOM會像元素及其所有子元素與前一個(狀態/內容)相比較,而后僅僅運用于有必要更新的DOM節點,并將這些DOM節點帶入期望的狀態。
在上一個例子中,用瀏覽器工具看到的效果如下:
從上圖看到,當我們創建一個元素,即使它每個節拍都在描述整個UI樹,那也僅僅是那些內容時刻在變化的文本節點被React DOM更新。
在我們的經驗中,思考UI的顯示如何和“喂數據”的時候一致,而不是思考怎樣才能時刻更新UI,這樣才能避免很多Bug。
4. 組件和屬性
組件能夠讓你將UI分割成獨立的、可重用的片段,并在隔離中思考每一個片段。
從概念上將,組件更像是js中的函數。它們能夠接收任意輸入(我們稱為屬性)并能返回React元素,來描述UI應該展現什么。
函數組件和類組件
最簡單的定義一個組件的方式是寫一個js函數,代碼如下:
function Welcome(props){
return <h1>Hello, {props.name}</h1>;
}
這個函數是一個有效的React組件,因為它接受了單特征對象參數并且返回了一個React元素。我們成這為組件函數化,因為它們和js函數一模一樣。
你也可以使用ES6類來定義組件,代碼如下:
class Welcome extends React.Component{
render(){
return <h1>hello, {this.props.name}</h1>;
}
}
以上兩個組件在React中是等效的。
class有一些額外特征我們會在下一節討論。
渲染一個組件
在前一章節,我們只見過表示DOM標簽的React元素:
const element = <div />;
而元素還能夠表示用戶自定義的組件:
const element = <Welcome name = "Sara" />;
這段代碼中,當React看到一個表示用戶自定義組件的元素時,它會把JSX參數作為一個單一對象傳遞到這個組件。我們把這個對象稱為屬性。
例如,下面這段代碼把“Hello,Sara”顯示在屏幕上:
function Welcome(props){
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name = "Sara"/>;
ReactDOM.render(
element,
document.getElementById('root')
);
讓我們看看上面一段代碼發生了什么:
//1. 我們調用ReactDOM.render()函數,函數帶有一個元素<Welcome name="Sara" />
//2. React調用Welcome組件,并將{name:'Sara'}作為屬性。
//3. Welcome組件將<h1>Hello,Sara</h1>作為返回值返回。
//4. React DOM將<h1>Hello,Sara</h1>更新到DOM節點中。
附加說明
組件命名的首字母必須大寫,否則會報錯。
組合組件
組件在它們的輸出中能夠涉及其他組件。這個特征能夠讓我們在任意細節層次(LOD)使用相同的組件進行抽象。一個按鈕、表單、對話、場景:在React應用中,所有這些都可以作為組件表達。
例如下面的代碼,我們創建了一個組件 App,它多次對Welcome組件進行渲染:
function Welcome(props){
return <h1>Hello, {props.name}</h1>;
}
function App(){
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
};
提煉組件
不要害怕把組件分割成更小的組件。
思考下這個Comment組件,代碼如下:
function Comment(props){
return(
<div className="Comment">
<div className = "UserInfo">
<img className= "Avatar"
src={props.author.avatarUrl}
alt={props.author.name} />
<div className="UserInfo-name">{props.author.name}</div>
</div>
<div className = "Comment-text">{props.text]</div>
<div className="Comment-date">{formatDate(props.date)</div>
</div>
);
}
Comment有三個屬性,作者(對象)、文本(字符串)、日期(日期類型),用來在網頁中描述評論的社交內容。
這個組件因為嵌套層太多,改變起來非常復雜,并且之后每一部分也很難重用。下面我們就對它進行改寫,把它分割成一個個的小組件:
數顯,我們來分割Avatar組件,代碼如下:
function Avatar(props){
return(
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
然后我們對Comment做了一些小修改,代碼如下:
function Comment(props){
return(
<div className = "Comment">
<div className = "UserInfo">
<Avatar user={props.author} />
<div className = "UserInfo-name">{props.author.name}</div>
</div>
<div className = "Comment-text">{props.text}</div>
<div className = "Comment-date">{formatDate(props.date)}</div>
</div>
);
}
下面,我們將UserInfo組件分離出來,代碼如下:
function UserInfo(props){
return(
<div className = "UserInfo">
<Avatar user={props.user} />
<div className="userInfo-name">{props.user.name}</div>
</div>
);
}
然后我們再對Comment組件做進一步修改,代碼如下:
function Comment(props){
return(
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">{props.text}</div>
<div className="Comment-date">{formatDate{props.date)}</div>
</div>
);
}
分割組件一開始看起來很像Grunt,但是在更大規模的應用中會得益于擁有大量可重用組件。一個好的經驗法則(a rule of thumb)是:如果你的一部分UI會被多次使用(比如Button、Panel、 Avatar)或者它本身很復雜(比如App,FeedStory,Commment),那么這個組件就是很好的做分割的選擇。
屬性是只讀的
無論你將一個組件聲明為一個類還是一個函數,它都不能改變它的屬性,請看下面這個sum函數:
function sum(a,b){
return a+b;
}
這樣的函數我們叫做“純”函數,因為它們不會改變它們的輸入,并且對同樣的輸入總是做同樣的輸出。
相反,下面的函數就“不純了”,因為它改變了自己的輸入,代碼如下:
function withdraw(account, amount){
//每次調用函數,它的輸入都發生變化
account.total -= amount;
}
React雖然很靈活,但是它有一條非常嚴格的規則:
所有React組件在對待它們的屬性時,必須像“純”函數那樣!
當然,應用的UI是動態的、是在時刻變化的。在下一節中,我們將介紹一個新的概念--狀態。狀態允許React組件在響應用戶交互、網絡響應等時,能夠實時改變它們的輸出,但并不違反上述規則。
什么是純函數
1.給出同樣的參數值,該函數總是求出同樣的結果。該函數結果值不依賴任何隱藏信息或程序執行處理可能改變的狀態或在程序的兩個不同的執行,也不能依賴來自I/O裝置的任何外部的輸入。
2.結果的求值不會促使任何可語義上可觀察的副作用或輸出,例如易變對象的變化或輸出到I/O裝置
什么是非純函數
1.返回當前天星期幾的函數是一個非純函數,因為在不同的時間它將產生不同的結果,它引用了一些全局狀態。同樣地,任何使用全局狀態或靜態變量潛在地是非純函數。
2.random()是非純函數,因為每次調用潛在地產生不同的值。這是因為偽隨機數產生器使用和更新了一個全局的“種子”狀態。加入我們修改它去拿種子作為參數,例如random(seed),那么random變為純函數,因為使用同一種子值的多次調用返回同一隨機數。
3.printf() 是非純函數,因為它促使輸出到一個I/O裝置,產生了副作用。
6. 狀態和生命周期
思考下上一節那個節拍時鐘的例子
到目前為止我們只學習了一種更新UI的方法。我們通過調用ReactDOM.render()函數來改變渲染輸出,代碼如下:
function tick(){
const element = {
<div>
<h1>Hello,world!</div>
<h2>It is {new Date().toLocaleTimeString()}.</h1>
</div>
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick,1000);
在這一部分中,我們將學習如何真正使Clock組件是可重用的并封裝的。Clock組件將能夠設置自己的計時器并每秒更新它。
讓我們看看clock是如何封裝的,代碼如下:
function Clock(props){
return(
<div>
<h1>hello,world!</h1>
<h2>It is {props.date.toLocaleTimeString()}.</h2>
</div>
);
}
function tick(){
ReactDOM.render(
<Clock date = {new Date()} />,
document.getElementById('root')
);
}
setInterval(tick,1000);
但是,上面的代碼丟失了一個很關鍵的要求:Clock組件設置計時器和每秒更新UI都應該在組件內部實現。
理想我們想寫成下面這樣:
ReactDOM.render(){
<Clock />
document.getElementById('root');
};
為了實現這種,我們需要在Clock組件中增加狀態。
狀態和屬性很相似,但是它是私有的并且完全由組件控制。
我們之前提到過,組件定義為class會有一些額外的特征。本地狀態準確來說就是一種僅僅對class有效的特征。
將一個函數轉為一個類
要把一個像Clock那樣的函數組件轉為類組件需要五步:
- 創建一個ES6類,命名相同,繼承React.Component類。
- 添加一個單一空方法render()。
- 把函數體放入render()方法中。
- 在render()方法中,用props代替this.props。
- 刪掉保留的空函數聲明。
示例代碼如下:
class Clock extends React.Component{
render(){
return(
<div>
<h1>Hello,world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
正如上面代碼,Clock類現在定義成了一個類而不是一個函數。這讓我們能夠使用一些額外的特征,比如local state、lifecycle hooks。
給類增加一個本地狀態
我們將用3步把date從props轉為state:
- 在render()方法中,用this.state.date代替this.props.date。
class Clock extends React.Component{
render(){
return(
<div>
<h1>Hello,world!</h1>
//change
<h2>it is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
- 添加一個類構造體,并對this.state進行初始化:
class Clock extends React.Component{
//add
constructor(props){
super(props);
this.state = {date:new Date()};
}
render(){
return(
<div>
<h1>Hello,world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
注意我們是怎么把props 傳給base constructor的:
constructor(props){
super(props);
this.state = {date:new Date()};
}
類組件總是調用帶props的構造體函數。
- 從<Clock />元素中把date屬性移除:
ReactDOM.render(
//change
<Clock />
document.getElementById('root')
);
然后我們把計時器代碼添加到組件中,結果看起來像這樣:
class Clock extends React.Component{
constructor(props){
super(props);
this.state = {date:new Date()};
}
render(){
return(
<div>
<h1>Hello,world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />
document.getElementById('root')
);
下一步,我們將讓Clock組件能夠設置自己的計時器和自我更新。
給類添加一個聲明周期方法
在帶有很多組件的應用中,當組件被銷毀時,應用能夠釋放掉組件占用的資源是很重要的。
當Clock組件首次插入到DOM節點時我們要設置一個計時器,這在React叫做掛載(mounting);當DOM節點把Clock組件移除時,我們要將計時器清除 , 這在React中叫做“卸載”(unmounting)。
當一個組件掛載和卸載時,我們能夠在組件類中聲明一個特殊的方法來運行一段代碼執行它,代碼示例如下:
class Clock extends React.Component{
constructor(props){
super(props);
this.state = {date:new Date()};
}
componentDidMount(){
//Mounting code
}
componentWillUnmount(){
//Unmounting code
}
render(){
return (
<div>
<h1>Hello,world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
這個方法我們稱作生命周期鉤子。
當組件輸出被渲染到DOM節點后,componentDidMount()運行。在這個方法里設置計時器非常合適,代碼如下:
componentDidMount(){
this.timerID = setInterval(
()=>this.tick(),
1000
);
}
通過componentWillUnmount()卸載計時器,代碼如下:
componentWillUnmount(){
clearInterval(this.timerID);
}
代碼補全:
class Clock extends React.Component{
constructor(props){
super(props);
this.state = {date:new Date()};
}
componentDidMount(){
this.timerID = setInterval(
()=>this.tick(),
1000
);
}
componentWillUnmount(){
clearInterval(this.timerID);
}
tick(){
this.setState({date:new Date()});
}
render(){
return(
<div>
<h1>Hello,world!</h1>
<h2>It is {this.state.date.toLcoaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(){
<Clock />,
document.getElementById('root');
);
代碼解析:
//1. <clock />傳入ReactDOM.render(),React調用Clock組件的構造體。當Clock需要顯示當前時間時,它會對this.state進行初始化,賦值一個當前時間對象.后續將更新整個狀態。
//2. React調用Clock組件的render()方法,將Clock的render()輸出更新到DOM節點上。
//3. 當Clock的輸出插入到DOM節點上時,React調用componentDidMount()方法。在方法中,Clock組件讓瀏覽器設置一個計時器來每秒調用一次 tick()。
//4. 每一秒瀏覽器調用tick()方法。方法中,Clock組件通過調用setState()打算要更新UI,setState()帶有一個當前時間對象的參數。通過調用setState(),React知道狀態已經發生變化,并再次調用render()方法來了解這次應該在屏幕中顯示什么。這次,在render()中的this.state.date和上一次不同,因此render輸出的是一個更新后的時間,React相應的更新DOM。
//5. 如果Clock組件從DOM中移除,React會調用componentWillUnmount(),計時器隨即停止。
state和props的介紹與比較
大家看到上面的代碼時可能會感到有些懵逼,感覺props和component as function就能解決的問題為什么要用component as class和state呢?下面來介紹下它們的區別和應用場景。
一. state
- state的作用
state是React組件中的一個對象。React把用戶界面當做是狀態機,想象它有不同的狀態然后渲染這些狀態,可以輕松讓用戶界面與數據保持一致。React中,更新組件的狀態state,會導致重新渲染UI(不需要操作DOM)。簡單來說,就是用戶界面會隨著state變化而變化。- state的工作原理
常用的通知React數據變化的方法是調用setState(data,callback)。這個方法會合并data到this.state,并重新渲染組件。渲染完成后,調用可選的callback回調。但大部分情況不需要提供callback,因為React會負責把UI更新到最新的狀態。- 哪些組件應該有state?
大部分組件的工作應該是從props中取出數據并渲染出來。但是,又是需要對用戶輸入,服務器請求或者時間變化等作出響應,這是才需要 state。組件應該盡可能的無狀態化,這樣能隔離state,把它放到最合理的地方(Redux做的就是這個事情?),也能減少冗余并易于解釋程序運作的過程。常用的模式就是創建等多個只負責渲染的無狀態組(stateless)組件,在它們的上層創建一個有狀態的(stateful)組件并把它的狀態通過props傳遞給子級,有狀態的組件封裝了所有的用戶交互邏輯,而這些無狀態的組件只負責聲明使的渲染數據。- 哪些應該作為state?
state應該包含那些可能被組件的時間處理器改變并觸發用戶更新的數據。這種數據一般很小且能被JSON序列化。當創建一個狀態化的組件時,應該保持數據的精簡,然后存入this.state。在render()中再根據state來計算需要的其他數據,因為如果在state里添加冗余數據或計算所得數據,經常需要手動保持同步。- 哪些不應該作為state?
this.state應該僅包含能表示用戶界面狀態所需的最少數據,因此不應該包括:
- 計算所得數據
- React組件:在render()中使用props和state來創建它。
- 基于props的重復數據:盡量保持用props來作為唯一的數據來源。把props保存到state中的有效的場景是需要知道它以前的值的時候,因為未來props可能會發生變化。
二. props
- props的作用
組件中的props是一種父級向子級傳遞數據的方式。- 復合組件
代碼如下:var Avatar = React.createClass({ render:function(){ return( <div> <ProfilePic username = {this.props.username} /> <ProfilePic username = {this.props.username} /> </div> ); } }); var profilePic = react.createClass({ render:function(){ return( <a href = {'http://www.facebook.com/' + this.props.username}> {this.props.username} </a> ); } }); React.render( <Avatar username = "ryanho" />, document.getElementById("example") );
從屬關系:
如果組件Y在render()方法中創建了組件X,那么Y就擁有X。
正確的使用state
關于setState()你需要知道三件事。
(1)不要試圖直接修改State
如下代碼,這樣不能重新渲染組件:
//Wrong
this.state.comment = 'Hello';
用setState()來對狀態進行修改:
//Correct
this.setState({Comment:'Hello'});
唯一能部署this.state的地方就是構造題函數。
(2) State的更新可能是異步的
React在一次頁面更新中可能要批處理多個setState()。由于this.props和this.state的更新可能是異步的,因此你不應該依賴this.props的值來計算下一次狀態。
例如下列代碼,這段代碼不能夠正確更新計數器:
this.setState({
counter:this.state.counter + this.props.increment,
});
為了修復這種可能出現的錯誤,可以使用setState()的來接受一個函數而不是對象。這個函數將會接收"前狀態"作為第一個參數,此時被更新的props作為第二個參數:
//Correct
this.setState((prevState , props) =>({
counter:preState.counter +props.increment
}));
上面的代碼我們使用了arrow function,如下代碼是常規寫法:
//Correct
this.setState(function(prevState, props){
return{
counter : prevState.counter + props.increment
};
});
(3) state的更新是融合的(Merged)
當我們調用setState(),React將我們提供的對象融入到當前狀態中。(...一臉懵逼)
例如,你的狀態可能包含很多獨立的變量:
constructor(props){
super(props);
this.state = {
posts: [],
comment :[]
};
}
而后你要分別調用setState()來更新它們:
componentDidMount(){
fetchPosts().then(response=>{
this.setState({
posts:response.posts
});
});
fetchComments().then(response =>{
this.setState({
comments:response.comments
});
});
}
數據自上而下傳遞(flow down)
父組件和子組件都不知道某一個組件是有狀態的(stateful)還是無狀態的(stateless),并且它們也不關心一個組件是被定義成class還是function。這就是為什么狀態經常被稱為是本地的或者封裝的。除非是某個組件的擁有者或者創建者,都不能訪問它。
一個組件可以將它的狀態作為porps傳遞給它的子組件,代碼如下:
<h2>It is {this.state.date.toLocaleTimeString()}.<h2>
this.state對用戶定義的組件也有效,代碼如下:
<FormattedDate date={this.state.date} />
FormattedDate組件可以通過props接收到date數據。但是它不知道這個數據是通過Clock組件的狀態,還是Clock的props,還是鍵盤輸入。請看如下代碼:
function FormattedDate(props){
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
我們將這種傳遞稱為是“自上而下的”或者稱為“單向的“。狀態是被組件擁有的,因此源于這些狀態的數據和UI僅能影響到這些組件的子組件。
如果你把一顆組件樹想像為一個 props瀑布,每一個組件的狀態就像是一個可以在任意時刻匯入瀑布的支流,當然,支流也是“向下流淌的”。
為了顯示所有組件是真正隔絕獨立的,在App組件里我們渲染了三個Clock組件,代碼如下:
function App(){
return(
<div>
<Clock />
<Clock />
<Clock />
<Clock />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
每個Clock組件都設置了自己的計時器并且獨立的更新。
6. 處理事件
帶有React元素的處理事件和在DOM節點中的處理事件很相似。但它們有一些句法上的不同:
- React事件使用駱駝命名法(camelCase),而不是小寫命名。
- JSX語法中你傳遞的是一個函數作為event handler,而不是一個字符串。
例如,在HTML中:
<button onClick = "activateLasers()">
Activate Lasers
</button>
在React中有一些輕微的不同,代碼如下:
<button onClick = {activateLasers}>
Activate Lasers
</button>
另一個不同是,在React中你不能通過返回false來阻止默認行為。你必須通過調用preventDefault來這樣做。例如在純HTML中,為了阻止打開新頁面的默認鏈接行為,你可以這樣寫:
<a href="#" onclick="console.log('This link was clicked.');
return false">
Click me
</a>
在React中,要寫成下面這樣:
function ActionLink(){
function handleClick(e){
e.preventDefault();
console.log('This link was click.');
}
return(
<a href = "#" onClick={handleClick}>
Click me
</a>
);
}
這段代碼中,e是一個虛構的事件。React根據W3C spec定義這些虛構事件,因此你不必擔心交叉瀏覽器的互通性。請看SyntheticEvent參考文檔來了解更多。
在你使用React時,你一般不必調用addEventListener函數來向DOM元素添加listener。相反,你只需在元素在初始化渲染時提供一個listener。(一臉懵逼)
當你使用ES6來定義組件時,通用的方法是將事件處理定義為類的一個方法。例如,Toggle組件渲染了一個按鈕,這個按鈕讓用戶能夠在‘NO’‘OFF'間來回切換狀態:
class Toggle extend React.Component{
constructor(props){
super(props);
this.state = {isToggleOn:true};
//This binding is necessary to make 'this' work in the callback
this.handleClick = this.handleClick.bing(this);
}
handleClick(){
this.setState(prevState => ({
isToggleon : !prevState.isToggleOn
}));
}
render(){
return(
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'NO' : 'OFF'}
</button>
);
}
}
ReactDOM.render(
<Toggle />
document.getElementById('root')
);
你一定要小心在JSX中'this'的含義。在 js中,類方法默認是不綁定的。如果你忘記綁定this.handlerClick并把它傳遞到onClick,當函數實際被調用時this會顯示未定義。
這并不是React特有的行為,它是js的特性。一般來說,如果你指向的方法后面不帶小括號(),像這樣
onClick = {this.handleClick}
你應該綁定那個方法。
如果調用bind讓你很煩躁,有兩種方法可以選。如果你對public class fields語法很熟,可以用class fields來綁定回調(僅限高手使用):
class LoggingButton extends React.Component{
//This syntax ensures 'this' is bound within handleClick.
//Warning:This is *experimental* syntax.
handleClick = () =>{
console.log('this is:', this);
}
render(){
return(
<button coClick = {this.handleClick}>
Click me
</button>
);
}
}
如果你不會class fields的語法,你在回調中可以用剪頭函數:
class LoggingButton extends React.Component{
handleClick(){
console.log('this is:',this);
}
render(){
//This syntax ensures 'this' is bound withon handleClick
return(
<button onClick = {(e) =>this.handleClick(e)}>
Click me
</button>
);
}
}
這個語法問題是:每次LoggingButton渲染時都要創建不同的回調。在大多數情況下這沒問題,但是,如果回調被作為一個prop傳遞給下層組件時,那些組件可能會進行額外的重新渲染。我們一般推薦在構造體中渲染或使用class fields語法,來避免這類問題的發生。
向Event Handlers中傳遞參數
在一個循環中,想要傳遞一個額外的參數到event handler是很常見的。例如,如果id是一個行ID,下面的兩種都有效:
<button onClick={(e) =>this.deleteRow(id,e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this.id)}>Delete Row</button>
上面兩種是等效的。使用箭頭函數和Function.prototype.bing都可以。
在兩個例子中,代表React的e參數將作為ID后的第二個參數被傳遞。在箭頭函數中,我們必須顯性的直接傳遞它。
7. 條件渲染
在React中,你可以創建區域組件,并按照你的需要封裝組件行為。然后,你可以根據你的應用的狀態,只渲染它們中的一部分。
在React中的條件渲染與js中的條件語句一樣。可以是用js的‘if’或其他條件運算符來創建代表當前狀態的元素,并且讓React更新UI來匹配狀態。
請看下面兩個組件:
function UserGreeting(props){
return <h1>Welcome back! </h1>;
}
function GuestGreeting(props){
return <h1>Please sign up. </h1>;
}
我們創建了一個Greeting組件,依照用戶是否已經登陸來顯示不同的組件,代碼如下:
function Greeting(props){
const isLoggedIn = props.isLoggedIn;
if(isLoggedIn){
return<UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
//Try changing to isLoggedIn = {true};
<Greeting isLoggedIn = {false} />,
document.getElementById('root');
);
這個例子根據不同的isLoggIn屬性渲染了不同的greeting組件。
元素變量
你可以使用變量來存儲元素。這能幫助你有條件的渲染組件中的一部分而輸出的其他部分不會改變。
請看下面兩個組件,表示登陸登出按鈕:
function LoginButton(props){
return(
<button onClick = {props.onClick}>
Login
</button>
);
}
function LogoutButton(props){
return(
<button onClick = {props.onClick}>
Logout
</button>
);
}
在下面的例子中,我們會創建一個有狀態的組件叫做LoginControl。
它根據當前狀態,既可以渲染<LoginButton />,也可以渲染<LogoutButton />,代碼如下:
class LoginControl extends React.Component{
contructor(props){
super(props);
//necessary binding
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClickbind(this);
}
handleLoginClick(){
this.setState({isLoggedIn:true});
}
handleLogoutClick(){
this.setState({isLoggedIn:true});
}
render(){
const isLoggedIn = this.state.isLoggedIn;
let button = null;
if(isLoggedIn){
button = <LogoutButton onClick={this.handleLogoutClick} />;
}else{
button = <LgoinButton onClick={this.handleLoginClick} />
}
return(
<div>
<Greeting isLoggedIn={isLoggedIn}/>
{button}
</div>
);
}
}
ReactDOM.render(
<LoginControl />,
document.getElementById('root')
);
雖然聲明一個變量并用if來條件渲染是很不錯的方法,但有時候你可能想使用更段的語法。在JSX中可以用內聯條件,下面我們來解釋。
帶邏輯和操作符的內聯if
如果你想在JSX中嵌入任何語句,加個大括號就可以了。這包括js的邏輯與運算符。請看下面代碼:
function Mailbox(props){
const unreadMessages = props.unreadMessages;
return(
<div>
<h1>Hello!</h1>
{unreadMessages.lengeh>0 &&
<h2>
You have{unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React','Re:React','Re:Re:React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElement('root');
);
這在js中也有效,true&&expression永遠是expression,false&& expression永遠是false。所以,如果條件是true,&&右邊的一大堆就會顯示處理啊,如果條件是false,那React會忽略并跳過它(所以就不顯示了)。
帶條件運算符的內聯if-Else
條件渲染內聯元素的方法是用js的條件運算符condition ? true : false。
在下面的例子中,我們用它來條件渲染一個小文本模塊,代碼如下:
render(){
const isLoggedIn = this.state.isLoggedIn;
return(
<div>
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
</div>
);
}
當然也可以用于更長的表達式:
render(){
const isLoggedIn = this.state.isLoggedIn;
return(
<div>
{isLoggedIn ? (
<LogoutButton onClick = {this.handleLogoutClick} />)
:(
<LoginButton onClick = {this.handleLoginClick} />)
}
</div>
);
}
就像在js中,根據你和你的團隊考慮的可讀性選擇合適的風格。也請記住,當條件變得太復雜了,別忘了分解組件。
阻止組件渲染
在一些很少出現的情景中,你可能想要組件隱藏,即使是它正在被另一個組件渲染。這時我們返回NULL就可以了。
在下面的例子中,<WarningBanner />組件根據props的warn值進行不同的渲染,如果prop的值是false,這個組件就不渲染,代碼如下:
function WarningBanner(props){
if(!props.warn){
return null;
}
return(
<div className = "warning">
Warning!
</div>
);
}
class Page extends React.Component{
constructor(props){
super(props);
this.state = {showWarning:true}
this.handleToggleClick = this.handleToggleClick.bind(this);
}
handleToggleClick(){
this.setState(prevState => ({
showWarning: !prevState.showWarning
}));
}
render(){
return(
<div>
<WarningBanner warn = {this.state.showWaring} />
<button onClick = {this.handleToggleClick ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />
document.getElementById('root');
);
從一個組件render()方法中返回null,不會影響組件生命周期方法的析構。例如,componentWillUpdate和componentDidUpdate仍然會被調用。
8. 列表和關鍵字
首先我們來回顧下如何在js中轉換列表。
在下面代碼中,我們使用map()函數獲取一個numbers數組,類型是double。我們將map()函數返回的新數組賦給doubled變量并在控制臺顯示,代碼如下:
const numbers = [1,2,3,4,5];
const doubled = numbers.map((number)=>number*2);
console.log(doubled);
這段代碼將[2,4,6,8,10]在控制臺中顯示出來。
在React中,將數組轉換成元素列幾乎是一樣的。
渲染多個組件
允許你創建組件集合并在JSX語法中允許使用{}花括號裹住它們。
在下面代碼中,我們使用js的map()函數對numbers數組進行遍歷。每一項都返回一個<li>元素。最終,我們將元素數組的結果賦給listItems,代碼如下:
const numbers = [1,2,3,4,5];
const listItems = numbers.map((number)=><li>{number}</li>
);
我們包含了整個listItems數組,數組元素類型是<ul>元素。我們來把它插入到DOM節點中,代碼如下:
ReactDOM.render(
<ul>{listItems}</ul>
document.getElementById('root')
);
基本列表組件
通常你會渲染一個組件列表。
我們能將前一個例子重構成一個組件,這個組件接收一個numbers數組并輸出一個無序元素列表,代碼如下:
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number)=><li>{number}<li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers{numbers} />,
document.getElementById('root');
);
當你運行這段代碼時,你會收到一條警告信息,說應該給列表項提供關鍵字。“Key”是一個特殊的字符串屬性,當你創建一個元素列表是應該把它包含在里面。我們在下一節來討論為什么這個這么重要。
讓我們將一個關鍵字在numbers.map()中賦給我們的列表項,代碼如下:
function NumberList(props){
const numbers = props.numbers;
const listItems = number.map((numbers)=>
<li key = {number.toString()}
{number}
</li>
);
return(
<ul>{listItems}</ul>
);
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers = {numbers} />,
document.getElementById('root');
);
關鍵字
關鍵字幫助React識別哪些項發生了改變、增加了或移除了。關鍵字應該在數組中就賦給元素項,并且是唯一的、固定不變的,代碼如下:
const numbers = [1,2,3,4,5];
const listItems = numbers.map((number)=>
<li key={numbers.toString()}>
{numbers}
</li>
);
選擇關鍵字最好的方法是用字符串,字符串要具有唯一性。大部分使用的關鍵字就是你數據的ID,代碼如下:
const todoItems = todo.map((todo)=>
<li key = {todo.id}>
{todo.text}
</li>
);
但如果你所渲染的列表項沒有固定ID的話,你可以使用列表項的索引作為Key,代碼如下:
const todoItems = todos.map((todo,index)=>
//Only do this if items have no stables IDs
<li key={index}>
{todo.text}
</li>
);
我們并不推薦使用索引作為關鍵字,因為列表項的循序可能發生改變。這會產生不好的影響并可能對組件的狀態產生問題。但是如果你對列表項不選擇一個嚴格的關鍵字,React會默認把項索引作為關鍵字使用。
具體請參考下面的文章:
in-depth explanation about why keys are necessary
按關鍵字選組件
關鍵字僅僅對圍繞數組的上下文有意義。換句話說,關鍵字只對一個列表有意義,而對列表中的單一項沒有意義。
例如,如果你要選一個ListItem組件,你應該在<ListItem />元素中保留關鍵字,而不是<li>元素中。代碼如下:
錯誤的代碼
function ListItem(props){
const value = props.value;
return(
//Wrong!There is no need to specify the Key here:
<li key = {value.toString()}>
{value}
</li>
);
}
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number)=>
//Wrong! The Key should have been specified here:
<ListItem value = {number} />
);
return(
<ul>
{listItems}
</ul>
);
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers} />
document.getElementById('root');
);
正確的代碼:
function ListItem(props){
//Correct!There is no need to specify the key here:
return <li>{props.value}</li>
}
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number)=>
//Correct! Key should be specified inside the array.
<ListItem key = {number.toString()}
value = {number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers = {numbers} />,
document.getElementById('root')
);
記住簡單規則:在map()函數中聲明關鍵字
關鍵字必須‘局部’唯一
數組中的關鍵字在遍歷該數組時必須顯示其唯一性。而不必在全局是唯一的。在不同的數組中可以用相同的關鍵字,代碼如下:
function Blog(props){
const sidebar = (
<ul>
{props.posts.map((post)=>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post)=>
<div key = {post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return(
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id:1, title: 'hello world', content: 'Welcome to learning React!.}
{id:2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts = {posts} />,
document.getElementById('root')
);
關鍵字是作為線索為React提供服務的,但不能從組件中獲取。如果在組件中你需要相同的值,可以通過props傳遞這個值,但必須命名為不同的名字,代碼如下:
const content = posts.map((post)=>
<Post
key = {post.id}
id = {post.id}
title = {post.title} />
);
在上面的代碼中,Post組件可以讀取props.id,但不能讀取props.key。
在JSX中嵌入map()
在上面的例子中,我們聲明了一個獨立的listItems變量并在JSX中包含了它,代碼如下:
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number)=>
<ListItem key = {number.toString()}
value = {number} />
);
return(
<ul>
{listItems}
</ul>
);
}
JSX語法允許在花括號中嵌入任何表達式,因此我們可以內聯map(),代碼如下:
function NumberList(props){
const numbers = props.numbers;
return(
<ul>
{numbers.map((number)=>
<ListItem key = {number.toString()}
value = {number} />
)};
</ul>
);
}
有時候這樣寫代碼會更簡潔,但是這種代碼風格不要濫用。代碼終歸是要有可讀性的,哪種可讀性好用哪種。
9. 表單
在React中,HTML表單的作用和其他DOM不同,表單需要保留一些外部狀態。例如下面的代碼是表單接收一個單一姓名:
<form>
<label>
Name:
<input type = "text" name = "name" />
</label>
<input type = "submit" value ="Submit" />
</form>
上面代碼中,表單的作用就是向服務器提交數據,當用戶提交表單時,服務器接收到表單數據,然后響應并產生一個新的頁面。如果在React中,這仍然是有效的。在大多數情況下,用js函數來處理表單的提交和訪問用戶鍵入表單的數據是很方便的。而達到這種效果的標準方法有一個技術名稱,叫“被控組件”。
被控組件
在HTML中,表單元素比如<input>、<textarea>和<select>,都會保持自己的狀態并根據用戶的輸入來更新狀態。而在React中,易變的狀態是保存在組件的狀態屬性中,并只能通過setState()來更新。
我們可以通過把React狀態作為“唯一信源”來將二者合并。而后渲染表單的React組件當用戶數據數據時,也能控制表單發生了什么。我們把這種值被React控制的表單輸入元素稱為被控組件。
例如,我們把上一個例子寫成被控組件:
class NameForm extends React.Component {
constructor(props){
super(props);
this.state = {value:''}
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event){
this.setState({value:event.target.value});
}
handleSubmit(event){
alert('A name was submitted:' + this.state.value);
event.preventDefault();
}
render(){
return(
<form onSubmit = {this.handleSubmit}>
<label>
Name:
<input type = "text" value = {this.state.value} onChange = {this.handleChange} />
</label>
<input type = "submit" value = "Submit" />
</form>
);
}
}
當value屬性在表單元素上被設置時,value的被顯示值永遠都是this.state.value,這就是讓React狀態成為“唯一信源”。當每次敲鍵盤運行handleChange來更新React狀態時,被顯示的值就更新到用戶輸入的值。
對于一個被控組件,每次狀態變化都和handler函數有關。handler函數能夠直接改變用戶輸入或使用戶輸入有效。例如,如果我們想要強迫姓名輸入寫成大寫字母,我們可以把 handleChange寫成這樣:
handleChange(event){
this.setState({value:event.target.value.toUpperCase()});
}
文本域標簽
在HTML中,<textarea> 元素通過它的內容直接定義它的文本,代碼如下:
<textarea>
Hello there, this is some text in a text area
</textarea>
在React中,<textarea>用一個value屬性來代替。這樣,一個使用<textarea>的表單的寫起來和使用單行輸入的表單很相似,代碼如下:
class EssayForm extends React.Component{
constructor(props){
super(props);
this.state = {
value:'Please write an essay about your favorite DOM element.'
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event){
this.setState({value:event.target.value});
}
handleSubmit(event){
alert('An essay was submitted:' + this.state.value);
event.preventDefault();
}
render(){
return(
<form onSubmit = {this.handleSubmit}>
<label>
Essay:
<textarea value = {this.state.value} onChange = {this.handleChange} />
</label>
<input type="submit" value = "Submit" />
</form>
);
}
}
注意,this.state.values是在構造體中被初始化的,所以一開始text域中會有顯示內容。
<select>標簽
在HTML中,<select>創建一個下拉列表。例如,下面的代碼是創建一個水果的下拉列表:
<select>
<option value = "grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option selected value = "coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
注意,Coconut選項被初始化為selected(被選擇)。在React中,不再使用selected屬性,而是在根<select>標簽中使用value屬性。這在被控組件中更方便,因為你只在一個地方更新就可以了,例如:
class FlavorForm extends React.Component{
constructor(props){
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event){
this.setState({value:event.target.value{);
}
handleSubmit(event){
alert('Your favorite flavor is :' + this.state.value});
event.preventDefault();
}
render(){
return(
<form onSubmit ={this.handleSubmit}>
<label>
Pick your favorite la Croix flavor:
//look at this , it is awesome!
<select value = {this.state.value} onChange = {this.handleChange}>
<option value = "grapefruit">Grapefruit</option>
<option value="lime"> Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
<input type = "submit" value="Submit" />
</form>
);
}
}
總之,<input type = "text"> <textarea><select>都很相似,它們都接收一個value屬性,用來實現一個被控組件。
注意
你可以傳遞一個數組到value屬性中,允許你在<select>標簽中選擇多個選項:<select nultiple={true} value={['B','C']}>
文件輸入標簽
在HTML中,<input type="file">讓用戶從設備存儲中選擇一個或更多文件上傳到服務器中,或者通過js來進行文件操作。
<input type = "file" />
在React中,<input type="file" />和普通的<input/>很相似,但有一點很重要的不同:它是只讀的(你不能以編程的方式設置其值)。而是要用文件相關API來與這些文件交互。
下面這個例子顯示了一個ref怎樣被用來訪問文件的,代碼如下:
class FileInput extends React.Component{
constructor(props){
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
);
}
handleSubmit(event){
event.preventDefault();
alert(
'Selected file - ${
this.fileInput.files[0].name
}'
);
}
render(){
return(
<form
onSubmit={this.handleSubmit}>
<label>
Upload file:
<input
type="file"
ref={input => {
this.fileInput = input;
}}
</label>
<br />
<button type = "submit">
Submit
</button>
</form>
);
}
}
ReactDOM.render(
<FileInput />,
document.getElementById('root')
);
多輸入處理
當需要處理多個被控 input元素時,可以為每一個元素增加name屬性并讓handler函數選擇基于不同的event.target.name做不同的事。
例如:
class Reservation extends React.Component{
constructor(props){
super(props);
this.state = {
isGoing:true,
numberOfGuests:2
}
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event){
const target = event.target;
const value = target.type ==='checkbox' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]:value
});
}
render(){
return(
<form>
<label>
Is going:
<input
name = "isGoing"
type = "checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Number of guests:
<input
name = "numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
注意下面是ES6語法:
this.setState({
[name]:value
});
下面是ES5語法,和上面是等效的:
var partialState = {};
partialState[name] = value;
this.setState(partialState);
前面的章節提到的,因為setState()自動將部分狀態與當前狀態比對,因此我們只需要調用改變的部分。
被控輸入為NULL值
為被控組件指定props值可以防止用戶改變輸入。如果你希望指定一個value但輸入仍然是可編輯的,那么你可以將value設置為NULL或未定義。
下面的代碼說明了這一點。(輸入一開始是鎖定的,但短暫延遲后變得可編輯。)
ReactDOM.render(<input value = "hi" />, mountNode);
setTimeOut(function(){
ReactDOM.render(<input value={null} />,mountNode);
},1000);
被控組件的取舍
有時候被控組件是很乏味的,因為你必須為每一種可能導致你數據變化的方法寫event handler,并且通過React組件輸送所有的輸入狀態。當你要把一個之前的代碼庫轉為React,或者將React應用集成到一個非React庫中時,這就變得非常讓人懊惱。在這種情況下,你可以使用非控組件,一種可選的實現輸入表單的技術。