
壹 ❀ 引
在從零開始的react入門教程(一)一文中,我們搭建了第一個屬於自己的react應用,並簡單學習了jsx語法。jsx寫法上與dom標簽高度一致,當然我們也知道,本質上這些react元素都是React.createElement()
的語法糖,通過編譯,bable會將其還原成最原始的樣子,比如如下代碼效果相同:
<div class="echo"></div>
// 等同於
React.createElement(
'div',
{className: 'echo'}
)
至少從書寫上,jsx為我們提供了極大便利。在文章結尾,我們也敲到了react元素並非組件,它可能是一個單一的標簽,也可能是一個代碼塊,在react中有專門的方式來創建組件,那么本文就從組件說起。
貳 ❀ 組件
貳 ❀ 壹 函數組件
react中的組件分為函數組件與class組件,兩者有一定區別,但都非常好理解。函數組件很簡單,比如我們現在想復用一段簡單的dom結構,但它的文本內容可能會不同,這種情況我們想到的就是文本內容是一個變量,這樣就能做到dom復用的目的了,所以函數組件就是做了這樣一件事:
// 這是一個函數組件,它接受一些props,並返回組合后的dom結構
function Echo(props) {
return <div>聽風是風又叫{props.name}</div>;
};
ReactDOM.render(<Echo name="聽風是風" />, document.getElementById("root"));
需要注意的是,函數組件的函數名是大寫的(class組件也是如此),我們在render中使用了組件Echo,並傳遞了一個name屬性,所有在組件上傳遞的屬性都會被包裹在props對象中,所以通過props參數我們能訪問到每一個傳遞給組件的屬性。通過打印props可以看到它是一個對象:
function Echo(props) {
console.log(props);
return <div>聽風是風又叫{props.name}</div>;
};

傳遞的數據格式除了字符,數字,它當然也支持對象傳遞,比如下面這個例子運行結果與上方相同:
const myName = {
name:'echo'
};
function Echo(props) {
console.log(props);
return <div>聽風是風又叫{props.name.name}</div>;
};
//
ReactDOM.render(<Echo name= {myName}/>, document.getElementById("root"));
我們來解讀下props.name.name
,首先我們是將myName
作為name
的值傳遞給了組件,所以要訪問到myName
得通過props.name
拿到,之后才是name
取到了具體的值。其次需要注意的是傳遞對象需要使用{}
包裹,如果不加花括號會有錯誤提示,jsx這里只支持加引號的文本或者表達式,而{myName}
就是一個簡單的表達式。
我們在上文中,也有將react元素賦予給一個變量的寫法,比如:
const ele = <div>我的名字是聽風。</div>
ReactDOM.render(ele, document.getElementById("root"));
其實組件也能像這樣賦予給一個變量,所以看到下面這樣的寫法也不要奇怪:
function Echo(props) {
return <div>我的名字是{props.name}</div>;
};
// 這里將組件賦予給了一個變量,所以render時直接用變量名
const ele = <Echo name="聽風是風"/>
ReactDOM.render(ele, document.getElementById("root"));
同理,react元素可以組合成代碼塊,組件同樣可以組合成一個新組件,比如:
function Echo(props) {
return <div>我的名字是{props.name}</div>;
};
function UserList() {
return (
// 注意,只能有一個父元素,所以得用一個標簽包裹
<div>
<Echo name="echo" />
<Echo name="聽風" />
</div>
);
};
ReactDOM.render(<UserList />, document.getElementById("root"));

