今日目标
✔ 掌握组件创建的两种方式。
✔ 掌握定义和操作状态的方法。
✔ 掌握事件绑定以及 this 指向的问题。
✔ 掌握表单处理的两种方式。
✔ 完成带交互的 B 站评论列表案例。
组件介绍
目标
了解 React 组件的概念、特点、分类。
概念
组件就是页面中的一部分,是 React 的一等公民,使用 React 就是在用组件,而所谓的组件化开发就是采用分而治之的思想来管理繁杂的页面逻辑。
🧐 了解模块:JS 模块一般是向外提供特定功能的代码片段,通常来说是一个 JS 文件。
特点
独立、可复用、可组合。
分类
小结
组件是什么?
组件的特点是什么?
组件的分类是什么?
函数式组件
目标
内容
1 2 3 4 5 6 7 8 9
| import ReactDOM from 'react-dom'
function Hello() { return <div>这是第一个函数组件</div> }
ReactDOM.render(<Hello />, document.getElementById('root'))
|
- 🧐 React 解析
<Hello/>
标签,发现是大写开头的会被当做组件进行解析,解析的时候又发现其是一个函数式组件,随后会调用此函数,将返回的虚拟 DOM 转为真实 DOM,并渲染到页面中。
小结
类组件
目标
内容
a,使用 ES6 语法的 class 创建的组件,又称复杂组件或有状态组件。
b,类名称也必须要大写字母开头。
c,类组件应该继承 React.Component
父类,从而可以使用父类中提供的方法或者属性。
d,类组件必须提供 render()
方法,此方法中的 this 指向此组件的实例对象,此方法中必须要有 return 返回值。
1 2 3 4 5 6
| class Hello extends React.Component { render() { return <div>这是第一个类组件</div> } } ReactDOM.render(<Hello />, document.getElementById('root'))
|
🧐 了解 ReactDOM.render()
解析类式组件的过程:React 解析 <Hello/>
标签,发现是大写开头的会被当做组件进行解析,解析的时候又发现其是一个类组件,会自动的 new 出来该类的实例,并通过实例调用原型上的 render()
方法,将 render()
方法返回的虚拟 DOM 转为真实 DOM,并渲染到页面中。
小结
class 组件的格式是:class 类名 ___ from ____
?
类组件的名称有什么要求?
类组件的内部必须提供 ____
方法?
ReactDOM.render()
解析类组件的过程是什么?
提取组件
目标
能够将 React 组件提取到独立的 JS 文件中。
内容
思考:项目中的组件多了之后,该如何组织这些组件呢?
选择 1:将所有组件放在同一个 JS 文件中。
选择 2:将每个组件放到单独的 JS 文件中。
实现
创建 App.js,创建组件(函数 或 类)。
在 App.js 中通过 export default 默认导出该组件。
在 index.js 中通过 import 默认导入 App 组件。
渲染组件。
代码
App.jsx
1 2 3 4 5 6 7
| import React, { Component } from 'react'
export default class App extends Component { render() { return <div>Hello World</div> } }
|
index.js
1 2 3 4
| import ReactDOM from 'react-dom' import App from './App'
ReactDOM.render(<App />, document.querySelector('#root'))
|
小结
开发者工具
极简插件
组件的状态
目标
了解 React 中状态的概念、特点和作用。
了解什么是有状态组件和无状态组件。
关于状态
概念
状态就是用来描述事物在某一时刻的的数据,例如:9 月 23 号时书的库存数量;18 岁时人的身高等。
特点
状态能被改变,改变了之后视图会有对应的变化。
作用
保存数据。
数据变化响应到视图(React 包内部的操作)。
有状态/无状态组件
🧐 2019 年 02 月 06 日,React v16.8 中引入了 React Hooks,从而函数式组件也能定义自己的状态了。
无状态组件的应用场景
小结
状态是什么?
React 中状态的特点是什么?
函数组件是__组件,类组件是__组件。
类组件的状态
目标
掌握 React 类组件中如何定义和使用状态。
定义
第一种方式:在 constructor 中通过 this.state = {}
。
1 2 3 4 5 6 7 8 9 10 11
| class App extends React.Component { constructor() { super() this.state = { list: [ { id: 1, name: '明天会更好' }, { id: 2, name: '今天' }, ], } } }
|
第二种方式:通过 state 来定义状态,state 对应的值必须是一个对象。
1 2 3 4 5 6 7 8 9
| class App extends React.Component { state = { list: [ { id: 1, name: '明天会更好' }, { id: 2, name: '今天' }, ], } }
|
🧐 思考两种方式的差异?
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class App extends React.Component { render() { return ( <> <h1>歌曲数量:{this.state.list.length}</h1> <ul> {this.state.list.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> </> ) } }
|
小结
定义组件的状态有哪两种方式?
this.state
对应的值必须是一个什么?
事件绑定
目标
掌握 React 中如何进行事件绑定。
掌握 React 中如何获取事件对象。
语法
1
| <元素 事件名1={事件处理函数1} 事件名2={事件处理函数2}></元素>
|
注意:React 事件名采用驼峰命名法,比如 onClick、onMouseEnter 等。
类组件中事件绑定
需求:点击按钮控制台打印 ‘Hello World’。
1 2 3 4 5 6 7 8 9 10 11 12 13
| class App extends React.Component { render() { return ( <button onClick={() => { console.log('Hello World') }} > click </button> ) } }
|
也可以把事件处理函数进行提取。
1 2 3 4 5 6 7 8
| class App extends React.Component { handleClick() { console.log('Hello World') } render() { return <button onClick={this.handleClick}>click</button> } }
|
函数式组件中的事件绑定
1 2 3 4 5 6
| const App = () => { const handleClick = () => { console.log('Hello World') } return <button onClick={handleClick}>click</button> }
|
获得事件对象
通过形参 e
可以拿到事件对象,例如 e.target
就是触发事件的那个 DOM 元素。
小结
点击计数
需求:点击按钮让数字在原来数字的基础上进行 +1。
目标
了解事件处理函数中的 this 指向问题。
实现
📝 需求:计数器(点击按钮加 1)
- 定义 State
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React, { Component } from 'react'
export default class App extends Component { state = { count: 0, } render() { return ( <div> <h2>计数器:{this.state.count}</h2> </div> ) } }
|
- 绑定点击事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class App extends Component { state = { count: 0, } handleClick() { console.log(this.state.count) } render() { return ( <div> <h2>计数器:{this.state.count}</h2> <button onClick={this.handleClick}>+1</button> </div> ) } }
|
🧐 注意:this.handleClick 不要加括号,加括号表示立即调用,期望的是点击按钮的时候才调用。
- 分析原因
render 函数是被组件实例调用的(可以通过修改 render 函数的名字来观察到),因此 render 函数中的 this 指向当前组件实例,所以在 render 函数中通过 this 实例访问 state 和 handleClick 没有问题。
但!<button onClick={this.handleClick}>+1</button>
,这样写,本质上是把 this.handleClick
这个方法赋值给了 onClick 这个属性,当点击按钮的时候,由 React 内部直接调用 onClick,那么 this 指向就是 undefined(class 的内部,开启了局部严格模式,所以 this 不会指向 window )。
小结
render 函数中的 this 指向是什么?
解决 this 指向问题
目标
掌握常见的 this 指向问题的解决方案。
方法 1
高阶函数:通过 this 来直接调用 handleClick 并返回箭头函数。
🧐 柯里化:通过函数调用继续返回函数的形式,实现多次接收参数最后统一处理的函数编码形式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class App extends React.Component { state = { count: 0, } handleClick() { return () => { console.log(this.state.count) } } render() { return ( <div> <h2>计数器:{this.state.count}</h2> <button onClick={this.handleClick()}>+1</button> </div> ) } }
|
方法 2
包裹一层箭头函数。
箭头函数中的 this 指向“外部”,即 render 函数,而 render 函数中的 this 正是组件实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class App extends Component { state = { count: 0, } handleClick() { console.log(this.state.count) } render() { return ( <div> <h2>计数器:{this.state.count}</h2> <button onClick={() => this.handleClick()}>+1</button> </div> ) } }
|
方法 3
使用 bind。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class App extends Component { state = { count: 0, } handleClick() { console.log(this.state.count) } render() { return ( <div> <h2>计数器:{this.state.count}</h2> <button onClick={this.handleClick.bind(this)}>+1</button> </div> ) } }
|
扩展
🤔 关于 class 中的实例方法和原型方法?
原型方法演示
1 2 3 4 5 6 7 8 9 10
| class App { handleClick() {} }
const app1 = new App() const app2 = new App()
console.log(app1)
console.log(app1.handleClick === app2.handleClick)
|
实例方法演示
1 2 3 4 5 6 7 8 9 10
| class App { handleClick = () => {} }
const app1 = new App() const app2 = new App()
console.log(app1)
console.log(app1.handleClick === app2.handleClick)
|
所以,要明白在 class 中直接写的方法和通过赋值语句添加的方法本质上不一样。
注意:在 constructor 中挂载的方法也是实例方法。
方法 4
通过赋值语句往实例上面添加一个箭头函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class App extends Component { state = { count: 0, } handleClick = () => { console.log(this.state.count) } render() { return ( <div> <h2>计数器:{this.state.count}</h2> <button onClick={this.handleClick}>+1</button> </div> ) } }
|
证明“外层” this 确实是组件实例
1 2 3 4 5 6
| class App { temp = this }
const app = new App() console.log(app === app.temp)
|
方法 5
在构造函数中再创建一个实例方法,和原型方法公用一个函数体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class App extends React.Component { constructor() { super() this.state = { count: 0, } this.handleClick = this.handleClick.bind(this) } handleClick() { console.log(this.state.count) } render() { return ( <div> <h2>计数器:{this.state.count}</h2> <button onClick={this.handleClick}>+1</button> </div> ) } }
|
小结
你最喜欢哪一种?为什么
修改状态
目标
掌握通过 setState 修改状态的写法。
错误写法
内容
语法:this.setState({ 要修改的部分数据 })
。
作用:修改 state 并更新视图。
来源:setState()
函数是通过继承而来的。
注意:setState()
的操作是合并,不会影响没有操作到的数据。
1
| this.setState({ count: this.state.count + 1 })
|
小结
通过哪个方法来修改 state 中的数据?
状态的不可变性
目标
了解 React 的核心理念,状态的不可变性。
解释
也就是说不要直接修改原数据,而是要产生一份新数据,然后通过 setState()
用新的数据覆盖原数据,这么做的其中一个重要原因就是为了 SCU(shouldComponentUpdate),为了性能优化。
不建议写法
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
| import React, { Component } from 'react' import ReactDOM from 'react-dom'
export default class App extends Component { state = { count: 0, list: [1, 2, 3], person: { name: 'jack', age: 18, }, } changeCount = () => { this.state.count++ this.setState({ count: this.state.count, }) } changeList = () => { this.state.list.push('Hello React') this.setState({ list: this.state.list, }) } changePerson = () => { this.state.person.name = 'ifer' this.state.person.age = 19 this.setState({ person: this.state.person, }) } render() { return ( <div> <h3>count</h3> <p>{this.state.count}</p> <button onClick={this.changeCount}>click</button> <hr /> <h3>list</h3> <ul> {this.state.list.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> <button onClick={this.changeList}>click</button> <hr /> <h3>person</h3> <p> {this.state.person.name} {this.state.person.age} </p> <button onClick={this.changePerson}>click</button> </div> ) } }
ReactDOM.render(<App />, document.querySelector('#root'))
|
建议写法
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
| import React, { Component } from 'react' import ReactDOM from 'react-dom'
export default class App extends Component { state = { count: 0, list: [1, 2, 3], person: { name: 'jack', age: 18, }, } changeCount = () => { this.setState({ count: this.state.count + 1, }) } changeList = () => { this.setState({ list: [...this.state.list, 'Hello React'], }) } changePerson = () => { this.setState({ person: { ...this.state.person, name: 'ifer', age: 19, }, }) } render() { return ( <div> <h3>count</h3> <p>{this.state.count}</p> <button onClick={this.changeCount}>click</button> <hr /> <h3>list</h3> <ul> {this.state.list.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> <button onClick={this.changeList}>click</button> <hr /> <h3>person</h3> <p> {this.state.person.name} {this.state.person.age} </p> <button onClick={this.changePerson}>click</button> </div> ) } }
ReactDOM.render(<App />, document.querySelector('#root'))
|
受控表单组件
目标
能够使用受控组件的方式收集到表单中的数据。
概念
受控不受控一般是针对表单来说的,所谓受控表单组件,即表单元素的 value 值受到了 React 中状态(state)的控制(对状态的操作会影响视图,视图的变化又会反映到状态上)。
在 state 中添加一个状态,作为表单元素的 value 值(数据影响视图)。
给表单元素绑定 onChange 事件,将表单元素的值设置为 state 的值(视图影响数据)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React from 'react'
export default class App extends React.Component { state = { username: '', } changeText = (e) => { this.setState({ username: e.target.value, }) } render() { const { username } = this.state return ( <ul> <li> <label htmlFor='username'>用户名</label> <input id='username' type='text' value={username} onChange={this.changeText} /> </li> </ul> ) } }
|
textarea
操作方式和 input 框一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import React from 'react'
export default class App extends React.Component { state = { content: '', } changeTextArea = (e) => { this.setState({ content: e.target.value, }) } render() { const { content } = this.state return ( <ul> {/* ... */} <li> <label htmlFor='content'>其他信息</label> <textarea id='content' cols='30' rows='10' value={content} onChange={this.changeTextArea}></textarea> </li> </ul> ) } }
|
select
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
| import React from 'react'
export default class App extends React.Component { state = { frame: 'react', } changeOption = (e) => { this.setState({ frame: e.target.value, }) } render() { const { frame } = this.state return ( <ul> {/* ... */} <li> <label htmlFor='frame'>框架</label> <select id='frame' value={frame} onChange={this.changeOption}> <option value='vue'>Vue</option> <option value='react'>React</option> <option value='angular'>Angular</option> </select> </li> </ul> ) } }
|
radio
多个单选按钮,绑定的值可以是一个字符串。
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
| export default class App extends React.Component { state = { checkedRadio: 'male', } changeRadio = (e) => { this.setState({ checkedRadio: e.target.value, }) } render() { const { checkedRadio } = this.state return ( <ul> {/* ... */} <li> <input id='male' type='radio' value='male' checked={checkedRadio === 'male'} onChange={this.changeRadio} /> <label htmlFor='male'>男</label> <input id='female' type='radio' value='female' checked={checkedRadio === 'female'} onChange={this.changeRadio} /> <label htmlFor='female'>女</label> <input id='unknow' type='radio' value='unknow' checked={checkedRadio === 'unknow'} onChange={this.changeRadio} /> <label htmlFor='unknow'>未知</label> </li> </ul> ) } }
|
checkbox
绑定的值可以是一个数组。
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 81 82
| import React from 'react'
export default class App extends React.Component { state = { username: '', content: '', frame: 'react', checkedRadio: 'male', checkedFruit: ['apple'], } changeText = (e) => { this.setState({ username: e.target.value, }) } changeTextArea = (e) => { this.setState({ content: e.target.value, }) } changeOption = (e) => { this.setState({ frame: e.target.value, }) } changeRadio = (e) => { this.setState({ checkedRadio: e.target.value, }) } changeCheckBox = (e) => { const checkedFruit = [...this.state.checkedFruit] const idx = checkedFruit.indexOf(e.target.value) if (idx === -1) { checkedFruit.push(e.target.value) } else { checkedFruit.splice(idx, 1) } this.setState({ checkedFruit, }) } render() { const { username, content, frame, checkedRadio, checkedFruit } = this.state return ( <ul> <li> <label htmlFor='username'>用户名</label> <input id='username' type='text' value={username} onChange={this.changeText} /> </li> <li> <label htmlFor='content'>其他信息</label> <textarea id='content' cols='30' rows='10' value={content} onChange={this.changeTextArea}></textarea> </li> <li> <label htmlFor='frame'>框架</label> <select id='frame' value={frame} onChange={this.changeOption}> <option value='vue'>Vue</option> <option value='react'>React</option> <option value='angular'>Angular</option> </select> </li> <li> <input id='male' type='radio' value='male' checked={checkedRadio === 'male'} onChange={this.changeRadio} /> <label htmlFor='male'>男</label> <input id='female' type='radio' value='female' checked={checkedRadio === 'female'} onChange={this.changeRadio} /> <label htmlFor='female'>女</label> <input id='unknow' type='radio' value='unknow' checked={checkedRadio === 'unknow'} onChange={this.changeRadio} /> <label htmlFor='unknow'>未知</label> </li> <li> <input id='apple' type='checkbox' value='apple' checked={checkedFruit.includes('apple')} onChange={this.changeCheckBox} /> <label htmlFor='apple'>Apple</label> <input id='orange' type='checkbox' value='orange' checked={checkedFruit.includes('orange')} onChange={this.changeCheckBox} /> <label htmlFor='orange'>Orange</label> </li> </ul> ) } }
|
完整代码
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 81 82
| import React from 'react'
export default class App extends React.Component { state = { username: '', content: '', frame: 'react', checkedRadio: 'male', checkedFruit: ['apple'], } changeText = (e) => { this.setState({ username: e.target.value, }) } changeTextArea = (e) => { this.setState({ content: e.target.value, }) } changeOption = (e) => { this.setState({ frame: e.target.value, }) } changeRadio = (e) => { this.setState({ checkedRadio: e.target.value, }) } changeCheckBox = (e) => { const checkedFruit = [...this.state.checkedFruit] const idx = checkedFruit.indexOf(e.target.value) if (idx === -1) { checkedFruit.push(e.target.value) } else { checkedFruit.splice(idx, 1) } this.setState({ checkedFruit, }) } render() { const { username, content, frame, checkedRadio, checkedFruit } = this.state return ( <ul> <li> <label htmlFor='username'>用户名</label> <input id='username' type='text' value={username} onChange={this.changeText} /> </li> <li> <label htmlFor='content'>其他信息</label> <textarea id='content' cols='30' rows='10' value={content} onChange={this.changeTextArea}></textarea> </li> <li> <label htmlFor='frame'>框架</label> <select id='frame' value={frame} onChange={this.changeOption}> <option value='vue'>Vue</option> <option value='react'>React</option> <option value='angular'>Angular</option> </select> </li> <li> <input id='male' type='radio' value='male' checked={checkedRadio === 'male'} onChange={this.changeRadio} /> <label htmlFor='male'>男</label> <input id='female' type='radio' value='female' checked={checkedRadio === 'female'} onChange={this.changeRadio} /> <label htmlFor='female'>女</label> <input id='unknow' type='radio' value='unknow' checked={checkedRadio === 'unknow'} onChange={this.changeRadio} /> <label htmlFor='unknow'>未知</label> </li> <li> <input id='apple' type='checkbox' value='apple' checked={checkedFruit.includes('apple')} onChange={this.changeCheckBox} /> <label htmlFor='apple'>Apple</label> <input id='orange' type='checkbox' value='orange' checked={checkedFruit.includes('orange')} onChange={this.changeCheckBox} /> <label htmlFor='orange'>Orange</label> </li> </ul> ) } }
|
简化代码
添加 name。
把 key 都替换成 e.target.name
。
观察规律,提取成一个 handleChange 函数。
根据 e.target.type
做出判断。
继续精简。
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
| import React from 'react'
export default class App extends React.Component { state = { username: '', content: '', frame: 'react', checkedRadio: 'male', checkedFruit: ['apple'], } handleChange = (e) => { let v if (e.target.type === 'checkbox') { const checkedFruit = [...this.state.checkedFruit] const idx = checkedFruit.indexOf(e.target.value) idx === -1 ? checkedFruit.push(e.target.value) : checkedFruit.splice(idx, 1) v = checkedFruit } else { v = e.target.value } this.setState({ [e.target.name]: v, }) } render() { const { username, content, frame, checkedRadio, checkedFruit } = this.state return ( <ul> <li> <label htmlFor='username'>用户名</label> <input id='username' name='username' type='text' value={username} onChange={this.handleChange} /> </li> <li> <label htmlFor='content'>其他信息</label> <textarea id='content' name='content' cols='30' rows='10' value={content} onChange={this.handleChange}></textarea> </li> <li> <label htmlFor='frame'>框架</label> <select id='frame' name='frame' value={frame} onChange={this.handleChange}> <option value='vue'>Vue</option> <option value='react'>React</option> <option value='angular'>Angular</option> </select> </li> <li> <input id='male' name='checkedRadio' type='radio' value='male' checked={checkedRadio === 'male'} onChange={this.handleChange} /> <label htmlFor='male'>男</label> <input id='female' name='checkedRadio' type='radio' value='female' checked={checkedRadio === 'female'} onChange={this.handleChange} /> <label htmlFor='female'>女</label> <input id='unknow' name='checkedRadio' type='radio' value='unknow' checked={checkedRadio === 'unknow'} onChange={this.handleChange} /> <label htmlFor='unknow'>未知</label> </li> <li> <input id='apple' name='checkedFruit' type='checkbox' value='apple' checked={checkedFruit.includes('apple')} onChange={this.handleChange} /> <label htmlFor='apple'>Apple</label> <input id='orange' name='checkedFruit' type='checkbox' value='orange' checked={checkedFruit.includes('orange')} onChange={this.handleChange} /> <label htmlFor='orange'>Orange</label> </li> </ul> ) } }
|
非受控表单组件
概念
非受控组件则是通过操作 DOM 的方式来获取数据,表单中的 value 并没有和 state 中的数据进行绑定。
内容
通过 React.createRef() 获取 DOM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import React, { Component } from 'react'
export default class App extends Component { input = React.createRef() handleChange = () => { console.log(this.input.current.value) } render() { return ( <div> {/* Step2 */} <input ref={this.input} type='text' placeholder='输入内容' onChange={this.handleChange} /> </div> ) } }
|
综合案例 📝
目标
完成带交互的 B 站评论列表案例。
整合数据和视图
把昨天 B 站评论列表的最终代码改造成一个 class 组件(不要忘了继承自 React.Component
)的形式来渲染。
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| import React from 'react' import ReactDOM from 'react-dom' import './index.css' import avatar from './images/avatar.png'
class App extends React.Component { state = { tabs: [ { id: 1, name: '热度', type: 'hot', }, { id: 2, name: '时间', type: 'time', }, ], active: 'time', list: [ { id: 1, author: '刘德华', comment: '给我一杯忘情水', time: '2021-10-10 09:09:00', img: 'https://y.qq.com/music/photo_new/T001R300x300M000003aQYLo2x8izP.jpg?max_age=2592000', attitude: 1, }, { id: 2, author: '周杰伦', comment: '听妈妈的话', time: '2021-10-11 09:09:00', img: 'https://y.qq.com/music/photo_new/T001R500x500M0000025NhlN2yWrP4.jpg?max_age=2592000', attitude: 0, }, { id: 3, author: '陈奕迅', comment: '十年', time: '2021-10-11 10:09:00', img: 'https://y.qq.com/music/photo_new/T001R500x500M000003Nz2So3XXYek.jpg?max_age=2592000', attitude: -1, }, ], } render() { const { state } = this return ( <div className='App'> <div className='comment-container'> <div className='comment-head'> <span>{state.list.length} 评论</span> </div> <div className='tabs-order'> <ul className='sort-container'> {state.tabs.map((item) => ( <li className={item.type === state.active ? 'on' : ''} key={item.id}> 按{item.name}排序 </li> ))} </ul> </div> <div className='comment-send'> <div className='user-face'> <img className='user-head' src={avatar} alt='' /> </div> <div className='textarea-container'> <textarea cols='80' rows='5' placeholder='发条友善的评论' className='ipt-txt'></textarea> <button className='comment-submit'>发表评论</button> </div> <div className='comment-emoji'> <i className='face'></i> <span className='text'>表情</span> </div> </div> <div className='comment-list'> {state.list.map((item) => ( <div className='list-item' key={item.id}> <div className='user-face'> <img className='user-head' src={item.img} alt='' /> </div> <div className='comment'> <div className='user'>{item.author}</div> <p className='text'>{item.comment}</p> <div className='info'> <span className='time'>{item.time}</span> <span className={item.attitude === 1 ? 'like liked' : 'like'}> <i className='icon'></i> </span> <span className={item.attitude === -1 ? 'hate hated' : 'hate'}> <i className='icon'></i> </span> <span className='reply btn-hover'>删除</span> </div> </div> </div> ))} </div> </div> </div> ) } }
ReactDOM.render(<App />, document.querySelector('#root'))
|
Tab 栏切换功能
- 给 Tab 栏注册点击事件。
1 2 3 4 5 6 7
| <ul className='sort-container'> {state.tabs.map((item) => ( <li className={item.type === state.active ? 'on' : ''} key={item.id} onClick={() => this.changeTab(item.type)}> 按{item.name}排序 </li> ))} </ul>
|
另外一种传递参数的写法(changeTab 的最后一个形参就是事件对象),官方文档。
1 2 3 4 5 6 7
| <ul className='sort-container'> {state.tabs.map((item) => ( <li className={item.type === state.active ? 'on' : ''} key={item.id} onClick={this.changeTab.bind(this, item.type)}> 按{item.name}排序 </li> ))} </ul>
|
- 修改 active 进行切换。
1 2 3 4 5
| changeTab(type) { this.setState({ active: type, }) }
|
删除评论
- 给删除按钮注册点击事件。
1 2 3
| <span className='reply btn-hover' onClick={() => this.delItem(item.id)}> 删除 </span>
|
- 通过 setState 删除对应的数据。
1 2 3 4 5
| delItem(id) { this.setState({ list: this.state.list.filter((item) => item.id !== id), }) }
|
- 暂无评论。
1 2 3 4 5 6 7 8 9 10
| class App extends React.Component { render() { const { state } = this return ( <div className='App'> <div className='comment-container'>{state.list.length > 0 ? <div className='comment-list'>{/* ... */}</div> : <div>暂无更多评论~</div>}</div> </div> ) } }
|
- 把三元判断抽离为
renderList
函数。
添加评论
- 通过受控组件的方式获取到评论内容。
1 2 3
| state = { content: '', }
|
1
| <textarea value={this.state.content} onChange={this.handleChange}></textarea>
|
1 2 3 4 5
| handleChange = (e) => { this.setState({ content: e.target.value, }) }
|
- 点击发表按钮在事件回调里面通过 setState 添加评论并重置输入的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13
| addItem = () => { const newComment = { id: Date.now(), author: '作者', comment: this.state.content, time: new Date(), attitude: 0, } this.setState({ list: [newComment, ...this.state.list], content: '', }) }
|
- 时间处理。
1 2 3 4 5 6 7 8 9 10 11 12
| import moment from 'moment'
class App extends React.Component { formatTime(time) { return moment(time).format('YYYY-MM-DD HH:mm:ss') } addItem = () => { const newComment = { time: this.formatTime(new Date()), } } }
|
点赞与踩
- 注册点击事件
1 2 3 4 5 6 7 8 9
| <div className='info'> <span className='time'>{item.time}</span> <span className={item.attitude === 1 ? 'like liked' : 'like'} onClick={() => this.changeAttitude(item.id, item.attitude === 1 ? 0 : 1)}> <i className='icon'></i> </span> <span className={item.attitude === -1 ? 'hate hated' : 'hate'} onClick={() => this.changeAttitude(item.id, item.attitude === -1 ? 0 : -1)}> <i className='icon'></i> </span> </div>
|
- 修改点赞状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| changeAttitude = (id, attitude) => { this.setState({ list: this.state.list.map((item) => { if (item.id === id) { return { ...item, attitude, } } else { return item } }), }) }
|
完整代码
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
| import React from 'react' import ReactDOM from 'react-dom' import moment from 'moment' import './index.css' import avatar from './images/avatar.png'
class App extends React.Component { state = { content: '', tabs: [ { id: 1, name: '热度', type: 'hot', }, { id: 2, name: '时间', type: 'time', }, ], active: 'time', list: [ { id: 1, author: '刘德华', comment: '给我一杯忘情水', time: '2021-10-10 09:09:00', img: 'https://y.qq.com/music/photo_new/T001R300x300M000003aQYLo2x8izP.jpg?max_age=2592000', attitude: 1, }, { id: 2, author: '周杰伦', comment: '听妈妈的话', time: '2021-10-11 09:09:00', img: 'https://y.qq.com/music/photo_new/T001R500x500M0000025NhlN2yWrP4.jpg?max_age=2592000', attitude: 0, }, { id: 3, author: '陈奕迅', comment: '十年', time: '2021-10-11 10:09:00', img: 'https://y.qq.com/music/photo_new/T001R500x500M000003Nz2So3XXYek.jpg?max_age=2592000', attitude: -1, }, ], } changeTab(type) { this.setState({ active: type, }) } delItem(id) { this.setState({ list: this.state.list.filter((item) => item.id !== id), }) } handleChange = (e) => { this.setState({ content: e.target.value, }) } formatTime(time) { return moment(time).format('YYYY-MM-DD HH:mm:ss') } addItem = () => { const newComment = { id: Date.now(), author: '作者', comment: this.state.content, time: this.formatTime(new Date()), attitude: 0, } this.setState({ list: [newComment, ...this.state.list], content: '', }) } changeAttitude = (id, attitude) => { this.setState({ list: this.state.list.map((item) => { if (item.id === id) { return { ...item, attitude, } } else { return item } }), }) } renderList() { return this.state.list.length > 0 ? ( <div className='comment-list'> {this.state.list.map((item) => ( <div className='list-item' key={item.id}> <div className='user-face'> <img className='user-head' src={item.img} alt='' /> </div> <div className='comment'> <div className='user'>{item.author}</div> <p className='text'>{item.comment}</p> <div className='info'> <span className='time'>{item.time}</span> <span className={item.attitude === 1 ? 'like liked' : 'like'} onClick={() => this.changeAttitude(item.id, item.attitude === 1 ? 0 : 1)}> <i className='icon'></i> </span> <span className={item.attitude === -1 ? 'hate hated' : 'hate'} onClick={() => this.changeAttitude(item.id, item.attitude === -1 ? 0 : -1)}> <i className='icon'></i> </span> <span className='reply btn-hover' onClick={() => this.delItem(item.id)}> 删除 </span> </div> </div> </div> ))} </div> ) : ( <div>暂无更多评论~</div> ) } render() { const { state } = this return ( <div className='App'> <div className='comment-container'> <div className='comment-head'> <span>{state.list.length} 评论</span> </div> <div className='tabs-order'> <ul className='sort-container'> {state.tabs.map((item) => ( <li className={item.type === state.active ? 'on' : ''} key={item.id} onClick={() => this.changeTab(item.type)}> 按{item.name}排序 </li> ))} </ul> </div> <div className='comment-send'> <div className='user-face'> <img className='user-head' src={avatar} alt='' /> </div> <div className='textarea-container'> <textarea cols='80' rows='5' placeholder='发条友善的评论' className='ipt-txt' value={this.state.content} onChange={this.handleChange}></textarea> <button className='comment-submit' onClick={this.addItem}> 发表评论 </button> </div> <div className='comment-emoji'> <i className='face'></i> <span className='text'>表情</span> </div> </div> {this.renderList()} </div> </div> ) } }
ReactDOM.render(<App />, document.querySelector('#root'))
|
留言案例 📝
需求分析
a,渲染评论列表(列表渲染)。
b,没有评论数据时渲染:暂无评论(条件渲染)。
c,获取评论信息,包括评论人和评论内容(受控组件)。
d,发表评论,更新评论列表(setState())。
界面准备
入口文件:index.js
1 2 3 4 5
| import ReactDOM from 'react-dom' import App from './App' import './index.css'
ReactDOM.render(<App />, document.getElementById('root'))
|
样式文件:index.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .app-top input { height: 29px; } .app-top textarea::-webkit-input-placeholder { line-height: 29px; } .app-top button { height: 35px; }
.app-top input, .app-top textarea, .app-top button { vertical-align: top; }
|
根组件:App.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import React from 'react'
export default class App extends React.Component { render() { return ( <div className='app'> <div class='app-top'> <input type='text' className='user' placeholder='请输入评论人' /> <textarea name='' id='' cols='30' rows='10' placeholder='请输入评论内容' /> <button>发表评论</button> </div> <div className='no-comment'>暂无评论,快去评论吧~</div> <ul> <li> <h3>评论人:jack</h3> <p>评论内容:沙发!!!</p> </li> </ul> </div> ) } }
|
全局样式:index.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| .app { width: 300px; padding: 10px; border: 1px solid #999; }
.user { width: 100%; box-sizing: border-box; margin-bottom: 10px; }
.content { width: 100%; box-sizing: border-box; margin-bottom: 10px; }
.no-comment { text-align: center; margin-top: 30px; }
|
渲染列表
在 state 中初始化评论列表数据。
使用数组的 map 方法遍历 state 中的列表数据。
给每一个被遍历的 li 元素添加 key 属性。
在 render 方法里的 ul 节点下嵌入表达式。
根组件: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
| import React from 'react'
export default class App extends React.Component { state = { comments: [ { id: 1, name: 'jack', content: '沙发!!!' }, { id: 2, name: 'rose', content: '板凳~' }, { id: 3, name: 'tom', content: '楼主好人' }, ], } render() { const { comments } = this.state return ( <div className='app'> <div> <input type='text' className='user' placeholder='请输入评论人' /> <br /> <textarea placeholder='请输入评论内容' /> <br /> <button>发表评论</button> </div> <div className='no-comment'>暂无评论,快去评论吧~</div> <ul> {comments.map((item) => ( <li key={item.id}> <h3>评论人:{item.name}</h3> <p>评论内容:{item.content}</p> </li> ))} </ul> </div> ) } }
|
暂无评论
判断列表数据的长度是否为 0。
如果为 0,则渲染暂无评论。
如果不为 0,那么渲染列表数据。
在 JSX 中大量写逻辑会导致很臃肿,所以我们可以把条件渲染的逻辑抽取成一个函数。
在 render 的 return 方法里面调用这个函数即可。
根组件: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
| import React from 'react'
export default class App extends React.Component { state = { comments: [ { id: 1, name: 'jack', content: '沙发!!!' }, { id: 2, name: 'rose', content: '板凳~' }, { id: 3, name: 'tom', content: '楼主好人' }, ], } renderList() { if (this.state.comments.length === 0) { return <div className='no-comment'>暂无评论,快去评论吧~</div> } return ( <ul> {this.state.comments.map((item) => ( <li key={item.id}> <h3>评论人:{item.name}</h3> <p>评论内容:{item.content}</p> </li> ))} </ul> ) } render() { return ( <div className='app'> <div> <input type='text' className='user' placeholder='请输入评论人' /> <br /> <textarea placeholder='请输入评论内容' /> <br /> <button>发表评论</button> </div> {this.renderList()} </div> ) } }
|
获取评论
通过受控组件来获取内容。
初始化用户名和用户内容的 state。
在结构中,把表单元素的 value 与 state 进行绑定,还需要绑定 name 属性和 onChange 属性。
在 handleChange 函数中利用 setState 来让数据保持一致。
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
| import React from 'react'
export default class App extends React.Component { state = { comments: [ { id: 1, name: 'jack', content: '沙发!!!' }, { id: 2, name: 'rose', content: '板凳~' }, { id: 3, name: 'tom', content: '楼主好人' }, ], name: '', content: '', } renderList() { const { comments } = this.state if (comments.length === 0) { return <div className='no-comment'>暂无评论,快去评论吧~</div> } return ( <ul> {comments.map((item) => ( <li key={item.id}> <h3>评论人:{item.name}</h3> <p>评论内容:{item.content}</p> </li> ))} </ul> ) } handleChange = (e) => { this.setState({ [e.target.name]: e.target.value, }) } render() { const { name, content } = this.state return ( <div className='app'> <div> <input type='text' name='name' className='user' placeholder='请输入评论人' value={name} onChange={this.handleChange} /> <br /> <textarea placeholder='请输入评论内容' name='content' value={content} onChange={this.handleChange} /> <br /> <button>发表评论</button> </div> {this.renderList()} </div> ) } }
|
发表评论
给按钮绑定点击事件。
在事件处理程序中,通过 state 获取评论信息。
将评论信息添加到 state 中,利用 setState 来更新页面。
添加评论前需要判断用户是否输入内容。
添加评论后,需要清空文本框用户输入的值。
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
| import React from 'react'
export default class App extends React.Component { state = { comments: [ { id: 1, name: 'jack', content: '沙发!!!' }, { id: 2, name: 'rose', content: '板凳~' }, { id: 3, name: 'tom', content: '楼主好人' }, ], name: '', content: '', } renderList() { const { comments } = this.state if (comments.length === 0) { return <div className='no-comment'>暂无评论,快去评论吧~</div> } return ( <ul> {comments.map((item) => ( <li key={item.id}> <h3>评论人:{item.name}</h3> <p>评论内容:{item.content}</p> </li> ))} </ul> ) } handleChange = (e) => { this.setState({ [e.target.name]: e.target.value, }) } handleSubmit = (e) => { const { name, content } = this.state if (name.trim() === '' || content.trim() === '') { alert('请输入内容') return } const newComments = [ { id: this.state.comments.length + 1, name, content, }, ...this.state.comments, ] this.setState({ comments: newComments, name: '', content: '', }) } render() { const { name, content } = this.state return ( <div className='app'> <div> <input type='text' name='name' className='user' placeholder='请输入评论人' value={name} onChange={this.handleChange} /> <br /> <textarea placeholder='请输入评论内容' name='content' value={content} onChange={this.handleChange} /> <br /> <button onClick={this.handleSubmit}>发表评论</button> </div> {this.renderList()} </div> ) } }
|
删除功能
a,利用 findIndex
并 splice
1 2 3 4 5 6 7 8 9
| handleDel = (id) => { const comments = [...this.state.comments] const idx = comments.findIndex((item) => item.id === id) comments.splice(idx, 1) this.setState({ comments, }) }
|
b,利用 findIndex
并 slice
1 2 3 4 5 6
| handleDel = (id) => { const idx = this.state.comments.findIndex((item) => item.id === id) this.setState({ comments: [...this.state.comments.slice(0, idx), ...this.state.comments.slice(idx + 1)], }) }
|
c,利用 filter
1 2 3 4 5
| handleDel = (id) => { this.setState({ comments: this.state.comments.filter((item) => item.id !== id), }) }
|
完整代码
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
| import React from 'react' import ReactDOM from 'react-dom' import './index.css'
export default class App extends React.Component { state = { comments: [ { id: 1, name: 'jack', content: '沙发!!!' }, { id: 2, name: 'tom', content: '楼主好人' }, ], name: '', content: '', } renderList() { const { comments } = this.state if (comments.length === 0) { return <div className='no-comment'>暂无评论,快去评论吧~</div> } return ( <ul> {comments.map((item) => ( <li key={item.id}> <h3>评论人:{item.name}</h3> <p>评论内容:{item.content}</p> <button onClick={() => this.handleDel(item.id)}>删除</button> </li> ))} </ul> ) } handleChange = (e) => { this.setState({ [e.target.name]: e.target.value, }) } handleSubmit = (e) => { const { name, content } = this.state if (name.trim() === '' || content.trim() === '') { alert('请输入内容') return } const newComments = [ { id: this.state.comments.length + 1, name, content, }, ...this.state.comments, ] this.setState({ comments: newComments, name: '', content: '', }) } handleDel = (id) => { this.setState({ comments: this.state.comments.filter((item) => item.id !== id), }) } render() { const { name, content } = this.state return ( <div className='app'> <div className='app-top'> <input type='text' name='name' className='user' placeholder='请输入评论人' value={name} onChange={this.handleChange} /> <textarea placeholder='请输入评论内容' name='content' value={content} onChange={this.handleChange} /> <button onClick={this.handleSubmit}>发表评论</button> </div> {this.renderList()} </div> ) } }
ReactDOM.render(<App />, document.querySelector('#root'))
|