react 入門與進階教程
前端學習對於我們來說越來越不友好,特別是隨着這幾年的發展,入門門檻越來越高,連進階道路都變成了一場馬拉松。在學習過程中,我們面臨很多選擇,vue與react便是一個兩難的選擇。
兩者都是非常優秀的框架,而且我不能非常主觀的說誰好誰不好。但是從我們初學者的角度來說,其實我們沒有必要去考慮誰的性能更好,誰的實現更優雅,誰更適合什么場景等各種因素,唯一一個需要考慮的標准就是,學習誰,能夠讓我們更快的掌握它。因為無論他們兩個你掌握了誰,都能夠讓你在找工作時更有底氣。這就足夠了。
因此,我這篇文章的目的,則是希望從與官方文檔不同的角度,來試圖讓react學習變得更加容易,如果你想要學習react,不妨花點時間讀下去。
為什么對於新人來說,官方文檔不能幫助你掌握得更好
對於vue的學習,很多朋友有一個大的誤解,認為vue官方出了中文文檔,所以掌握起來會更加容易。然而事實上並非如此。
官方文檔可能告訴了你vue/react的基礎知識有哪些,可是這些知識怎么用,官方文檔並沒有告訴我們。而且vue官方文檔為了降低學習門檻(繞開了vue-cli),在講述知識的時候,不少地方其實與實際開發是有差距的,這個差距會導致你看完了官方文檔,仍然不知道如何使用vue做一些事情。
當然,這樣的問題,react官方文檔也存在。雖然對於經驗豐富的大神來說,這並不是問題,但是對於新人來說,這樣的差距往往會使得大家有一種似懂非懂的感覺。
這也是我為什么要從和官方文檔不一樣的角度來入手的原因。
學前准備
在准備學習本文的react知識之前,希望你已經擁有了ES6的知識與知道了create-react-app
的安裝與使用,我們的學習將會建立在這基礎之上,如果你暫時還沒有接觸過他們,不用擔心,可以回過頭去閱讀我的前兩篇文章。不用花太多時間就可以初步掌握。
ES6常用知識合集
詳解create-react-app 與 ES6 modules
你可以暫時不用對react有什么基礎的了解,我們可以從0開始,當然,如果你看過官方文檔或者從其他地方學習過相關知識就更好了。
開始啦,萬能的Hello World程序
首先,假設你已經在電腦上安裝好了create-react-app
並知道如何使用,那么我就開始在你電腦上存放開發項目的目錄(本文中假設為develop)里開始創建一個名為first-react
的react項目。操作順序如下:
1
2
3
4
5
6
7
8
9
10
11
|
// 在develop目錄創建first-react項目
> create-react-app first-react
// 進入新創建的項目
> cd first-react
// 安裝項目依賴包
> npm install
// 安裝完畢之后啟動項目
> npm start
|
啟動之后,效果分別如下圖所示:
自動生成的項目是一個簡單的react demo。這個時候項目中會有三個文件夾,我們來分別了解一下這三個文件夾的作用。
- node_modules
項目依賴包存放位置。當我們運行npm install安裝package.json中的依賴包時,該文件夾會自動創建,所有的依賴包會安裝到該文件夾里。 - public
主要的作用是html入口文件的存放。當然我們也可以存放其他公用的靜態資源,如圖片,css等。其中的index.html就是我們項目的入口html文件。 - src
組件的存放目錄。在create-react-app創建的項目中,每一個單獨的文件都可以被看成一個單獨的模塊,單獨的image,單獨的css,單獨js等,而所有的組件都存放於src目錄中,其中index.js則是js的入口文件。雖然我們並沒有在index.html中使用script標簽引入他,但是他的作用就和此一樣。
我們在最初學習開發一個頁面的時候,就已經知道一個頁面會有一個html文件,比如index.html,然后分別在html文件中,通過script與link標簽引入js與css。但是在構建工具中,我們只需要按照一定的規則來組織文件即可,整合的工作構建工具會自動幫助我們完成,這也是構建工具給前端開發帶來的便利之處,也因為如此,前端的模塊化開發才成為了可能。
我們還是和上一篇文章中說的一樣,先清空src目錄里所有的其他文件,僅僅只留下空的入口文件index.js
,並在index.js
寫入如下的代碼:
1
2
3
4
5
6
7
|
// src/index.js
import React from 'react';
import { render } from 'react-dom';
const root = document.querySelector('#root');
render(<div>Hello World!</div>, root);
|
保存之后,結果如下:
如何你能輕松看懂這四行代碼,那么說明你離掌握react已經不遠了。至少你已經掌握了ES6的相關知識。我來解釋一下這些代碼的作用。
import React from 'react';
在我們通過npm install
指令安裝依賴包的時候,就已經安裝好了react,因此我們可以直接import。這句話的作用就在於,能夠讓構建工具在當前模塊中識別jsx。而jsx,是一種類似於html標簽的模板語言,我們只需要懂得html標簽,就不必花費額外的精力去了解jsx,因為我們可以直接理解為它就是html標簽,但是在此基礎上,擴展了更多的能力。例如這里,程序能夠識別Hello World!,正是這句話的作用。
import { render } from 'react-dom';
這是利用ES6的解析結構的語法,僅僅引入了react-dom
的render
方法。render方法的作用,就是將react組件,渲染進DOM結構中,它的第一個參數就是react 組件,第二個參數則是一個DOM元素對象。const root = document.querySelector('#root');
這句話就很簡答了,如果你理解不了,那么說明你的基礎還不足以支撐你學習react, – -。render(<div>Hello World!</div>, root);
這是最核心的一步,通過render
方法,將寫好的react組件渲染進DOM元素對象。而這里的root
,則是在index.html
中寫好的一個元素。這里的div
,可以理解為一個最簡單的react組件。
OK,理解了這些,那么我們就可以開始學習react最核心的內容組件
了。
react組件
曾經,創建react組件有三種方式,但是既然都決定在ES6的基礎上來學習react了,那么我也就只介紹其中的兩種方式了。反正另外一種方式也已經被官方廢棄。
當一個組件,並沒有額外的邏輯處理,僅僅只是用於數據的展示時,我們推薦使用函數式的方式來創建一個無狀態組件。
我們結合簡單的例子來理解。在項目的src目錄里創建一個叫做helloWorld.jsx
的文件。在該文件中,我們將創建一個正式的react組件,代碼如下:
1
2
3
4
5
6
7
8
9
10
|
// src/helloWorld.jsx
import React from 'react';
const HelloWorld = () => {
return (
<div>Hello World!</div>
)
}
export default HelloWorld;
|
並在index.js
中引入該組件。修改index.js
代碼如下:
1
2
3
4
5
6
7
8
9
10
|
// src/index.js
import React from 'react';
import { render } from 'react-dom';
// 引入HelloWorld組件
import HelloWorld from './helloWorld';
const root = document.querySelector('#root');
render(<HelloWorld />, root);
|
保存后運行,我們發現結果一樣。
在helloWorld.jsx
中,我們仍然引入了react
,是因為所有會涉及到jsx模板的組件,我們都要引入它,這樣構建工具才會識別得到。
組件里只是簡單的創建了一個HelloWorld函數,並返回了一段html(jsx模板)。並在最后將HelloWorld函數作為對外的接口暴露出來export default HelloWorld
。
接下來我們通過一點一點擴展HelloWorld組件能力的方式,來學習組件相關的基礎知識。
向組件內部傳遞參數
向組件內部傳遞參數的方式很簡單,這就和在html標簽上添加一個屬性一樣。
例如我們希望向HelloWorld組件內傳遞一個name屬性。那么只需要我們在使用該組件的時候,添加一個屬性即可。
1
2
|
<HelloWorld name="Tom" />
<HelloWorld name="Jake" />
|
如果我們希望組件最終渲染的結果是諸如:Tom say: Hello, world!
其中的名字可以在傳入時自定義。那么我們在組件中應該如何接收傳遞進來的參數呢?
我們修改HelloWorld.jsx
如下:
1
2
3
4
5
6
7
8
9
10
11
|
// src/helloWorld.jsx
import React from 'react';
const HelloWorld = (props) => {
console.log(props);
return (
<div>{ props.name } say: Hello World!</div>
)
}
export default HelloWorld;
|
並在index.js中修改render方法的使用,向組件中傳入一個name屬性
1
2
|
// src/index.js
render(, root);
|
結果如下:
在HelloWorld
組件中,我使用了一個叫做props的參數。而通過打印出來props可以得知,props正是一個組件在使用時,所有傳遞進來屬性組合而成的一個對象。大家也可以在學習時多傳入幾個額外的參數,他們都會出現在props對象里。
而在jsx模板中,通過
這樣的方式來將變量傳入進來。這是jsx模板語言支持的一種語法,大家記住能用即可。
大家要記住,使用這種方式創建的無狀態組件,會比另外一種方式性能更高,因此如果你的組件僅僅只是用於簡單的數據展示,沒有額外的邏輯處理,就要優先選擇這種方式。
那么我們繼續升級HelloWorld組件的能力。現在我希望有一個點擊事件,當我們點擊該組件時,會在Console工具中打印出傳入的name值。這就涉及到了另外一種組件的創建,也就是當我們的組件開始有邏輯處理,之前的那種方式勝任不了時索要采取的一種形式。
修改helloWorld.jsx
文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.props);
console.log(this.props.name);
}
render () {
return (
<div onClick={this.clickHander}>{ this.props.name } say: Hello World!</div>
)
}
}
export default HelloWorld;
|
如果,你同時熟知第一種react組件的創建方式,與ES6語法的話,相信上面的代碼,並不會對你造成多少困擾。
沒錯,這種方式創建的組件,正是通過繼承react的Component
對象而來。所以創建的方式也是利用ES6的class語法來生成。也正因為如此,其中的很多實用方式,也就跟class的使用一樣了。
上面的render方法,則是Component中,專門提供的用來處理jsx模板的方法。
與第一種方式不同的是,我們接收傳入進來的參數,使用的是this.props
,第一種方式將props放置於函數參數中,而這種方式則是將props掛載與實例對象上,因此會有所不同。
而我們想要給一個組件添加點擊事件,方式也與html標簽中幾乎一致
react事件相關的知識大家可以當做一個進階課程去研究,這里就暫時不多說,詳情可以參考官方文檔 https://facebook.github.io/react/docs/events.html
好了,現在大家初步認識了react的第二種組件的創建方式,那么我們繼續搞事情,現在我想要的效果,是傳入兩個名字,name1=Tom, name2='Jason'
,我希望第一次點擊時,log出Tom,第二次log出Jason,第三次Tom…
這個時候,我們就需要引入react組件非常核心的知識狀態state
。
修改helloWorld.jsx
代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
state = {
switch: 0,
name: this.props.name1
}
clickHander = () => {
const { name1, name2 } = this.props;
if (this.state.switch === 0) {
console.log(name1);
this.setState({
switch: 1,
name: name2
})
} else {
console.log(name2);
this.setState({
switch: 0,
name: name1
})
}
}
render () {
return (
<div onClick={this.clickHander}>{ this.state.name } say: Hello World!</div>
)
}
}
export default HelloWorld;
|
先來說說state相關的基礎知識。首先了解ES6 class語法的同學都應該知道,當我們通過這種方式來寫的時候,其實是將state寫入了構造函數之中。
1
2
3
4
|
state = {}
// 等同於ES5構造函數中的
this.state = {}
|
因此深入掌握class語法對於學習react組件的幫助非常巨大,我們需要清楚的知道什么樣的寫法會放入對象的什么位置,是構造函數中,還是原型中等。這也是為什么開篇我會強調一定要先對我的前兩篇文章所介紹的知識有一定了解才行。
因此,在對象中,我們可以通過this.state
的方式來訪問state中所存儲的屬性。同時,react還提供了如下的方式來修改state的值
1
2
3
|
this.setState({
name: 'newName'
})
|
setState
接收一個對象,它的運行結果類似於執行一次assign方法。會修改傳入的屬性,而其他的屬性則保持不變。
react賦予state的特性,則是當state被修改時,會引起組件的一次重新渲染。即render方法會重新執行一次。也正是由於這個特性,因此當我們想要改變界面上的元素內容時,常常只需要改變state中的值就行了。這也是為什么結合render方法,我們可以不再需要jquery的原因所在。
而setState
也有一個非常重要的特性,那就是,該方法是異步的。它並不會立即執行,而會在下一輪事件循環中執行。
說到這里,基礎薄弱的同學就開始頭暈了,這就是為什么我在前面的文章都反復強調基礎知識的重要性,基礎扎實,很多東西稍微一提,你就知道是怎么回事,不扎實,到處都是讓你頭暈的點,不知道的沒關系,讀我這篇文章 http://www.jianshu.com/p/12b9f73c5a4f。
相信不理解這個點的同學肯定會遇到很多坑,所以千萬要記住了。
1
2
3
4
5
6
7
|
// 假設state.name的初始值為Tom,我們改變它的值
this.setState({
name: 'Jason'
})
// 然后立即查看它的值
console.log(this.state.name) // 仍然為Tom,不會立即改變
|
refs
我們知道,react組件其實是虛擬DOM,因此通常我們需要通過特殊的方式才能拿到真正的DOM元素。大概說一說虛擬DOM是個什么形式存在的,它其實就是通過js對象的方式將DOM元素相關的都存儲其實,比如一個div元素可能會是這樣:
1
2
3
4
5
6
7
8
9
|
// 當然可能命名會是其他的,大概表達一個意思,不深究哈
{
nodeName: 'div',
className: 'hello-world',
style: {},
parentNodes: 'root',
childrenNodes: []
...
}
|
而我們想要拿到真實的DOM元素,react中提供了一種叫做ref
的屬性來實現這個目的。
修改helloWorld.jsx
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.refs)
}
render () {
return (
<div className="container" onClick={this.clickHander}>
<div ref="hello" className="hello">Hello</div>
<div ref="world" className="world">World</div>
</div>
)
}
}
export default HelloWorld;
|
為了區分ES6語法中的class關鍵字,當我們在jsx中給元素添加class時,需要使用
className
來代替
我們在jsx中,可以給元素添加ref
屬性,而這些擁有ref屬性的元素,會統一放在組件對象的refs
中,因此,當我們想要訪問對應的真實DOM時,則通過this.refs
來訪問即可。
當然,ref的值不僅僅可以為一個名字,同時還可以為一個回調函數,這個函數會在render渲染時執行,也就是說,每當render函數執行一次,ref的回調函數也會執行一次。
修改helloWorld.jsx
如下,感受一下ref回調的知識點
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// src/helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.refs)
}
refCallback = (elem) => {
console.log(elem);
}
render () {
return (
<div className="container" onClick={this.clickHander}>
<div ref="hello" className="hello">Hello</div>
<div ref={this.refCallback} className="world">World</div>
</div>
)
}
}
export default HelloWorld;
|
大概介紹一下我暫時能想到的ref使用的一個場景。例如我們要實現元素拖拽的時候,或者寫一個slider組件。我們可能會非常頻繁的改動元素的位置。這個時候,如果我們仍然通過react組件的state來存儲元素的位置,那么就會導致react組件過於頻繁的渲染,這就會引發一個嚴重的性能問題。所以這個時候我們不得不獲取到真實DOM,並通過常規的方式來做。
同樣的道理也適用於vue中,我們要盡量避免將可能會變動頻率非常高的屬性存放於vue組件的data中。
組件生命周期
所謂組件的生命周期,指的就是一個組件,從創建到銷毀的這樣一個過程。
而react為組件的生命周期提供了很多的鈎子函數。很多地方也為生命周期畫了很清晰明了的圖幫助大家理解。但是我在初學的時候其實並沒有看懂,還是在我懂得了生命周期之后,才看懂的那些圖。所以呢,這里我也就不去找圖了。我們這樣理解。
通俗來說,react為一個組件,划分了如下的時刻。
- 組件第一次渲染完成的前后時刻,
componentWillMount
渲染完成之前componentDidMount
渲染完成之后
所謂的渲染完成,即組件已經被渲染成為真實DOM並插入到了html之中。
- 組件屬性(我們前面提到的props與state)更新的前后時刻
componentWillReceiveProps
接收到一個新的props時,在重新render之前調用shouldComponentUpdate
接收到一個新的state或者props時,在重新render之前調用componentWillUpdate
接收到一個新的state或者props時,在重新render之前調用componentDidUpdate
組件完成更新之后調用 - 組件取消掛載之前(取消之后就沒必要提供鈎子函數了)
componentWillUnmount
在學習之初你不用記住這些函數的具體名字,你只需要記住這三個大的時刻即可,第一次渲染完成前后,更新前后,取消之前。當你要使用時,再查具體對應的名字叫什么即可。
而且根據我的經驗,初學之時,其實也不知道這些鈎子函數會有什么用,會在什么時候用,這需要我們在實踐中慢慢掌握,所以也不用着急。當我們上手寫了幾個稍微復雜的例子,自然會知道如何去使用他們。
所以這里我只詳細介紹一下,我們最常用的一個生命周期構造函數,組件第一次渲染完成之后調用的componentDidMount
。
既然是組件第一次渲染完成之后才會調用,也就是說,該函數在react組件的生命周期中,只會調用一次。而渲染完成,則表示組件已經被渲染成為真實DOM插入了html中。所以這時候就可以通過ref獲取真實元素。記住它的特點,這會幫助我們正確的使用它。
修改helloWorld.jsx
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// src/helloWorld.jsx
import React, { Component } from 'react';
class HelloWorld extends Component {
clickHander = () => {
console.log(this.refs)
}
// 這時已經可以獲取到真實DOM,而componentWillMount則不行
componentDidMount (props) {
console.log(this.refs)
}
render () {
return (
<div className="container" onClick={this.clickHander}>
<div ref="hello" className="hello">Hello</div>
<div ref="world" className="world">World</div>
</div>
)
}
}
export default HelloWorld;
|
我們在實際開發中,常常需要通過ajax獲取數據,而數據請求的這個行為,則最適合放在componentDidMount
中來執行。
通常會在首次渲染改變組件狀態(state)的行為,或者稱之為有副作用的行為,都建議放在
componentDidMount
中來執行。主要是因為state的改動會引發組件的重新渲染。
組件之間的交互
作為react學習中的一個非常重要的點,組件之間的交互還是需要我們認真掌握的。這個時候hello world就滿足不了我們學習的欲望了,所以我們可以先把它給刪掉。
那么組件之間的交互,大概可以分為如下兩種:
- 父組件與子組件之間交互
- 子組件與子組件之間交互
當然可能有的人會問,2個不相干的組件之間如何交互?如果,你的代碼里,出現了兩個不相干的組件還要交互,那說明你的組件划分肯定是有問題的。這就是典型的給自己挖坑找事兒。即使確實有,那也是通過react-redux把他們變成子組件對吧。但是,通常情況下,不到萬不得已,並不建議使用react-redux,除非你的項目確實非常龐大了,需要管理的狀態非常多了,已經不得不使用,一定要記住,react-redux這類狀態管理器是最后的選擇。
我們來想想一個簡單常見的場景:頁面里有一個submit提交按鈕,當我們點擊提交后,按鈕前出現一個loading圖,並變為不可點擊狀態,片刻之后,接口請求成功,飄出一個彈窗,告訴你,提交成功。大家可以想一想,這種場景,借助react組件應該如何做?
首先可以很簡單的想到,將按鈕與彈窗分別划分為兩個不同的組件:
。然后創建一個父組件來管理這兩個子組件
。
那么在父組件中,我們需要考慮什么因素?Button的loading圖是否展示,彈窗是否展示對吧。
OK,根據這些思考,我們開始來實現這個簡單的場景。
首先創建一個Button
組件。在src目錄下創建一個叫做Button.jsx
的文件,代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/Button.jsx
import React from 'react';
const Button = (props) => {
const { children, loading, submit } = props
return (
<button onClick={submit} disabled={ loading ? 'disabled' : null }>
{ loading && <i className="loading"></i> }
{ children }
</button>
)
}
export default Button;
|
注意,當你引入了一個新創建的文件時,可能需要重新啟動服務才會找得到新的組件
由於這里的Button組件僅僅是簡單的展示,並無額外的邏輯需要處理,因此我們使用無狀態的組件。在這個組件里,出現了一個新的知識點:children
1
2
3
4
5
6
7
8
9
|
// 假如我們這樣使用Button組件時
<Button>確認</Button>
// 那么標簽中間的確認二字就會放入props的children屬性中
// 無狀態組件中
props.children = '確認'
// 有狀態組件中
this.props.children = '確認'
|
當然,children還可以是更多的元素,這和我們熟知的DOM元素的children保持一致。
還有一個需要注意的知識點,則是在jsx模板中,我們可以使用JavaScript表達式來執行簡單的邏輯處理
我們可以列舉一些常見的表達式:
1
2
3
4
5
6
7
|
<div>{ message }</div>
<Button disabled={ loading ? 'disabled' : null }></Button>
{ dialog && <Dialog /> }
{ pending ? <Aaaa /> : <Bbbb /> }
|
如果對於JavaScript表達式了解不夠多的朋友,建議深入學習一下相關的知識。
理解了這些知識之后,相信對於上面的Button組件所涉及到的東西也就能夠非常清楚知道是怎么回事了。接下來,我們需要創建一個彈窗組件,Dialog.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// src/Dialog.jsx
import React, { Component } from 'react';
const Dialog = (props) => {
const { message, close } = props;
return (
<div className="dialog-backdrop">
<div className="dialog-container">
<div className="dialog-header">提示</div>
<div className="dialog-body">{ message }</div>
<div className="dialog-footer">
<button className="btn" onClick={ close }>確定</button>
</div>
</div>
</div>
)
}
export default Dialog;
|
這個組件沒有太多特別的東西,唯一需要關注的一點是,我們也可以通過props傳遞一個函數給子組件。例如這里的close方法。該方法在父組件中定義,但是卻在子組件Dialog中執行,他的作用是關閉彈窗。
我們很容易知道父組件想要修改子組件,只需要通過改變傳入的props屬性即可。那么子組件想要修改父組件的狀態呢?正是父組件通過向子組件傳遞一個函數的方式來改變。
該函數在父組件中定義,在子組件中執行。而函數的執行內容,則是修改父組件的狀態。這就是close的原理,我們來看看父組件中是如何處理這些邏輯的。
創建一個父組件App.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
// src/App.jsx
import React, { Component } from 'react';
import Button from './Button';
import Dialog from './Dialog';
import './style.css';
class App extends Component {
state = {
loading: false,
dialog: false,
message: 'xxx'
}
submit = () => {
this.setState({
loading: true
})
// 模擬數據請求的過程,假設數據請求會經歷1s得到結果
setTimeout(() => {
// 通過隨機數的方式模擬可能出現的成功與失敗兩種結果
const res = Math.random(1);
if (res > 0.5) {
this.setState({
dialog: true,
message: '提交成功!'
})
} else {
this.setState({
dialog: true,
message: '提交失敗!'
})
}
this.setState({ loading: false })
}, 1000)
}
close = () => {
this.setState({
dialog: false
})
}
render () {
const { loading, dialog, message } = this.state;
return (
<div className="app-wrap">
<Button loading={ loading } submit={ this.submit }>提交</Button>
{ dialog && <Dialog message={ message } close={ this.close } /> }
</div>
)
}
}
export default App;
|
App組件的state中,loading用於判斷Button按鈕是否顯示loading圖標,dialog用於判斷是否需要顯示彈窗,message則是表示彈窗的提示內容。
我們自定義的鈎子函數submit
和close
則分別是與子組件Button與Dialog交互的一個橋梁。前面我們說過了,想要在子組件中改變父級的狀態,就需要通過在父組件中創建鈎子函數,並傳遞給子組件執行的方式來完成。
在App.jsx中我們還看到代碼中引入了一個css文件。這是構建工具幫助我們整合的方式,我們可以直接將css文件當做一個單獨的模塊引入進來。我們還可以通過同樣的方式引入圖片等資源。
style.css也是在src目錄下創建的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
// src/style.scss
button {
background: none;
border: none;
outline: none;
width: 100px;
height: 30px;
border: 1px solid orange;
border-radius: 4px;
font-size: 16px;
display: block;
margin: 20px auto;
}
.loading {
display: inline-block;
width: 10px;
height: 10px;
border: 2px solid #ccc;
border-radius: 10px;
margin-right: 10px;
border-bottom: transparent;
border-top: transparent;
animation-name: loading;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.dialog-backdrop {
background: rgba(0, 0, 0, 0.2);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.dialog-container {
width: 300px;
background: #FFFFFF;
border-radius: 4px;
position: absolute;
top: 20%;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px;
}
.dialog-header {
height: 20px;
text-align: center;
line-height: 20px;
}
.dialog-body {
line-height: 1.6;
text-align: center;
margin-top: 20px;
}
.dialog-footer {
margin-top: 20px;
}
.dialog-footer button {
margin: 0 auto;
border: none;
background: orange;
color: #fff;
}
@keyframes loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
|
最后修改index.js,即可將程序運行起來。
1
2
3
4
5
6
7
8
|
// src/index.js
import React from 'react';
import { render } from 'react-dom';
import App from './App';
const root = document.querySelector('#root');
render(, root);
|
那么總結一下組件之間的交互。
父組件改變子組件,通過改變傳入的props屬性值即可。
而子組件改變父組件的狀態,則需要在父組件中創建鈎子函數,然后讓鈎子函數通過props傳遞給子組件,並在子組件中執行。
那么子組件與子組件之間的交互方式,也就是通過影響共同的父組件來進行交互的。正如我們這個例子中的點擊按鈕,出現彈窗一樣。這就是react組件之間交互的核心。
異步組件
在學習異步組件之前,可能還需要大家去折騰一下如何禁用瀏覽器的跨域限制。禁用跨域限制可以讓我們使用更多的公共api進行學習,但是很多人並不知道還可以這樣玩。總之一句話,知道了如何禁用瀏覽器的跨域限制,會讓你的學習速度提升很多,很多項目你就可以動手自己嘗試了。
我這里只能提供在mac環境下如何禁用chrome瀏覽器的跨域限制。在命令行工具中輸入以下指令啟動chrome即可。
1
|
> open -a "Google Chrome" --args --disable-web-security --user-data-dir
|
在safari瀏覽器中則更加簡單。
windows環境下如何做需要大家自己去研究。
OK,禁用跨域限制以后,我們就可以自如的請求別人的接口。這個時候再來學習異步組件就能輕松很多。
異步組件並不是那么復雜,由於接口請求會經歷一點時間,因此在組件第一次渲染的時候,並不能直接將我們想要的數據渲染完成,那么就得再接口請求成功之后,重新渲染一次組件。上面的知識已經告訴大家,通過使用this.setState
修改state的值可以達到重新渲染的目的。
所以我們通常的做法就是在接口請求成功之后,使用this.setState
。
為了降低學習難度,我們暫時先使用jquery中提供的方法來請求數據。
目前比較常用的是axios
首先在我們的項目中,安裝jquery庫。我們通常都會使用這樣的方式來安裝新的組件和庫。
1
|
> npm install jquery
|
然后在src目錄下創建一個News.jsx,借助知乎日報的api,我們來嘗試完成一個簡單的異步組件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
// src/News.jsx
import React, { Component } from 'react';
import $ from 'jquery';
class News extends Component {
state = {
stories: [],
topStories: []
}
componentDidMount() {
$.get('http://news-at.zhihu.com/api/4/news/latest').then(resp => {
console.log(resp);
this.setState({
stories: resp.stories,
topStories: resp.top_stories
})
})
}
render() {
const { stories, topStories } = this.state;
// 觀察每一次render數據的變化
console.log(this.state);
return (
<div className="latest-news">
<section className="part1">
<div className="title">最熱</div>
<div className="container">
{
topStories.map((item, i) => (
<div className="item-box" key={i}>
<img src={ item.image } alt=""/>
<div className="sub-title">{ item.title }</div>
</div>
))
}
</div>
</section>
<section className="part2">
<div className="title">熱門</div>
<div className="container">
{
stories.map((item, i) => (
<div className="item-box" key={i}>
<img src={ item.images[0] } alt=""/>
<div className="sub-title">{ item.title }</div>
</div>
))
}
</div>
</section>
</div>
)
}
}
export default News;
|
在style.css
中簡單補上相關的css樣式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// src/style.css
.latest-news {
width: 780px;
margin: 20px auto;
}
.latest-news section {
margin-bottom: 20px;
}
.latest-news .title {
height: 40px;
line-height: 40px;
font-size: 16px;
padding: 0 10px;
}
.latest-news .container {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.latest-news .item-box {
width: 30%;
overflow: hidden;
margin-bottom: 20px;
}
.latest-news .item-box img {
width: 100%;
height: 200px;
}
.latest-news .item-box .sub-title {
font-size: 12px;
line-height: 1.6;
margin-top: 10px;
}
|
並在App.jsx中引入使用即可。
1
2
3
4
5
|
// src/App.jsx
import News from './News';
// 將下面這一句放於render函數的jsx模板中即可
|
這個組件除了獲取數據,沒有額外的邏輯處理,但仍然有幾個需要非常注意的地方。
1、 若非特殊情況,盡量保證數據請求的操作在componentDidMount
中完成。
2、 react中的列表渲染通常通過調用數組的原生方法map方法來完成,具體使用方式可參考上例。
3、為了確保性能,被渲染的每一列都需要給他配置一個唯一的標識,正入上栗中的key={i}
。我們來假想一個場景,如果我們在數組的最前面新增一條數據,如果沒有唯一的標識,那么所有的數據都會被重新渲染,一旦數據量過大,這會造成嚴重的性能消耗。唯一標識會告訴react,這些數據已經存在了,你只需要渲染新增的那一條就可以了。
4、如果你想要深入了解該組件的具體變化,你可以在render方法中,通過console.log(this.state)
的方式,觀察在整個過程中,組件渲染了多少次,已經每一次this.state
中的具體值是什么,是如何變化的。
高階組件
很多人寫文章喜歡把問題復雜化,因此當我學習高階組件的時候,查閱到的很多文章都給人一種高階組件高深莫測的感覺。但是事實上卻未必。我們常常有一些口頭俗語,比如說“包一層”就是可以用來簡單解釋高階組件的。在普通組件外面包一層邏輯,就是高階組件。
在進一步學習高階組件之前,我們來回顧一下new與構造函數之間的關系。在前面我有文章提到過為什么構造函數中this在運行時會指向new出來的實例,不知道還有沒有人記得。我將那段代碼復制過來。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// 先一本正經的創建一個構造函數,其實該函數與普通函數並無區別
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
}
// 將構造函數以參數形式傳入
function New(func) {
// 聲明一個中間對象,該對象為最終返回的實例
var res = {};
if (func.prototype !== null) {
// 將實例的原型指向構造函數的原型
res.__proto__ = func.prototype;
}
// ret為構造函數執行的結果,這里通過apply,將構造函數內部的this指向修改為指向res,即為實例對象
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
// 當我們在構造函數中明確指定了返回對象時,那么new的執行結果就是該返回對象
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
// 如果沒有明確指定返回對象,則默認返回res,這個res就是實例對象
return res;
}
// 通過new聲明創建實例,這里的p1,實際接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());
// 當然,這里也可以判斷出實例的類型了
console.log(p1 instanceof Person); // true
|
在上面的例子中,首先我們定義了一個本質上與普通函數沒區別的構造函數,然后將該構造函數作為參數傳入New函數中。我在New函數中進行了一些的邏輯處理,讓New函數的返回值為一個實例,正因為New的內部邏輯,讓構造函數中的this能夠指向返回的實例。這個例子就是一個“包一層”的案例。
再來看一個簡單的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import React, { Component } from 'react';
class Div extends Component {
componentDidMount() {
console.log('這是新增的能力');
}
render () {
return (
<div>{ this.props.children }</div>
)
}
}
export default Div;
|
在上面的例子中,我們把html的DIV標簽作為基礎元件。對他新增了一個輸出一條提示信息的能力。而新的Div組件,就可以理解為div標簽的高階組件。所以到這里希望大家已經理解了包一層的具體含義。
react組件的高階組件,就是在基礎react組件外面包一層,給該基礎組件賦予新的能力。
OK,我們來試試定義第一個高階組件,該高階組件的第一個能力,就是向基礎組件中傳入一個props參數。
在例子中,傳入的參數可能沒有任何實際意義,但是在實際開發中,我們可以傳入非常有必要的參數來簡化我們的代碼和邏輯。
先來定義一個擁有上述能力的高階組件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// src/Addsss.jsx
import React from 'react';
// 定義一個接受一個react組件作為參數的函數
function Addsss(Container) {
// 該函數返回一個新的組件,我們可以在該組件中進行新能力的附加
return class Asss extends React.Component {
componentDidMount() {}
render() {
return (
{ this.props.children }
)
}
}
}
export default Addsss;
|
盡管這個高階組價足夠簡單,但是他已經呈現了高階組件的定義方式。現在我們在一個基礎組件中來使用該高階組件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/basic.jsx
import React, { Component } from 'react';
import Addsss from './Addsss';
class Basic extends Component {
componentDidMount() {
console.log(this.props.name);
}
render() {
return (
<div className={this.props.name}>{ this.props.children }</div>
)
}
}
export default Addsss(Basic);
|
我們看到其實在基礎組件中,對外拋出的接口是Addsss(Basic)
,這是高階組件里定義的函數運行的結果。也就是說,其實基礎組件中返回的是高階組件中定義的Asss中間組件。這和new的思路幾乎完全一致。
當然,想要理解,並熟練使用高階組件並不是一件容易的事情,大家初學時也不用非要完全掌握他。當你對react慢慢熟練之后,你可以嘗試使用高階組件讓自己的代碼更加靈活與簡練。這正是向函數式編程思維轉變的一個過程。
在進步學習的過程中,你會發現無論是路由組件react-router,或者react-redux都會使用高階組件來實現一些功能。只要你遇到他們的時候,你能明白,哦,原來是這么回事兒就行了。
react路由
react提供了react-router組件來幫助我們實現路由功能。
但是react-router是一個不太好講的知識點。因為由於react-router 4進行了顛覆性的更新,導致了react-router 3與react-router 4的使用方式大不一樣。也正是由於變化太大,所以很多項目仍然正在使用react-router3,並且沒有過渡到react-router4的打算。
因此這里我就不多講,提供一些參考學習資料。
- react-router 3: react-router 3 使用教程
- react-router 4: react-router 4 全攻略
未完待續
由於時間關系,暫時就只能寫到這里了。
本來還寫了一個比較完整的例子也在這篇文章里逐步分析如何實現的,但是時間確實不夠。所以如果覺得看了上面的知識還想進一步學習的話,可以先去https://github.com/yangbo5207/advance15 看看這個完整例子的樣子。
另外我曾經寫了一篇如何快速掌握一門前端框架,希望大家可以參考參考。
按照我的計划,只要理解了上面我所提到的知識,並把我准備的這個完整例子理解了。那么你的react掌握程度也算是小有所成了。至少應屆畢業生找工作能提到這些思維方式應該會很有幫助。