在這個例子中,組件Echo
作為基礎組件,重新構建出了一個新組建UserList
,所以到這里我們可以發現,組件算自定義的react元素,它可能是由react元素組成,也可能是由組件構成。
貳 ❀ 貳 class組件
除了上面的函數組件外,我們還可以通過class創建組件,沒錯,就是ES6的class,看一個簡單的例子:
class Echo extends React.Component {
render() {
return <div>我的名字是{this.props.name}</div>;
}
};
ReactDOM.render(<Echo name="聽風" />, document.getElementById("root"));
由於ReactDOM.render
這一塊代碼相同,我們把目光放在class創建上,事實上這里使用了extends繼承了Component類,得到了一個新組件Echo
。由於此時的Echo
並不是函數,也不能接受函數形參,但事實上我們可以通過this.props
直接訪問到當前組件所傳遞的屬性,讓人心安的是,與函數組件相比,我們同樣有方法獲取外部傳遞的props。
extends中的render
方法是固定寫法,它里面包含的是此組件需要渲染的dom結構,如果你了解過ES6的class類,除了render固有方法外,其實我們可以在這個類中自定義任何我們想要的屬性以及方法,比如:
const o = {
a: 1,
b: 2,
};
class Echo extends React.Component {
// 這是一個自定義方法
add(a, b) {
return a + b;
}
// 這是固定方法
render() {
return <div>{this.add(this.props.nums.a, this.props.nums.b)}</div>;
}
}
ReactDOM.render(<Echo nums={o} />, document.getElementById("root"));
看着似乎有點復雜,我們來解釋做了什么,首先我們在外部定義了一個包含2個數字的對象o,並將其作為nums
屬性傳遞給了組件Echo
,在組件內除了render
方法外,我們還自定義了一個方法add
,最終渲染的文本由此方法提供,所以我們在返回的標簽中調用了此方法。前面說了可以通過this.props
訪問到外部傳遞的屬性,所以這里順利拿到了函數的兩個實參並參與了計算。
那么到這里我們知道,除了一些組件固有方法屬性外,我們也可以定義自己的方法用於處理渲染外的其它業務邏輯。
舉個很常見的情景,在實際開發中,有時候我們處理的組件結構會相對龐大和復雜,這時候我們就能通過功能拆分,將一個大組件在內部拆分成單個小的功能塊,比如下面這段代碼:
class Echo extends React.Component {
handleRenderTop() {
return "我是頭部";
}
// 自定義的render方法
renderTop() {
return <div>{this.handleRenderTop()}</div>;
}
handleRenderMiddle() {
// dosomething
}
renderMiddle() {
return <div>我是中間部分</div>;
}
handleRenderBottom() {
// dosomething
}
renderBottom() {
return <div>我是底部</div>;
}
// 官方提供的固定render方法
render() {
return (
<div>
{this.renderTop()}
{this.renderMiddle()}
{this.renderBottom()}
</div>
);
}
}
ReactDOM.render(<Echo />, document.getElementById("root"));
在上述代碼中,假設這個組件結構和邏輯比較復雜,通過拆分我們將其分為了上中下三個部分,並創建了對應的處理方法,最終在render
中我們將其組成在一起,這樣寫的好處是可以讓組件的結構更清晰,也利於后期對於代碼的維護。當然這里也只是提供了一種思路和可能性,具體做法還需要自行探索。
其實在class組件中除了固有render
方法外,還有ES6的constructor,以及組件生命周期函數,這些都是固定寫法,不過我們現在不急,后面會展開說明。
肆 ❀ props與State
肆 ❀ 壹 基本概念與區別
與vue雙向數據綁定不同,react提供的是單向數據流,我們可以將react的數據流動理解成一條瀑布,水流(數據)從上往下流動,傳遞到了瀑布中的每個角落(組件),而這里的水流其實就是由props和State構成,數據能讓看似靜態的組件換發新生,所以現在我們來介紹下組件中的數據props與State,並了解它們的關系以及區別。
先說props,其實通過前面的例子我們已經得到,props就是外部傳遞給組件的屬性,在函數組件中,可以直接通過形參props
訪問,而在class組件中,我們一樣能通過this.props
訪問到外部傳遞的屬性。
那么什么是State呢,說直白點,State就是組件內部定義的私有屬性,這就是兩者最直觀的區別。
State在react中更官方的解釋是狀態機,狀態的變化會引起視圖的變化,所以我們只需要修改狀態,react會自動幫我們更新視圖。比如下面這個例子:
class Echo extends React.Component {
constructor(props) {
// 參照ES6,extends時,constructor內使用this必須調用super,否則報錯
super(props);
this.state = { name: "echo" };
}
render() {
return (
<div>
我的名字是{this.state.name},年齡{this.props.age}
</div>
);
}
}
ReactDOM.render(<Echo age="27" />, document.getElementById("root"));

