一杯茶的時間,上手 React 框架開發
- 2020 年 4 月 7 日
- 筆記

React(也被稱為 React.js 或者 ReactJS)是一個用於構建用戶界面的 JavaScript 庫。起源於 Facebook 內部項目,最初用來架設 Instagram 的網站,並於 2013 年 5 月開源。React 性能較高,並且它的聲明式、組件化特性讓編寫代碼變得簡單,隨着 React 社區的發展,越來越多的人投入 React 的學習和開發,使得 React 不僅可以用來開發 Web 應用,還能開發桌面端應用,TV應用,VR應用,IoT應用等,因此 React 還具有一次學習,隨處編寫的特性。本教程將帶你快速入門 React 開發,通過 20-30 分鐘的學習,你不僅可以了解 React 的基礎概念,而且能開發出一個待辦事項小應用,還在想什麼了?馬上學起來吧!本文所有代碼已放在 GitHub 倉庫[1]中。
此教程屬於 React 前端工程師學習路線[2]的一部分,點擊可查看全部內容。
Hello, World
我們將構建什麼?
在這篇教程中,我們將展示給你如何使用 React 構建一個待辦事項應用,下面最終項目的展示成果:

你也可以在這裡看到我們最後構建的結果:最終結果[3]。如果你現在對代碼還不是很理解,或者你還不熟悉代碼語法,別擔心!這篇教程的目標就是幫助你理解 React 和它的語法。
我們推薦你在繼續閱讀這篇教程之前先熟悉一下這個待辦事項,你甚至可以嘗試添加幾個待辦事項!你可能注意到當你添加了2個待辦事項之後,會出現不同的顏色;這就是 React 中條件渲染的魅力。
當你熟悉了這個待辦事項之後你就可以關閉它了。在這篇教程的學習中,我們將從一個 Hello World 代碼模板開始,然後帶領你初始化開發環境,這樣你就可以開始構建這個待辦事項了。
你將學到什麼?
你將學習所有 React 的基礎概念,其中又分為三個部分:
•編寫組件相關:包括 JSX 語法、Component、Props•組件的交互:包括 State 和生命周期•組件的渲染:包括列表和 Key、條件渲染•和 DOM & HTML 相關:包括事件處理、表單。
前提條件
我們假設你熟系 HTML 和 JavaScript,但即使你是從其他編程語言轉過來的,你也能看懂這篇教程。我們還假設你對一些編程語言的概念比較熟悉,比如函數、對象、數組,如果對類了解就更好了。
如果你需要複習 JavaScript,我們推薦你閱讀這篇指南[4]。你可能注意到了我們使用了一些 ES6 的特性 — 一個最近的 JavaScript 版本。在這篇教程,我們會使用 arrow functions[5],classes[6],和 const[7]。你可以使用 Babel REPL[8] 來檢查 ES6 代碼編譯之後的結果。
環境準備
首先準備 Node 開發環境,訪問 Node 官方網站[9]下載並安裝。打開終端輸入如下命令檢測 Node 是否安裝成功:
node -v # v10.16.0 npm -v # 6.9.0
注意 Windows 用戶需要打開 cmd 工具,Mac 和 Linux 是終端。
如果上面的命令有輸出且無報錯,那麼代表 Node 環境安裝成功。接下來我們將使用 React 腳手架 — Create React App[10](簡稱 CRA)來初始化項目,同時這也是官方推薦初始化 React 項目的最佳方式。
在終端中輸入如下命令:
npx create-react-app my-todolist
等待命令運行完成,接着輸入如下命令開啟項目:
cd my-todolist && npm start
CRA 會自動開啟項目並打開瀏覽器,你應該可以看到下面的結果:

??? 恭喜你!成功創建了第一個 React 應用!
現在 CRA 初始化的項目里有很多無關的內容,為了開始接下來的學習,我們還需要做一點清理工作。首先在終端中按 ctrl + c
關閉剛剛運行的開發環境,然後在終端中依次輸入如下的命令:
# 進入 src 目錄 cd src # 如果你在使用 Mac 或者 Linux: rm -f * # 或者,你在使用 Windows: del * # 然後,創建我們將學習用的 JS 文件 # 如果你在使用 Mac 或者 Linux: touch index.js # 或者,你在使用 Windows type nul > index.js # 最後,切回到項目目錄文件夾下 cd ..
此時如果在終端項目目錄下運行 npm start
會報錯,因為我們的 index.js
還沒有內容,我們在終端中使用 ctrl +c
關閉開發服務器,然後使用編輯器打開項目,在剛剛創建的 index.js 文件中加入如下代碼:
import React from "react"; import ReactDOM from "react-dom"; class App extends React.Component { render() { return <div>Hello, World</div>; } } ReactDOM.render(<App />, document.getElementById("root"));
我們看到 index.js
裏面的代碼分為三個部分。
首先是一系列導包,我們導入了 react
包,並命名為 React,導入了 react-dom
包並命名為 ReactDOM。對於包含 React 組件(我們將在之後講解)的文件都必須在文件開頭導入 React。
然後我們定義了一個 React 組件,命名為 App,繼承自 React.Component,組件的內容我們將會在後面進行講解。
接着我們使用 ReactDOM 的 render 方法來渲染剛剛定義的 App 組件,render
方法接收兩個參數,第一個參數為我們的 React 根級組件,第二個參數接收一個 DOM 節點,代表我們將把和 React 應用掛載到這個 DOM 節點下,進而渲染到瀏覽器中。
注意 上面代碼的三個部分中,第一部分和第三部分在整篇教程中是不會修改的,同時在編寫任意 React 應用,這兩個部分都是必須的。後面所有涉及到的代碼修改都是關於第二部分代碼的修改,或者是在第一部分到第三部分之間插入或刪除代碼。
保存代碼,在終端中使用 npm start
命令開啟開發服務器,現在瀏覽器應該會顯示如下內容:

準備工作已經就緒!
你可能對上面的代碼細節還不是很清楚,別擔心,我們將馬上帶你領略 React 的神奇世界!
JSX 語法
首先我們來看一下 React 引以為傲的特性之一 — JSX。它允許我們在 JS 代碼中使用 XML 語法來編寫用戶界面,使得我們可以充分的利用 JS 的強大特性來操作用戶界面。
一個 React 組件的 render 方法中 return
的內容就為這個組件所將渲染的內容。比如我們現在的代碼:
render() { return <div>Hello, World</div>; }
這裡的 <div>Hello, World</div>
是一段 JSX 代碼,它最終會被 Babel[11] 轉譯成下面這段 JS 代碼:
React.createElement( 'div', null, 'Hello, World' )
React.createElement()
接收三個參數:
•第一個參數代表 JSX 元素標籤。•第二個參數代表這個 JSX 元素接收的屬性,它是一個對象,這裡因為我們的 div
沒有接收任何屬性,所以它是 null
。•第三個參數代表 JSX 元素包裹的內容。
React.createElement()
會對參數做一些檢查確保你寫的代碼不會產生 BUG,它最終會創建一個類似下面的對象:
{ type: 'div', props: { children: 'Hello, World' } };
這些對象被稱之為 「React Element」。你可以認為它們描述了你想要在屏幕上看到的內容。React 將會接收這些對象,使用它們來構建 DOM,並且對它們進行更新。
注意 我們推薦你使用 「Babel」 [12] 查看 JSX 的編譯結果。
App 組件最終返回這段 JSX 代碼,所以我們使用 ReactDOM 的 render
方法渲染 App 組件,最終顯示在屏幕上的就是 Hello, World"
內容。
JSX 作為變量使用
因為 JSX 最終會被編譯成一個 JS 對象,所以我們可以把它當做一個 JS 對象使用,它享有和一個 JS 對象同等的地位,比如可以將其賦值給一個變量,我們修改上面代碼中的 render 方法如下:
render() { const element = <div>Hello, World</div>; return element; }
保存代碼,我們發現瀏覽器中渲染的內容和我們之前類似。
在 JSX 中使用變量
我們可以使用大括號 {}
在 JSX 中動態的插入變量值,比如我們修改 render 方法如下:
render() { const content = "World"; const element = <div>Hello, {content}</div>; return element; }
保存代碼,發現瀏覽器中效果依然不變。
JSX 中使用 JSX
我們可以在 JSX 中再包含 JSX,這樣我們編寫任意層次的 HTML 結構:
render() { const element = <li>Hello, World</li> return ( <div> <ul> {element} </ul> </div> ) }
JSX 中添加節點屬性
我們可以像在 HTML 中一樣,給元素標籤加上屬性,只不過我們需要遵守駝峰式命名[13]法則,比如在 HTML 上的屬性 data-index
在 JSX 節點上要寫成 dataIndex
。
const element = <div dataIndex="0">Hello, World</div>;
注意 在 JSX 中所有的屬性都要更換成駝峰式命名,比如
onclick
要改成onClick
,唯一比較特殊的就是class
,因為在 JS 中class
是保留字,我們要把class
改成className
。
const element = <div className="app">Hello, World</div>;
實戰
我們使用這一節講解的 JSX 知識,來繼續完成我們的待辦事項應用。
在編輯器中打開 src/index.js
,對 App 組件做如下改變:
class App extends React.Component { render() { const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; return ( <ul> <li>Hello, {todoList[0]}</li> <li>Hello, {todoList[1]}</li> <li>Hello, {todoList[2]}</li> <li>Hello, {todoList[3]}</li> </ul> ); } }
可以看到,我們使用 const
定義了一個 todoList
數組常量,並且在 JSX 中使用 {}
進行動態插值,插入了數組的四個元素。
最後保存代碼,瀏覽器中的效果應該是這樣的:

提示 無需關閉剛才使用
npm start
開啟的開發服務器,修改代碼後,瀏覽器中的內容將會自動刷新!
你可能注意到了我們手動獲取了數組的四個值,然後逐一的用 {}
語法插入到 JSX 中並最終渲染,這樣做還比較原始,我們將在後面列表和 Key小節中簡化這種寫法。
在這一小節中,我們了解了 JSX 的概念,並且實踐了相關的知識。我們還提出了組件的概念,但是並沒有深入講解它,在下一小節中我們將詳細地講解組件的知識。
Component
React 的核心特點之一就是組件化,即我們將巨大的業務邏輯拆分成一個個邏輯清晰的小組件,然後通過組合這些組件來完成業務功能。
React 提供兩種組件寫法:1)函數式組件 2)類組件。
函數式組件
在 React 中,函數式組件會默認接收一個 props
參數,然後返回一段 JSX:
function Todo(props) { return <li>Hello, 圖雀</li>; }
關於 props
我們將在下一節中講解。
類組件
通過繼承自 React.Component 的類來代表一個組件。
class Todo extends React.Component { render() { return <li>Hello, 圖雀</li>; } }
我們發現,在類組件中,我們需要在 render
方法裏面返回需要渲染的 JSX。
組件組合
我們可以組合不同的組件來完成複雜的業務邏輯:
class App extends React.Component { render() { return ( <ul> <Todo /> <Todo /> </ul> ); } }
在上面的代碼中,我們在類組件 App 中使用了我們之前定義的 Todo 組件,我們看到,組件以 <Component />
的形式使用,比如 Todo 組件使用時為 <Todo />
,我們在 Todo 組件沒有子組件時使用這種寫法;當 Todo 組件需要包含子組件時,我們需要寫成下面的形式:
class App extends React.Component { render() { return ( <ul> <Todo>Hello World</Todo> <Todo>Hello Tuture</Todo>> </ul> ); } }
組件渲染
我們在第一節中講到,通過 ReactDOM.render
方法接收兩個參數:1)根組件 2) 待掛載的 DOM 節點,可以將組件的內容渲染到 HTML 中。
ReactDOM.render(<App />, document.getElementById('root'));
實戰
我們運用在這一節中學到的組件知識來繼續完成我們的待辦事項應用。我們編寫一個 Todo 類組件,用於代表單個待辦事項,然後在 App 類組件中使用 Todo 組件。
打開 src/index.js
文件,實現 Todo 組件,並調整 App 組件代碼如下:
class Todo extends React.Component { render() { return <li>Hello, 圖雀</li>; } } class App extends React.Component { render() { const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; return ( <ul> <Todo /> <Todo /> <Todo /> <Todo /> </ul> ); } } ReactDOM.render(<App />, document.getElementById("root"));
保存代碼,然後你應該可以在瀏覽器中看到如下結果:

你可能注意到我們暫時沒有使用之前定義的 todoList 數組,而是使用了四個相同的 Todo 組件,我們使用繼承自 React.Component
類的形式定義 Todo 組件,然後在組件的 render
中返回了 <li>Hello, 圖雀</li>
,所以最終瀏覽器中會渲染四個 "Hello, 圖雀"
。並且因為 Todo 組件不需要包含子組件,所以我們寫成了 <Todo />
的形式。
現在4個待辦事項都是一樣的內容,這有點單調,你可能會想,如果可以像調用函數那樣可以通過傳參對組件進行個性化定製就好了,你的想法是對的!我們將在下一節中引出 props,它允許你給組件傳遞內容,從而進行個性化內容定製。
Props
React 為組件提供了 Props,使得在使用組件時,可以給組件傳入屬性進行個性化渲染。
函數式組件中使用 Props
函數式組件默認接收 props
參數,它是一個對象,用於保存父組件傳遞下來的內容:
function Todo(props) { return ( <li>Hello, {props.content}</li> ) } <Todo content="圖雀" />
我們給 Todo 函數式組件傳遞了一個 content
屬性, 它的值為 "圖雀"
,所有傳遞的屬性都會合併進 props
對象中,然後傳遞給 Todo 組件,這裡 props
對象是這樣的 props = { content: "圖雀" }
,如果我們再傳遞一個屬性:
<Todo content="圖雀" from="圖雀社區" />
最終 props
對象就會變成這樣:props={ content: "圖雀", from: "圖雀社區" }
。
注意 如果給組件傳遞
key
屬性是不會併入 props 對象中的,所以我們在子組件中也取不到key
屬性,我們將在列表和 Key中詳細講解。
類組件中使用 Props
類組件中基本和函數式組件中的 Props 保持一致,除了是通過 this.props
來獲取父組件傳遞下來的屬性內容:
class Todo extends React.Component { render() { return <li>Hello, {this.props.content}</li>; } } <Todo content="圖雀" />
實戰
我們運用這一節中學到的 Props 知識來繼續完成我們的待辦事項應用。
打開 src/index.js
文件,分別調整 Todo 和 App 組件,修改後代碼如下:
import React from "react"; import ReactDOM from "react-dom"; class Todo extends React.Component { render() { return <li>Hello, {this.props.content}</li>; } } class App extends React.Component { render() { const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; return ( <ul> <Todo content={todoList[0]} /> <Todo content={todoList[1]} /> <Todo content={todoList[2]} /> <Todo content={todoList[3]} /> </ul> ); } } ReactDOM.render(<App />, document.getElementById("root"));
注意到我們又重新開始使用之前定義的 todoList 數組,然後給每個 Todo 組件傳遞一個 content
屬性,分別賦值數組的每一項,最後在 Todo 組件中使用我們傳遞下來的 content
屬性。
保存修改的內容,你應該可以在瀏覽器中看到如下的內容:

可以看到,我們的內容又回來了,和我們之前在 JSX 中看到的內容一樣,但是這一次我們成功使用了組件來渲染接收到的 Props 內容。
State 和生命周期
React 通過給類組件提供 State 來創造交互式的內容 — 即內容可以在渲染之後發生變化。
定義 State
通過在類組件中添加 constructor
方法,並在其中定義和初始化 State:
constructor(props) { super(props); this.state = { todoList: ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"] }; }
這裡 constructor 方法接收的 props 屬性就是我們在上一節中講到的那個 props;並且 React 約定每個繼承自 React.Component 的組件在定義 constructor 方法時,要在方法內首行加入 super(props)
。
接着我們 this.state
來定義組件的 state,並使用 { todoList: ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"] }
對象來初始化 state。
使用 State
我們可以在一個組件中的多處地方通過 this.state
的方式來使用 state,比如我們在這一節中將講到的生命周期函數中,比如在 render 方法中:
class App extends React.Component { constructor(props) { super(props); this.state = { todoList: ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"] }; } render() { return ( <ul> <Todo content={this.state.todoList[0]} /> <Todo content={this.state.todoList[1]} /> <Todo content={this.state.todoList[2]} /> <Todo content={this.state.todoList[3]} /> </ul> ); } }
我們通過 this.state.todoList
可以獲取我們在 constructor
方法中定義的 state,可以看到,我們使用 this.state.todoList[0]
的方式替代了之前的 todoList[0]
。
更新 State
我們通過 this.setState
方法來更新 state,從而使得網頁內容在渲染之後還能變化:
this.setState({ todoList: newTodoList });
注意
關於 this.setState
我們需要注意以下幾點:
1)這裡不能夠通過直接修改 this.state
的方式來更新 state
:
// 錯誤的 this.state.todoList = newTodoList;
2)State 的更新是合併更新的:
比如原 state
是這樣的:
constructor(props) { super(props); this.state = { todoList: [], nowTodo: '', }; }
然後你調用 this.setState()
方法來更新 state
:
this.setState({ nowTodo: "Hello, 圖雀" });
React 將會合併更新,即將 nowTodo
的新內容合併進原 this.state
,當更新之後,我們的 this.state
將會是下面這樣的:
this.state = { todoList: [], nowTodo: "Hello, 圖雀" };
不會因為只單獨設置了 nowTodo
的值,就將 todoList
給覆蓋掉。
生命周期函數
React 提供生命周期函數來追蹤一個組件從創建到銷毀的全過程。主要包含三個方面:
•掛載(Mounting)•更新(Updating)•卸載(Unmounting)
一個簡化版的生命周期的圖示是這樣的:

注意 React 生命周期相對而言比較複雜,我們這裡不會詳細講解每個部分,上面的圖示用戶可以試着了解一下,對它有個大概的印象。 查看完整版的生命周期圖示請參考這個鏈接:點擊查看[14]。
這裡我們主要講解掛載和卸載裏面常用的生命周期函數。
掛載
其中掛載中主要常用的有三個方法:
•constructor()
•render()
•componentDidMount()
constructor()
在組件創建時調用,如果你不需要初始化 State
,即不需要 this.state = { ... }
這個過程,那麼你不需要定義這個方法。
render()
方法是掛載時用來渲染內容的方法,每個類組件都需要一個 render 方法。
componentDidMount()
方法是當組件掛載到 DOM 節點中之後會調用的一個方法,我們通常在這裡發起一些異步操作,用於獲取服務器端的數據等。
卸載
卸載只有一個方法:
•componentWillUnmount()
componentWillUnmount 是當組件從 DOM 節點中卸載之前會調用的方法,我們一般在這裏面銷毀定時器等會導致內存泄露的內容。
實戰
我們運用在這一節中學到的 State 和生命周期知識來繼續完成我們的待辦事項應用。
打開 src/index.js
,修改代碼如下:
import React from "react"; import ReactDOM from "react-dom"; const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; class Todo extends React.Component { render() { return <li>Hello, {this.props.content}</li>; } } class App extends React.Component { constructor(props) { super(props); this.state = { todoList: [] }; } componentDidMount() { this.timer = setTimeout(() => { this.setState({ todoList: todoList }); }, 2000); } componentWillUnmount() { clearTimeout(this.timer); } render() { return ( <ul> <Todo content={this.state.todoList[0]} /> <Todo content={this.state.todoList[1]} /> <Todo content={this.state.todoList[2]} /> <Todo content={this.state.todoList[3]} /> </ul> ); } } ReactDOM.render(<App />, document.getElementById("root"));
可以看到我們主要改動了五個部分:
•將 todoList 移動到組件外面。•定義 constructor 方法,並且通過設置 this.state = { todoList: [] }
來初始化組件的 State,這裡我們將 todoList
初始化為空數組。•添加 componentDidMount
生命周期方法,當組件掛載到 DOM 節點之後,設置一個時間為 2S 的定時器,並賦值給 this.timer
,用於在組件卸載時銷毀定時器。等到 2S 之後,使用 this.setState({ todoList: todoList })
來使用我們剛剛移動到組件外面的 todoList 來更新組件的 this.state.todoList
。•添加 componentWillUnMount
生命周期方法,在組件卸載時,通過 clearTimeout(this.timer)
來清除我們之前設置的定時器,防止出現內存泄露。
保存修改的代碼,我們應該會看到瀏覽器中有一個內容更新的過程,在組件剛剛創建並掛載時,瀏覽器屏幕上應該是這樣的:

因為我們在 this.state
初始化時,將 todoList
設置為了空數組,所以一開始 "Hello"
後面的 this.props.content
內容為空,我們就出現了四個 "Hello, "
。
然後當過了 2S 之後,我們可以看到熟悉的內容出現了:

因為在過了 2S 之後,我們在定時器的回調函數中將 todoList
設置為了定義在組件外面的那個 todoList
數組,它有四個元素,所以顯示在瀏覽器上面的內容又是我們之前的樣子。
恭喜你!成功創建了自己第一個交互式的組件!
讀到這裡,你有可能有點累了,試着離開座椅,活動活動,喝杯咖啡,精彩稍後繼續 ?
列表和 Key
目前我們有四個 Todo 組件,我們是一個一個取值然後渲染,這顯得有點原始,並且不可擴展,因為當我們的 todoList 數組很大的時候(比如 100 個元素),一個一個獲取就顯得不切實際了,這個時候我們就需要循環介入了。
渲染組件列表
JSX 允許我們渲染一個列表:
render() { const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; const renderTodoList = todoList.map((todo) => ( <Todo content={todo} /> )); return ( <ul> {renderTodoList} </ul> ); }
我們通過對 todoList 進行 map
遍歷,返回了一個 Todo 列表,然後使用 {}
插值語法渲染這個列表。
當然我們可以在 JSX 中使用表達式,所以上面的代碼可以寫成這樣:
render() { const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; return ( <ul> {todoList.map((todo) => ( <Todo content={todo} /> ))} </ul> ); }
加上 Key
React 要求給列表中每個組件加上 key
屬性,用於標誌在列表中這個組件的身份,這樣當列表內容進行了修改:增加或刪除了元素時,React 可以根據 key
屬性高效的對列表組件進行創建和銷毀操作:
render() { const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; return ( <ul> {todoList.map((todo, index) => ( <Todo content={todo} key={index} /> ))} </ul> ); }
這裡我們使用了列表的 index
作為組件的 key
值,React 社區推薦的最佳實踐方式是使用列表數據元素的唯一標識符作為 key
值,如果你的數據是來自數據庫獲取,那麼列表元素數據的主鍵可以作為 key
。
這裡的 key
值不會作為 props傳遞給子組件,React 會在編譯組件時將 key
值從 props 中排除,即最終我們的第一個 Todo 組件的 props
如下:
props = { content: "圖雀" }
而不是我們認為的:
props = { content: "圖雀", key: 0 }
實戰
我們運用這一節學到的列表和 Key 的知識來繼續完成我們的待辦事項應用。
打開 src/index.js
,代碼修改如下:
import React from "react"; import ReactDOM from "react-dom"; const todoList = ["圖雀", "圖雀寫作工具", "圖雀社區", "圖雀文檔"]; class Todo extends React.Component { render() { return <li>Hello, {this.props.content}</li>; } } class App extends React.Component { constructor(props) { super(props); this.state = { todoList: [] }; } componentDidMount() { this.timer = setTimeout(() => { this.setState({ todoList: todoList }); }, 2000); } componentWillUnmount() { clearTimeout(this.timer); } render() { return ( <ul> {this.state.todoList.map((todo, index) => ( <Todo content={todo} key={index} /> ))} </ul> ); } } ReactDOM.render(<App />, document.getElementById("root"));
可以看到,我們將之前的手動獲取元素渲染改成了使用內嵌表達式的方式,通過對 this.state.todoList
列表進行 map 操作生成Todo 組件列表,然後使用列表的 index
作為組件的 key
值,最後渲染。
保存內容,查看瀏覽器裏面的內容,我們可以看到內容會有一個變化的過程,一開始是這樣的:

你會發現一片空白,然後過了 2S 變成了下面這樣:

這是因為,一開始 this.state.todoList
在 constructor 中初始化時是空數組, this.state.todoList
進行 map 操作時返回空數組,所以我們的瀏覽器中沒有內容,當組件掛載之後,等待 2S,我們更新 this.state.todoList
內容,就看到瀏覽器裏面獲得了更新。
條件渲染
在 React 中,我們可以根據不同的情況,渲染不同的內容,這也被成為條件渲染。
if-else 條件渲染
render() { if (this.props.content === "圖雀") { return <li>你好, {this.props.content}</li>; } else { return <li>Hello, {this.props.content}</li>; } }
在上面的代碼中,我們判斷 this.props.content
的內容,當內容為 "圖雀"
時,我們渲染 "你好, 圖雀"
,對於其他內容我們依然渲染 "Hello, 圖雀"
。
三元表達式條件渲染
我們還可以直接在 JSX 中使用三元表達式進行條件渲染:
render() { return this.props.content === "圖雀"? ( <li>你好, {this.props.content}</li> ) : ( <li>Hello, {this.props.content}</li> ); }
當然三元表達式還可以用來條件渲染不同的 React 元素屬性:
render() { return ( <li className={this.state.isClicked ? 'clicked' : 'notclicked'}>Hello, {this.props.content}</li> ) }
上面我們判斷組件的 this.state.isClicked
屬性,如果 this.state.isClicked
屬性為 true
,那麼我們最終渲染 "clicked"
類:
render() { return ( <li className="clicked"}>Hello, {this.props.content}</li> ) }
如果 this.state.isClicked
為 false
,那麼最終渲染 notclicked
類:
render() { return ( <li className="notclicked">Hello, {this.props.content}</li> ) }
實戰
我們運用本節學到的知識繼續完成我們的待辦事項應用。
打開 src/index.js
,對 Todo 和 App 組件作出如下修改:
class Todo extends React.Component { render() { if (this.props.index % 2 === 0) { return <li style={{ color: "red" }}>Hello, {this.props.content}</li>; } return <li>Hello, {this.props.content}</li>; } } class App extends React.Component { constructor(props) { super(props); this.state = { todoList: [] }; } componentDidMount() { this.timer = setTimeout(() => { this.setState({ todoList: todoList }); }, 2000); } componentWillUnmount() { clearTimeout(this.timer); } render() { return ( <ul> {this.state.todoList.map((todo, index) => ( <Todo content={todo} key={index} index={index} /> ))} </ul> ); } }
我們在首先在 App 組件中給 Todo 組件傳入了一個 index
屬性,然後在 Todo 組件的 render 方法中,對 this.props.index
進行判斷,如果為偶數,那麼渲染一個紅色的文字,如果為奇數則保持不變。
這裡我們通過給 li
元素的 style
屬性賦值一個對象來實現在 JSX 中設置元素的 CSS 屬性,我們可以通過同樣的方式設置任何 CSS 屬性:
// 黑底紅字的 Hello, 圖雀 <li style={{ color: "red", backgroundColor: "black"}}>Hello, 圖雀</li>
注意 這裡有一點需要做一下改變,就是像
background-color
這樣的屬性,要寫成駝峰式backgroundColor
。對應的比如font-size
,也要寫成fontSize
。
保存代碼,你應該可以在瀏覽器中看到如下內容:

我們看到瀏覽器中的效果確實是在偶數項(數組中 0 和 2 項)變成了紅色的字體,而(數組中 1 和 3 項)還是之前的黑色樣式。
事件處理
在 React 元素中處理事件和在 HTML 中類似,就是寫法有點不一樣。
JSX 中的事件處理
這裡的不一樣主要包含以下兩點:
•React 中的事件要使用駝峰式命名:onClick
,而不是全小寫:onclick
。•在 JSX 中,你傳遞的是一個事件處理函數,而不是一個字符串。
在 HTML 中,我們處理事件是這樣的:
<button onclick="handleClick()">點我</button>
在 React 中,我們需要寫成下面這樣:
function Button() { function handleClick() { console.log('按鈕被點擊了'); } return ( <button onClick={handleClick}>點我</button> ) }
注意到我們在上面定義了一個函數式組件,然後返回一個按鈕,並在按鈕上面定義了點擊事件和對應的處理方法。
注意 這裡我們的的點擊事件使用了駝峰式的
onClick
來命名,並且在 JSX 中傳給事件的屬性是一個函數:handleClick
,而不是之前 HTML 中單純的一個字符串:"handleClick()"
。
合成事件
我們在以前編寫 HTML 的事件處理時,特別是在處理表單時,常常需要禁用瀏覽器的默認屬性。
提示 比如一般表單提交時都會刷新瀏覽器,但是我們有時候希望提交表單之後不刷新瀏覽器,所以我們需要禁用瀏覽器的默認屬性。
在 HTML 中我們禁用事件的默認屬性是通過調用定義在事件上的 preventDefault
或者設置事件的 cancelBubble
:
// 當點擊某個鏈接之後,禁止打開頁面 document.getElementById("myAnchor").addEventListener("click", function(event){ event.preventDefault() });
在 JSX 中,事件處理和這個類似:
function Link() { function handleClick(event) { event.preventDefault(); console.log('鏈接被點擊了,但是它不會跳轉頁面,因為默認行為被禁用了'); } return ( <a onClick={handleClick} href="https://tuture.co">點我</a> ) }
實戰
我們運用這一節中學到的知識來繼續完成我們的待辦事項應用。
我們之前的待辦事項的 todoList 數組都是直接硬編碼在代碼里,不可以進行增刪改,這相當死板,一個更真實的 todoList 應該要具備增加功能,這一功能實現需要兩個步驟:
•允許用戶輸入新的待辦事項。•將這個輸入的待辦事項加入到現有的 todoList 列表裏面。
在這一小節中,我們將來實現第一個步驟的內容。
打開 src/index.js
,對 App 組件內容作出如下修改:
class App extends React.Component { constructor(props) { super(props); this.state = { nowTodo: "", todoList: [] }; } componentDidMount() { this.timer = setTimeout(() => { this.setState({ todoList: todoList }); }, 2000); } componentWillUnmount() { clearTimeout(this.timer); } handleChange(e) { this.setState({ nowTodo: e.target.value }); } render() { return ( <div> <div> <input type="text" onChange={e => this.handleChange(e)} /> <div>{this.state.nowTodo}</div> </div> <ul> {this.state.todoList.map((todo, index) => ( <Todo content={todo} key={index} index={index} /> ))} </ul> </div> ); } }
可以看到,我們新加入的代碼主要有四個部分:
•首先在 state 裏面添加了一個新的屬性 nowTodo
,我們將用它來保存用戶新輸入的待辦事項。•然後我們在 render 方法裏面,在返回的 JSX 中使用 div
將內容包裹起來,接着加入了一個 div
,在裏面加入了一個 input
和 一個 div
,input 用於處理用戶的輸入,我們在上面定義了 onChange
事件,事件處理函數是一個箭頭函數,它接收事件 e
,然後用戶輸入時,會在函數裏面調用 this.handleChange
方法,將事件 e
傳給這個方法。•在 handleChange
裏面通過 this.setState
使用 input 的值來更新 nowTodo
的內容。•最後在 div 裏面使用 {}
插值語法展示 nowTodo
的內容。
保存代碼,打開瀏覽器,你應該可以看到如下的內容:

當你嘗試在輸入框中鍵入內容時,輸入框的下面應會顯示相同的內容:

這是因為當我們在輸入框裏面輸入內容時,我們使用了輸入框的值更新 this.state.nowTodo
,然後在輸入框之下展示 this.state.nowTodo
的值。
表單
接下來我們來完成增加新的待辦事項的功能的第二個步驟:允許用戶將新輸入的待辦事項加入到 todoList 列表中。
打開 src/index.js
文件,對 App 組件做出如下修改:
class App extends React.Component { constructor(props) { super(props); this.state = { nowTodo: "", todoList: [] }; } componentDidMount() { this.timer = setTimeout(() => { this.setState({ todoList: todoList }); }, 2000); } componentWillUnmount() { clearTimeout(this.timer); } handleChange(e) { this.setState({ nowTodo: e.target.value }); } handleSubmit(e) { e.preventDefault(e); const newTodoList = this.state.todoList.concat(this.state.nowTodo); this.setState({ todoList: newTodoList, nowTodo: "" }); } render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <input type="text" onChange={e => this.handleChange(e)} /> <button type="submit">提交</button> </form> <ul> {this.state.todoList.map((todo, index) => ( <Todo content={todo} key={index} index={index} /> ))} </ul> </div> ); } }
上面的變化主要有三個部分:
•首先我們將 render
方法中返回的 JSX 的最外層的 div
替換成了 form
,然後在上面定義了 onSubmit
提交事件,並且通過一個箭頭函數接收事件 e
來進行事件處理,在 form 被提交時,在箭頭函數裏面會調用 handleSubmit
方法, 並將 e 傳遞給這個函數。•在 handleSubmit 方法裏面,我們首先調用了 e.preventDefault()
方法,來禁止瀏覽器的默認事件處理,這樣我們在提交表單之後,瀏覽器就不會刷新,之後是將現有的 this.sate.todoList
列表加上新輸入的 nowTodo
,最後是使用 this.setState
更新 todoList
和 nowTodo
;這樣我們就可以通過輸入內容添加新的待辦事項了。•接着我們將之前的展示 this.state.nowTodo
的 div 替換成 提交按鈕 button
,並將 button 的 type
設置為 submit
屬性,表示在點擊這個 button 之後,會觸發表單提交;將新輸入的內容加入現有的待辦事項中。
注意 我們在
handleSubmit
方法裏面使用this.setState
更新狀態時,將nowTodo
設置為了空字符串,代表我們在加入新的待辦事項之後,將清除現有輸入的 nowTodo 待辦事項內容。
保存代碼,打開瀏覽器,在輸入框裏面輸入點東西,你應該可以看到下面的內容:

當你點擊提交按鈕之後,新的待辦事項會加入到現有的 todoList 列表中,你應該可以看到下面的內容:

恭喜你!你成功使用 React 完成了一個簡單的待辦事項應用,它可以完成如下的功能:
•異步獲取將要展示的待辦事項:todoList•將待辦事項展示出來•偶數項待辦事項將會展示成紅色•可以添加新的待辦事項
做得好!我們希望你現在已經對 React 的運行機制有了一個比較完整的了解,也希望本篇教程能夠為你踏入 React 開發世界提供一個好的開始!感謝你的閱讀!
後記
受限於篇幅,我們的待辦事項還不完整,如果你有額外的時間或者你想要練習你新學到的 React 知識,下面是一些使我們的待辦事項變得完整的一些想法,我們將按實現難度給這些功能排序:
•在添加新的待辦事項之後,清空輸入框裏面的內容,方便下一次輸入。這樣涉及到 React 受控組件[15]的知識。•允許對單個事項進行刪除。這涉及到子組件修改父組件的狀態[16]知識。•允許用戶對單個事項進行修改。•允許用戶對待辦事項進行搜索。
想要學習更多精彩的實戰技術教程?來圖雀社區[17]逛逛吧。
References
[1]
GitHub 倉庫: [https://github.com/pftom/react-quickstart-tutorial [2]
React 前端工程師學習路線: https://github.com/tuture-dev/react-roadmap [3]
最終結果: https://codesandbox.io/s/currying-grass-rglst [4]
這篇指南: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/A_re-introduction_to_JavaScript [5]
arrow functions: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions [6]
classes: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes [7]
const: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/const [8]
Babel REPL: https://www.babeljs.cn/repl#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYewdgzgLgBApgGzgWzmWBeGAeAFgRgD4AJRBEAGhgHcQAnBAEwEJsB6AwgbgChRJY_KAEMAlmDh0YWRiGABXVOgB0AczhQAokiVQAQgE8AkowAUAcjogQUcwEpeAJTjDgUACIB5ALLK6aRklTRBQ0KCohMQk6Bx4gA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react&prettier=false&targets=&version=7.7.3 [9]
Node 官方網站: https://nodejs.org/zh-cn/ [10]
Create React App: https://create-react-app.dev/ [11]
Babel: https://www.babeljs.cn/ [12]
「Babel」 : https://www.babeljs.cn/repl#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=MYewdgzgLgBApgGzgWzmWBeGAeAFgRgD4AJRBEAGhgHUQAnBAE2wHoDCBuAKFEll6gBDAJZg4dGFkYhgAV1ToAdAHM4UAKJIFUAEIBPAJKMAFAHI6IEFFMBKbgCU4g4FAAiAeQCyiumkbjjRBQ0KCoBETE6Oy4gA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react&prettier=false&targets=&version=7.7.3 [13]
駝峰式命名: https://baike.baidu.com/item/%E9%AA%86%E9%A9%BC%E5%91%BD%E5%90%8D%E6%B3%95 [14]
點擊查看: http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/ [15]
React 受控組件: https://zh-hans.reactjs.org/docs/forms.html#controlled-components [16]
修改父組件的狀態: https://zh-hans.reactjs.org/docs/lifting-state-up.html [17]
圖雀社區: https://tuture.co