一. 使用React編寫todolist
1. 佔位符Fragment;
先從下面圖片開始瞭解本節的目錄結構、代碼、效果:
目錄結構:
代碼:
index.js
TodoList.js
效果:
上圖中紅色方框中的dom結構是用一個div包裹的,但是有時候我們在實現flex佈局的時候該div包裹會影響我們的樣式。React提供了一個佔位符Fragment。我們引入該佔位符,同時將最外層的div包裹替換成Fragment即可。
修改後的代碼:
TodoList.js
效果:
二. React中的響應式設計思想和事件綁定
如果用原生js或jquery來做todolist的話,我們一般是獲取輸入框的value值,然後通過document.getElementById獲取列表區域,然後將value值掛載到列表區域。
React與上面直接操作dom的形式不同。它是一個響應式的框架,通過操作數據,React會感知到數據的變化,自動地幫你去生成dom。所以寫React項目的時候,我們只需要關注數據層的變化。
1. Constructor
在js中,一個對象或者類就有一個constructor構造函數。在React中,當我們去創建或者使用TodoList這個組件的時候,constructor這個構造函數時優於別的任何函數的,自動的最先被執行的一個函數。所以我們可以在該構造函數裏定義數據。
2. 如何雙向綁定
以todolist的input框的value值爲例,我們將它和todolist狀態中的inputValue進行綁定。
<input value={this.state.inputValue}/>
想在JSX語法中使用js的變量(如this.state.inputValue),我們需要用大括號將其括起來。
3. React事件綁定
以監聽input框的change事件爲例,原生js是onchange, 在React中應該寫成onChange。
TodoList.js:
import React, { Component, Fragment } from 'react';
class TodoList extends Component {
constructor(props) {
//固定寫法,意思是TodoList是繼承自React的component這個組件,我們這要調用一次父類的構造函數
super(props);
//React的數據需要定義在狀態裏面,下面的state就是表示組件的狀態
this.state = {
inputValue: 'hahah', //input框的value值
list: [] //數組中的每一項
}
}
render() {
return ( //jsx語法要求render函數返回的內容外層必須用一個標籤包裹
<Fragment>
<div>
<input
value={this.state.inputValue}
onChange={this.handleInputChange}
/>
<button>提交</button>
</div>
<ul>
<li>學英語</li>
<li>學React</li>
</ul>
</Fragment>
)
}
handleInputChange(e) {
console.log(e.target.value); //控制檯會打印出第一次修改後的value值,但是input框內容未改變
}
}
export default TodoList; //導出TodoList組件
按照常理,我們想input框的value跟着改變的話,加入如下代碼即可:
handleInputChange(e) {
this.state.inputValue = e.target.value; //Cannot read property 'state' of undefined
}
實際上,上面的this並不是指向TodoList這個組件,打印this結果是undefined。我們可以通過bind來更改this的指向。
<input
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
handleInputChange(e) {
this.state.inputValue = e.target.value; //此時this指向的是該組件,但是還是不能改變輸入框的值
}
//原因是React不能直接通過this.state來改變state裏面的值,它提供了一個方法:this.setState({})
最終解決辦法:
handleInputChange(e) {
this.setState({
inputValue: e.target.value
})
}
三. 實現TodoList新增刪除功能
import React, { Component, Fragment } from 'react';
class TodoList extends Component {
constructor(props) {
//固定寫法,意思是TodoList是繼承自React的component這個組件,我們這要調用一次父類的構造函數
super(props);
//React的數據需要定義在狀態裏面,下面的state就是表示組件的狀態
this.state = {
inputValue: '', //input框的value值
list: [] //數組中的每一項
}
}
render() {
return ( //jsx語法要求render函數返回的內容外層必須用一個標籤包裹
<Fragment>
<div>
<input
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
/>
<button onClick={this.handleBtnClick.bind(this)}>提交</button>
</div>
<ul>
{
this.state.list.map((item, index) => {
return (
<li
key={index}
onClick={this.handleItemDelete.bind(this, index)}
>{item}</li>
)
})
}
</ul>
</Fragment>
)
}
//輸入框value發生改變
handleInputChange(e) {
this.setState({
inputValue: e.target.value
})
}
//點擊提交將value加入列表項
handleBtnClick() {
this.setState({
//es6的展開運算符,將this.state.list該數組展開成參數列表的形式(如:[...[1, 2]] => [1, 2])
list: [...this.state.list, this.state.inputValue],
inputValue: ''
})
}
//點擊列表項刪除
handleItemDelete(index) {
//immutable, React不允許我們直接對state做任何修改
const list = this.state.list;
list.splice(index, 1);
this.setState({
list: list
})
}
}
export default TodoList; //導出TodoList組件
四、JSX語法細節補充
1、如何在JSX語法中編輯註釋。
我們只需要將註釋用花括號包裹即可。注意:單行註釋應該重起一行。
2、在React中引入外部樣式。
新建樣式文件style.css
在TodoList.js中引入
給input框添加類名
結果顯示樣式發生了改變,但是控制檯報錯:
這是因爲React爲了避免將input中的class與類相混淆,不推薦我們使用class來表示元素的類名,推薦我們使用className來代替。
將上面的class修改成className,樣式發生了改變且控制檯不報錯。
3、對輸入框輸出內容進行轉義。
對於上圖的結果,我們希望輸出結果中的h1被解析轉義出來。代碼需進行如下修改:
最終結果如下所示:
4、點擊標籤左邊文字使光標聚焦在輸入框。
在HTML中label標籤的作用是擴大輸入框的點擊區域。我們需要實現一個功能:點擊輸入內容,讓光標聚焦在輸入框。
添加如下代碼實現:
效果如下:
但是控制檯報錯:
原因和上面一樣,也是React爲了避免label標籤中的for屬性和js的for語句混淆,不推薦使用for來關聯input的id。使用htmlFor來替換for。
效果一樣,同時控制檯也不報錯。
五. 拆分組件與組件之間的傳值
1. 將TodoList這個組件拆分成兩個組件
首先在src目錄下創建一個TodoItem.js組件
輸入內容如下:
在TodoList中引入該組件:
修改TodoList中的JSX:
注意:應該用一個元素包裹TodoItem與註釋,否則會報錯。
結果界面:
顯然這不是我們想要的結果,這是因爲我們將TodoItem裏面內容寫死了。要實現我們的功能,我們需要父組件向子組件傳值
2. 父組件向子組件傳值
通過屬性的方式進行傳值,子組件通過this.props接收。具體例子如下:
通過屬性傳值:
通過this.props接收:
結果界面:
3. 子組件操作父組件的數據
接下來實現點擊每個列表項自動刪除的功能:
首先子組件需要獲取被點擊那一項的index值:
接下來我們需要將該值傳遞給父組件的handleItemDelete方法,刪除數組中對應index的那一項。
父組件:
子組件:
此時,當我們點擊列表項的時候報錯
原因是因爲我們子組件中調用this.props.deleteItem方法實際上就是調用父組件的this.handleItemDelete方法,但是這裏的this指向是子組件,我們需要強制改變this的指向,使它指向父組件。
最後功能成功實現了,也沒有報錯。
六. TodoList代碼優化
1. TodoItem.js代碼優化
利用ES6的解構賦值對代碼優化:
優化前:
優化後:
2. TodoList.js代碼優化
主要優化部分:
1. 在constructor部分改變this的指向
2.將JSX語法中的邏輯部分用一個方法來表示
3.使用新版React推薦的setState語法(不直接返回一個對象,而是返回一個函數,函數內再return一個對象)
4.解決使用循環帶來的key值問題
優化前:
import React, { Component, Fragment } from 'react';
import './style.css';
import TodoItem from './TodoItem';
class TodoList extends Component {
constructor(props) {
//固定寫法,意思是TodoList是繼承自React的component這個組件,我們這要調用一次父類的構造函數
super(props);
//React的數據需要定義在狀態裏面,下面的state就是表示組件的狀態
this.state = {
inputValue: '', //input框的value值
list: [] //數組中的每一項
}
}
render() {
return (
<Fragment>
<div>
<label htmlFor="insert">輸入內容</label>
<input
id="insert"
value={this.state.inputValue}
onChange={this.handleInputChange.bind(this)}
className='input'
/>
<button onClick={this.handleBtnClick.bind(this)}>提交</button>
</div>
<ul>
{
this.state.list.map((item, index) => {
return (
<div>
<TodoItem
content={item}
index={index}
deleteItem={this.handleItemDelete.bind(this)}
/>
</div>
)
})
}
</ul>
</Fragment>
)
}
//輸入框value發生改變
handleInputChange(e) {
this.setState({
inputValue: e.target.value
})
}
//點擊提交將value加入列表項
handleBtnClick() {
this.setState({
//es6的展開運算符,將this.state.list該數組展開成參數列表的形式(如:[...[1, 2]] => [1, 2])
list: [...this.state.list, this.state.inputValue],
inputValue: ''
})
}
//點擊列表項刪除
handleItemDelete(index) {
//immutable, React不允許我們直接對state做任何修改
const list = this.state.list;
list.splice(index, 1);
this.setState({
list: list
})
}
}
export default TodoList; //導出TodoList組件
優化後:
import React, { Component, Fragment } from 'react';
import './style.css';
import TodoItem from './TodoItem';
class TodoList extends Component {
constructor(props) {
//固定寫法,意思是TodoList是繼承自React的component這個組件,我們這要調用一次父類的構造函數
super(props);
//React的數據需要定義在狀態裏面,下面的state就是表示組件的狀態
this.state = {
inputValue: '', //input框的value值
list: [] //數組中的每一項
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
}
render() {
return (
<Fragment>
<div>
<label htmlFor="insert">輸入內容</label>
<input
id="insert"
value={this.state.inputValue}
onChange={this.handleInputChange}
className='input'
/>
<button onClick={this.handleBtnClick}>提交</button>
</div>
<ul>
{this.getTodoItem()}
</ul>
</Fragment>
)
}
//輸入框value發生改變
handleInputChange(e) {
const value = e.target.value;
this.setState(() => ({
inputValue: value
}));
}
//點擊提交將value加入列表項
handleBtnClick() {
this.setState((prevState) => ({
//prevState指的是修改數據之前那次數據,該寫法可以避免我們有時候不小心改變了state的狀態
list: [...prevState.list, prevState.inputValue],
inputValue: ''
}));
}
//遍歷list,顯示每一個item
getTodoItem() {
return this.state.list.map((item, index) => {
return (
<TodoItem
content={item}
index={index}
key={index}
deleteItem={this.handleItemDelete}
/>
)
})
}
//點擊列表項刪除
handleItemDelete(index) {
this.setState((prevState) => {
const list = prevState.list;
list.splice(index, 1);
return {list};
})
}
}
export default TodoList; //導出TodoList組件
七. 圍繞React衍生出的一些思考
1. 命令式開發、聲明式開發
命令式開發: 我們之前使用的jquery開發都是直接操作dom,我們把這種方式稱爲命令式開發。就是我們開發的時候,需要告訴頁面,你要如何去獲取,接着又如何去掛載,這種命令式的方式。
聲明式開發:React就是聲明式的開發,比如我們要開發一個網頁,之前命令式的開發需要一步一步地去指導如何去開發。而React不是這樣的,它是面向數據來編程的,我們只需要把數據構建出就可以,React會自動根據數據去構建這個網頁。數據就類似一個圖紙,React根據圖紙會自動地幫你去構建這棟房子。
2.可以與其它框架並存
在React項目的入口文件,我們將TodoList這個組件掛載到id等於root的元素下面,我們開發過程中,第三方框架在id等於root的元素之外使用是不會影響到React的,同樣React也不會影響其他的框架。
3. 組件化
4. 單向數據流
父組件可以傳數據給子組件,但是不允許子組件直接改變傳遞過來的數據。這是因爲當該數據被多個組件使用的時候,我們子組件改變了該數據,別的組件使用該數據時也被改變了,而且在出現bug時不容易定位。
5.視圖層的框架
因爲React在開發大型項目的時候,組件樹比較複雜,單靠React父子組件傳值很麻煩,我們需要引用別的像Redux這樣的數據層的框架來輔助我們開發。React只負責解決頁面與數據渲染上的一些問題,至於組件之間如何傳值,它並不負責。對於Todolist這種小型的項目,藉助React內部的傳值機制就可以,但是大中型項目,這時遠遠不夠的。
6. 函數式編程
我們在React文件中可以看到,React開發都是一個函數一個函數地去編寫。這帶來了幾個好處:
1. 維護起來容易。函數比較大地時候可以進行拆分,每個函數各司其職。
2. 方便自動化測試。我們只需要給函數一個輸入值,看輸出值是否正確,給前端自動化測試帶來了很大地便捷性。