在上述例子中,外部傳遞的age
就是props,所以在內部也是通過this.props
訪問,而內部定義的屬性則是通過this.state
聲明,一個在里一個在外,它們共同構成了組件Echo
的數據。
上述例子中constructor
內部調用了super
方法,這一步是必要的,如果你想在繼承類的構造方法constructor
中使用this,你就一定得調用一次,這也是ES6的規定,簡單復習下ES6的繼承:
class Parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Child extends Parent {
constructor(x, y, z) {
// 本質上就是調用了超類
super(x, y);
this.z = z; // 正確
}
say() {
console.log(this.x, this.y, this.z);
}
}
const o = new Child(1, 2, 3);
console.log(o);
o.say(); //1,2,3
首先我們定義了一個父類Parent,在它的構造方法中定義了x,y兩個屬性,之后我們通過extends讓Child類繼承了Parent,並在Child的構造方法中執行了super
,這里本質上其實就是調用了父類Parent的構造函數方法,只是在執行super
時this指向了Child實例,這也讓Child實例o順利繼承了Parent上定義的屬性。我們可以輸出實例o,可以看到它確實繼承了來着Parent的屬性。

super本意其實就是超類,所有被繼承的類都可以叫超類,也就是我們長理解的父類,它並不是一個很復雜的概念,這里就簡單復習下。
肆 ❀ 貳 不要修改props以及如何修改props
在上文中,我們介紹了props與State的基本作用與區別,一個組件可以在內部定義自己需要的屬性,也可以接受外部傳遞的屬性。事實上,比如父子組件結構,父組件定義的State也能作為props傳遞給子組件使用,只是對於不同組件它的含義不同,這也對應了上文瀑布的比喻,水流由props與State構成就是這個意思了。
我們說State是私有屬性,雖然它可以傳遞給其它組件作為props使用,但站在私有的角度,我雖然大方的給你用,那你就應該只使用而不去修改它,這就是props第一准則,props應該像純函數那樣,只使用傳遞的屬性,而不去修改它(想改也改不掉,改了就報錯)。
為什么這么說,你想想,react本身就是單向數據流,父傳遞數據給子使用,如果在子組件內隨意修改父傳遞的對象反過來影響了父,那這不就亂套了嗎。
那么問題來了,如果父傳了屬性給子,子真的要改怎么辦?也不是沒辦法,第一我們可以在父提供props同時,也提供一個修改props的方法過去給子調用,子雖然是調用點,但本質執行的是父的方法,這是可行的。第二點,將傳遞進來的props復制一份,自己想怎么玩就怎么玩,也不是不可以,比如:
function Child(props) {
// 拷貝一份自己玩
let num = props.num;
num++;
return <div>{num}</div>;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { num: 1 };
}
render() {
return (
<div>
<Child num={this.state.num} />
</div>
);
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));
當然如果子組件也是class組件也可以,還是這個例子,只是修改了Child部分:
class Child extends React.Component {
constructor(props) {
super(props);
// 將傳遞進來的props賦予子組件的state
this.state = {
num:props.num
}
}
render() {
return <div>{++this.state.num}</div>;
}
}
再或者直接賦值成this上的一條屬性:
class Child extends React.Component {
constructor(props) {
super(props);
}
// 將傳遞進來的props賦予給this
num = this.props.num;
render() {
return <div>{++this.num}</div>;
}
}
以上便是復制一份的常規操作,我們再來看看父提供修改方法的做法:
class Child extends React.Component {
render() {
return (
<div>
<div>{this.props.num}</div>
<button type="button" onClick={() => this.props.onClick()}>
點我
</button>
</div>
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { num: 1 };
}
// 傳遞給子組件使用的方法
handleClick() {
// 拷貝了state中num
let num_ = this.state.num;
// 自增
num_ += 1;
// 更新state中的num
this.setState({ num: num_ });
}
render() {
return (
<div>
<Child num={this.state.num} onClick={() => this.handleClick()} />
</div>
);
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));

我們來解釋下這段代碼,我們在Parent中定義了state,其中包含num變量,以及定義了handleClick
方法,在
<Child num={this.state.num} onClick={() => this.handleClick()} />
這行代碼中,我們將state中的num
與handleClick
分別以num
與onClick
這兩個變量名傳遞進去了。
對於事件定義react有個規則,比如我們傳遞給子組件的變量名是on[Click],那么具體方法定義名一般以handle[click]來命名,簡單點說,on[event]與handle[event]配對使用,event就是代表你事件具體含義的名字,有一個統一的規則,這樣也利於同事之間的代碼協作。
在子組件內部,我們通過props能訪問到傳遞的num與onClick這兩個屬性,我們將其關聯到dom中,當點擊按鈕就會執行父組件中的handleClick
方法。有同學可能注意到handleClick
中更新num的操作了,按照我們常規理解,直接this.state.num++
不就行了嗎,很遺憾,這是react需要注意的第二點,我們無法直接修改state,比如如下行為都不被允許:
// 直接修改不允許
this.state.num = 2;
// 同理,這也是直接修改了state,也不被允許
this.setState({ num: this.state.num++ });
官方推薦做法,同樣也是將state中你要修改的部分拷貝出來,操作完成,再利用setState
更新。
如果你了解過vue,在深入響應式原理一文中,也有類似的要求,比如請使用Vue.set(object, propertyName, value)
去更新某個對象中的某條屬性,而不是直接修改它,否則你的修改可能並不會觸發視圖更新,其實都是差不多的道理,這里就順帶一提了。
OK,到這里我們對於這一塊知識點做個小結,props與state構成了react單向數據流的數據部分,同為屬性,只是一個私有一個是從外部傳遞的而已。其次,props只讀不可修改,若要修改請使用類似拷貝的折中方法,state除了拷貝外還得通過setState
重新賦值。前面也說了,props就是外部傳遞的state,所以兩者都不能直接修改也不是不無道理,記住這點就好了。
伍 ❀ 總
現在是凌晨1點半(封面圖也是應景了),其實寫到這里,第二部分知識我想說的也都差不多了,看了眼篇幅,四千多字,再長一些知識點可能也有點多了,所以這篇就先介紹到這里。怎么說呢,關於文章的編寫我心里其實還是會有遺憾的,我畢竟只是個初學者,實戰項目經驗還遠遠不足,很多東西還不能從根源去解釋清楚,比如setState
可能是異步行為,所以不要用state變化作為你部分邏輯的執行判斷條件,舉個例子:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
bol: true,
};
}
// 這是生命周期函數
componentDidMount(){
for (let i = 0; i < 5; i++) {
if (this.state.bol) {
console.log(i);
this.setState({ bol: false });
}
}
console.log(this.state.bol);//true
}
// 這也是生命周期函數
componentDidUpdate(){
console.log(this.state.bol);//false
}
render() {
return null;
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));
我們預期的是在輸出i為0之后,就修改bol狀態,之后循環無法再次進入這段代碼,但很遺憾,for循環會完整執行完畢並輸出0-1-2-3-4,直到在生命周期componentDidUpdate
中我們才捕獲到修改成功的狀態。遺憾的是我目前的經驗還不足以將這塊知識吃透,沒吃透的東西我不會寫,所以這里算留個坑吧,之后一定會單獨寫一篇文章介紹state的問題,把這塊弄情況,那么這篇文章就先說到這里了,本文結束。