1. React脚手架
1.1. 使用React脚手架创建项目
- 命令:
npx create-react-app react-basic
- npx create-react-app 是固定命令,
create-react-app
是 React 脚手架的名称 - react-basic 表示项目名称,可以修改
- npx create-react-app 是固定命令,
- 启动项目:
yarn start
ornpm start
npx
是 npm v5.2 版本新添加的命令,用来简化 npm 中工具包的使用- 原始:1 全局安装
npm i -g create-react-app
2 在通过脚手架的命令来创建 React 项目 - 现在:npx 调用最新的 create-react-app 直接创建 React 项目
- 原始:1 全局安装
1.2. React基本使用
由于创建的react版本都是18以上,需要降到17,我删除node_modules
和package-lock.json
文件,修改package.json
文件,再npm install
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// 创建react元素
const title = React.createElement('h1', null, 'Hello React')
// 渲染react元素到页面
ReactDOM.render(title, document.getElementById('root'))
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// 创建react元素
const title = React.createElement(
'ul',
{ className: 'list' },
React.createElement('li', null, 'react'),
React.createElement('li', null, 'vue'),
React.createElement('li', null, 'angular')
)
// 渲染react元素到页面
ReactDOM.render(title, document.getElementById('root'))
- 能够说出react是什么
- 是用于构建用户界面的javascript库
- 能够说出react的特点
- 声明式ui
- 组件化
- 一处学习,多次使用 react-dom react-native
2. JSX的基本使用
2.1. createElement的问题
- 繁琐不简洁
- 不直观,无法一眼看出所描述的结构
- 不优雅,开发体验不好
JSX简介:JSX是JavaScript XML的简写,表示了在Javascript代码中写XML(HTML)格式的代码
优势:声明式语法更加直观,与HTML结构相同,降低学习成本,提高开发效率。
JSX是react的核心内容
注意:JSX 不是标准的 JS 语法,是 JS 的语法扩展。脚手架中内置的 @babel/plugin-transform-react-jsx 包,用来解析该语法。
使用方法:
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// 创建react元素
const title = <div class="box">
<h1>Hello React</h1>
</div>
// 渲染react元素到页面
ReactDOM.render(title, document.getElementById('root'))
2.2. JSX注意点
- 只有在脚手架中才能使用jsx语法
- 因为JSX需要经过babel的编译处理,才能在浏览器中使用。脚手架中已经默认有了这个配置。
- JSX必须要有一个根节点,
<></>
<React.Fragment></React.Fragment>
- 没有子节点的元素可以使用
/>
结束 - JSX中语法更接近与JavaScript
class
=====>className
for
========>htmlFor
- JSX可以换行,如果JSX有多行,推荐使用
()
包裹JSX,防止自动插入分号的bug
2.3. JSX中嵌入JavaScript表达式
- 基本使用
const name = 'fg'
const age = 18
const title = (
<h1>
{name} is {age} years old
</h1>
)
- 可以访问对象属性
const car = {
brand: '红旗H9'
}
const title = <h1>我有一辆{car.brand}</h1>
- 可以访问数组下标
const friends = ['张三', '李四']
const title = (
<h1>
汽车:{friends[1]}
</h1>
)
- 可以使用三元运算符
const gender = 18
const title = (
<h1>
性别:{age >= 18? '是':'否'}
</h1>
)
- 可以调用方法
function sayHi() {
return '你好'
}
const title = <h1>姓名:{sayHi()}</h1>
不要出现语句,比如
if
for
条件渲染
// 通过if/else控制
const isLoading = false
const loadData = () => {
if (isLoading) {
return <div>Loading...</div>
} else {
return <div>数据加载完成</div>
}
}
const title = <div>{loadData()}</div>
// 通过三元运算符控制
const isLoding = false
const loadData = () => {
return isLoding ? (
<div>Loading.....</div>
) : (
<div>数据加载完成</div>
)
}
const title = <div>{loadData()}</div>
// 逻辑运算符
const isLoding = false
const loadData = () => {
return isLoding && <div>加载中...</div>
}
const title = <div>条件渲染:{loadData()}</div>
- 列表渲染
// 在react中,通过map方法进行列表渲染
const cars = ['红旗H5', '红旗H9', '小米SU7']
const list = cars.map(item => {
return <li>{item}</li>
})
const title = <div>{list}</div>
// 直接在JSX中渲染
const cars = ['红旗H5', '红旗H9', '小米SU7']
const title = (
<div>
{ cars.map(item => <li>{item}</li>) }
</div>
)
// key属性的使用
const title = (
<div>
{cars.map((item) => (
<li key={item}>{item}</li>
))}
</div>
)
注意:列表渲染时应该给重复渲染的元素添加key属性,key属性的值要保证唯一
- 样式处理
// 行内样式-style
const div = (
<div style={{ color: 'red', backgroundColor: 'yellow' }}>style样式</div>
)
// 类名-className
import './index.css'
const div = <div className="div">style样式</div>
2.4. 总结
- JSX是React的核心内容
- JSX表示在JS代码中书写HTML结构,是React声明式的体现
- 使用JSX配合嵌入的JS表达式,条件渲染,列表渲染,可以渲染任意的UI结构
- 结果使用className和style的方式给JSX添加样式
- React完全利用JS的语言自身的能力来编写UI,而不是造轮子增强HTML的功能。(对比VUE)
3. 组件基础
3.1. 函数组件
使用JS的函数或者箭头函数创建的组件
- 为了区分普通标签,函数组件的名称必须
大写字母开头
- 函数组件
必须有返回值
,表示该组件的结构 - 如果返回值为null,表示不渲染任何内容
使用函数创建组件
function Hello() {
return <div>函数组件</div>
}
使用箭头函数创建组件
const Hello = () => <div>函数组件</div>
使用组件
ReactDOM.render(<Hello />, document.getElementById('root'))
3.2. 类组件
使用ES6的class语法创建组件
- 类组件的名称必须是大写字母开头
- 类组件应该继承
React.Component
父类,从而可以使用父类中提供的方法或者属性 - 类组件必须提供
render
方法 - render方法必须有返回值,表示该组件的结构
定义组件
class Hello extends React.Component {
render() {
return <div>类组件</div>
}
}
使用组件
ReactDOM.render(<Hello />, document.getElementById('root'))
例子:
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import Hello from './components/Hello.jsx'
import Demo from './components/Demo.jsx'
// 创建react元素
const element = (
<div>
<Hello />
<Demo />
</div>
)
// 渲染react元素到页面
ReactDOM.render(element, document.getElementById('root'))
import { Component } from "react"
class Demo extends Component {
render() {
return (
<div>
<h1>类组件</h1>
</div>
)
}
}
export default Demo
const Hello = () => {
return (
<div>
<h1>函数组件</h1>
</div>
)
}
export default Hello
3.3. 状态组件
- 函数组件又叫做无状态组件(木偶组件/静态组件),是不能自己提供数据的。
- 类组件又叫做有状态组件(智能组件),可以自己提供数据,组件内部的状态发生改变,内容也会自动更新。
- 状态(state)即组件的私有数据,当组件的状态发生了改变,页面结构也会发生改变。
- 函数组件是没有状态的,只负责页面的展示(静态,不会发生变化)性能比较高。
- 类组件有自己的状态,负责更新UI,只要类组件的数据发生了改变,UI就会发生更新。
- 在复杂的项目中,一般都是由函数组件和类组件共同配合来完成的。
比如计数器案例,点击按钮让数值+1, 0和1就是不同时刻的状态,当状态从0变成1之后,UI也要跟着发生变化。React想要实现这种功能,就需要使用有状态组件来完成。
类组件的状态
- 状态state即数据,是组件内部的私有数据,只有在组件内部可以使用
- state的值是一个对象,表示一个组件中可以有多个数据
- state的基本使用
import { Component } from "react"
class Demo extends Component {
constructor() {
super()
this.state = {
name: '类组件'
}
}
render() {
return (
<div>
<h1>{this.state.name}</h1>
</div>
)
}
}
export default Demo
简洁写法:
class Demo extends Component {
state = {
name: '类组件'
}
render() {
return (
<div>
<h1>{this.state.name}</h1>
</div>
)
}
}
3.4. 事件处理
3.4.1. 注册事件
React注册时间与DOM的事件语法非常像
语法:
on + 事件名 = {事件处理程序}
注意:React事件采用驼峰命名法
import { Component } from "react"
class Demo extends Component {
render() {
return (
<div>
<button onClick={this.handleClick}>点我</button>
</div>
)
}
handleClick = () => {
console.log("点击了按钮")
}
}
export default Demo
3.4.2. 事件对象
- 可以通过事件处理程序的参数获取到事件对象
- React 中的事件对象叫做:合成事件(对象)
- 合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题
function handleClick(e) {
e.preventDefault()
console.log('事件对象', e)
}
<a onClick={this.handleClick}>点我,不会跳转页面</a>
3.4.3. this指向问题
事件处理程序中的this指向的是undefined
render方法中的this指向的是当前react组件,只有事件处理程序中的this有问题
import { Component } from "react"
class Demo extends Component {
state = {
count: 0
}
render() {
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.handleClick}>点我+1</button>
</div>
)
}
handleClick () {
console.log(this) // undefined
}
}
export default Demo
解决方案一:
使用bind
import { Component } from "react"
class Demo extends Component {
state = {
count: 0
}
render() {
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.handleClick.bind(this)}>点我+1</button>
</div>
)
}
handleClick() {
console.log(this.state.count)
}
}
export default Demo
解决方案二:
使用箭头函数,箭头函数自身没有this,访问的是外部的this
import { Component } from "react"
class Demo extends Component {
state = {
count: 0
}
render() {
return (
<div>
<h1>{this.state.count}</h1>
<button onClick={this.handleClick}>点我+1</button>
</div>
)
}
handleClick = () => {
console.log(this.state.count)
}
}
export default Demo
3.4.5. setState修改状态
语法:
this.setState({要修改的数据})
这样就能更改数据了。
3.5. 表单处理
react中处理表单元素有两种方式:
- 受控组件
- 非受控组件(DOM操作)
3.5.1. 受控组件
- HTML中表单元素是可输入的,即表单用户并维护着自己的可变状态(value)。
- 但是在react中,可变状态通常是保存在state中的,并且要求状态只能通过
setState
进行修改。 - React中将state中的数据与表单元素的value值绑定到了一起,
由state的值来控制表单元素的值
- 受控组件:value值受到了react控制的表单元素
案例:
- 在state中添加一个状态,作为表单元素的value值(控制表单元素的值)
- 给表单元素添加change事件,设置state的值为表单元素的值(控制值的变化)
import { Component } from "react"
class Demo extends Component {
state = {
msg: 'hello react'
}
render() {
return (
<div>
<input type="text" value={this.state.msg} onChange={this.handleChange} />
</div>
)
}
handleChange = (e) => {
this.setState({
msg: e.target.value
})
}
}
export default Demo
常见的受控组件
- 文本框、文本域、下拉框(操作value属性)
- 复选框(操作checked属性)
import { Component } from 'react'
class Demo extends Component {
state = {
username: '',
desc: '',
city: 2,
isSingle: true,
}
render() {
return (
<div>
姓名:
<input
type="text"
name="username"
value={this.state.msg}
onChange={this.handleChange}
/>
<br />
描述:
<textarea
name="desc"
value={this.state.desc}
onChange={this.handleChange}
></textarea>
<br />
城市:
<select
name="city"
value={this.state.city}
onChange={this.handleChange}
>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
</select>
<br />
是否单身:
<input
type="checkbox"
name="isSingle"
checked={this.state.isSingle}
onChange={this.handleChange}
></input>
</div>
)
}
handleChange = (e) => {
let { name, type, value, checked } = e.target
console.log(name, type, value, checked)
value = type === 'checkbox' ? checked : value
console.log(name, value)
this.setState({
[name]: value,
})
}
}
export default Demo
3.5.2. 非受控组件
借助ref,使用原生DOM的方式来获取表单元素的值
constructor() {
super()
this.txtRef = React.createRef()
}
<input type="text" ref={this.txtRef} onChange={this.change} />
change = () => {
console.log(this.txtRef.current.value)
}
4. 组件通信
组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据。在组件化过程中,我们将一个完整的功能
拆分成多个组件,以更好的完成整个应用的功能。而在这个过程中,多个组件之间不可避免的要共享某些数据
。为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯。
大白话:一个组件使用另一个组件的状态
props
- 组件是封闭的,要接收外部数据应该通过props来实现
- props的作用:接收传递给组件的数据
- 传递数据:给组件标签添加属性
- 接收数据:函数组件通过参数props接收数据,类组件通过this.props接收数据
4.1. 函数组件通信
父组件:
import Son from './Son.jsx'
const Father = () => {
const style = {
width: '500px',
height: '300px',
backgroundColor: 'skyblue',
}
return (
<div style={style}>
<h1>我是父组件</h1>
<Son name="fg" age={19} />
</div>
)
}
export default Father
子组件:
const Son = (props) => {
console.log(props)
const style = {
width: '500px',
height: '200px',
backgroundColor: 'orange',
marginTop: '58px',
}
return (
<div style={style}>
<h1>我是子组件</h1>
<p>
接收到父组件传来的数据:{props.name}--{props.age}岁
</p>
</div>
)
}
export default Son
4.2. 类组件通信
父组件:
import Son from './Son.jsx'
import { Component } from 'react'
class Father extends Component {
render() {
const style = {
width: '500px',
height: '300px',
backgroundColor: 'skyblue',
}
return (
<div style={style}>
<h1>我是父组件</h1>
<Son name="fg" age={19} />
</div>
)
}
}
export default Father
子组件:
import { Component } from 'react'
class Son extends Component {
constructor(props) {
super(props)
}
render() {
const style = {
width: '500px',
height: '200px',
backgroundColor: 'orange',
marginTop: '58px',
}
return (
<div style={style}>
<h1>我是子组件</h1>
<p>
接收到父组件传来的数据:{this.props.name}--{this.props.age}岁
</p>
</div>
)
}
}
export default Son
props的特点
- 可以给组件传递任意类型的数据
- props是只读的,不允许修改props的数据,单向数据流
- 注意:在类组件中使用的时候,需要把props传递给super(),否则构造函数无法获取到props
4.3. 三种通信方式
4.3.1. 父传子
- 父组件提供要传递的state数据
- 给子组件标签添加属性,值为state中的数据
- 子组件中通过props接收父组件中传递的数据
父组件:
import Son from './Son.jsx'
import { Component } from 'react'
class Father extends Component {
state = {
name: 'fg',
age: 19,
}
render() {
const style = {
width: '500px',
height: '300px',
backgroundColor: 'skyblue',
}
return (
<div style={style}>
<h1>我是父组件</h1>
<Son name={this.state.name} age={this.state.age} />
</div>
)
}
}
export default Father
子组件:
import { Component } from 'react'
class Son extends Component {
// 可用可不用,推荐用
constructor(props) {
super(props)
}
render() {
const style = {
width: '500px',
height: '200px',
backgroundColor: 'orange',
marginTop: '58px',
}
return (
<div style={style}>
<h1>我是子组件</h1>
<p>
接收到父组件传来的数据:{this.props.name}--{this.props.age}岁
</p>
</div>
)
}
}
export default Son
4.3.2. 子传父
- 父组件提供一个回调函数(用于接收数据)
- 将该函数作为属性的值,传递给子组件
- 子组件通过props调用回调函数
- 将子组件的数据作为参数传递给回调函数
父组件:
import Son from './Son.jsx'
import { Component } from 'react'
class Father extends Component {
render() {
const style = {
width: '500px',
height: '300px',
backgroundColor: 'skyblue',
}
return (
<div style={style}>
<h1>我是父组件</h1>
<Son getChildMsg={this.getChildMsg} />
</div>
)
}
getChildMsg = (msg) => {
console.log('父组件接收到的子组件信息为:', msg)
}
}
export default Father
子组件:
import { Component } from 'react'
class Son extends Component {
state = {
childMsg: '我是子组件的数据',
}
render() {
const style = {
width: '500px',
height: '200px',
backgroundColor: 'orange',
marginTop: '58px',
}
return (
<div style={style}>
<h1>我是子组件</h1>
<button onClick={this.handleClick}>点我给父组件传数据</button>
</div>
)
}
handleClick = () => {
this.props.getChildMsg(this.state.childMsg)
}
}
export default Son
4.3.3. 兄弟组件传值
- 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
- 公共组件职责:
- 提供共享状态
- 提供操作共享状态的方法
- 要通信的子组件只需通过props接收状态或操作状态的方法
状态提升前:
状态提升之后:
Son1给Son2传数据:
公共组件:
import Son1 from './Son1.jsx'
import Son2 from './Son2.jsx'
import { Component } from 'react'
class Father extends Component {
state = {
msg: '',
}
render() {
const style1 = {
width: '500px',
height: '300px',
backgroundColor: 'skyblue',
}
const style2 = {
display: 'flex',
}
return (
<div style={style1}>
<h1>我是公共组件</h1>
<div style={style2}>
<Son1 say={this.sendMsg} />
<Son2 msg={this.state.msg} />
</div>
</div>
)
}
sendMsg = (msg) => {
this.setState({
msg,
})
}
}
export default Father
Son1组件:
import { Component } from 'react'
class Son1 extends Component {
render() {
const style = {
width: '500px',
height: '200px',
backgroundColor: 'orange',
marginTop: '58px',
}
return (
<div style={style}>
<h1>我是子组件1</h1>
<button onClick={this.sayMsg}>给Son2组件传数据</button>
</div>
)
}
sayMsg = () => {
this.props.say('Hello,Son2组件')
}
}
export default Son1
Son2组件:
import { Component } from 'react'
class Son2 extends Component {
render() {
const style = {
width: '500px',
height: '200px',
backgroundColor: 'gray',
marginTop: '58px',
}
return (
<div style={style}>
<h1>我是子组件2</h1>
收到Son1组件的数据:{this.props.msg}
</div>
)
}
}
export default Son2
4.4. 组件通信–context
App组件要传递数据给Child组件时,该如何处理?
处理方式:使用 props 一层层组件往下传递(繁琐)
更好的姿势:使用 Context
作用:跨组件传递数据(比如:主题、语言等)
实现思路:
Father.jsx
import Son from './Son.jsx'
import React, { Component } from 'react'
const { Provider, Consumer } = React.createContext()
export { Consumer }
class Father extends Component {
state = {
msg: '鸡你太美',
num: 666,
}
render() {
const style1 = {
width: '500px',
height: '300px',
backgroundColor: 'skyblue',
}
const { msg, num } = this.state
return (
<Provider value={{ msg, num }}>
<div style={style1}>
<h1>我是父组件</h1>
<Son />
</div>
</Provider>
)
}
}
export default Father
Son.jsx
import { Component } from 'react'
import Sun from './Sun.jsx'
class Son extends Component {
render() {
const style = {
width: '400px',
height: '235px',
backgroundColor: 'orange',
}
return (
<div style={style}>
<h1>我是子组件</h1>
<Sun />
</div>
)
}
}
export default Son
Sun.jsx
import React, { Component } from 'react'
import { Consumer } from './Father'
class Sun extends Component {
render() {
const style = {
width: '300px',
height: '173px',
backgroundColor: 'gray',
}
return (
<div style={style}>
<Consumer>
{({ msg, num }) => (
<h1>
我是孙组件--{msg}
{num}
</h1>
)}
</Consumer>
</div>
)
}
}
export default Sun
总结:
- 如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯
- Context提供了两个组件:Provider 和 Consumer
- Provider组件:用来提供数据
- Consumer组件:用来消费数据
4.5. Props深入
4.5.1. children属性
children属性:表示该组件的子节点,只要组件有子节点,props就有该属性
children 属性与普通的props一样,值可以是任意值(文本、React元素、组件,甚至是函数)
import Son from './Son.jsx'
import React, { Component } from 'react'
export default class Father extends Component {
render() {
return (
<div>
<h1>children属性</h1>
<Son title="我是标题">我是内容</Son>
</div>
)
}
}
import { Component } from 'react'
export default class Son extends Component {
render() {
return (
<div>
<div className="title">
<h3>{this.props.title}</h3>
</div>
<div className="content">{this.props.children}</div>
</div>
)
}
}
4.5.2. props校验
目的:校验接收的props的数据类型,增加组件的健壮性
对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据
如果传入的数据格式不对,可能会导致组件内部报错。组件的使用者不能很明确的知道错误的原因。
props校验允许在创建组件的时候,就约定props的格式、类型等
作用:规定接收的props的类型必须为数组,如果不是数组就会报错,增加组件的健壮性。
import Son from './Son.jsx'
import React, { Component } from 'react'
export default class Father extends Component {
render() {
return (
<div>
<h1>children属性</h1>
<Son title="我是标题">我是内容</Son>
</div>
)
}
}
import { Component } from 'react'
import propTypes from 'prop-types'
export default class Son extends Component {
static propTypes = {
title: propTypes.string.isRequired,
}
render() {
return (
<div>
<div className="title">
<h3>{this.props.title}</h3>
</div>
<div className="content">{this.props.children}</div>
</div>
)
}
}
约束规则
- 常见类型:array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象:shape({ })
// 常见类型
optionalFunc: PropTypes.func,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
4.5.3. props默认值
场景:分页组件 每页显示条数
作用:给 props 设置默认值,在未传入 props 时生效
function App(props) {
return (
<div>
此处展示props的默认值:{props.pageSize}
</div>
)
}
// 设置默认值
App.defaultProps = {
pageSize: 10
}
// 不传入pageSize属性
<App />
4.6. TodoList案例
App.jsx
import { Component } from 'react'
import './App.css'
import Header from './components/Header'
import Footer from './components/Footer'
import List from './components/List'
export default class App extends Component {
state = {
todos: [],
}
render() {
const { todos } = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo} />
<List
todos={todos}
updateTodo={this.updateTodo}
deleteTodo={this.deleteTodo}
/>
<Footer
todos={todos}
checkAllTodo={this.checkAllTodo}
clearAllDone={this.clearAllDone}
/>
</div>
</div>
)
}
// 添加todo
addTodo = (todo) => {
// 获取原todo
const { todos } = this.state
// 追加
const newTodos = [todo, ...todos]
// 更新状态
this.setState({
todos: newTodos,
})
}
// 更新todo
updateTodo = (id, isDone) => {
const { todos } = this.state
const newTodos = todos.map((todo) => {
if (todo.id === id) {
return {
...todo,
isDone,
}
} else {
return todo
}
})
this.setState({
todos: newTodos,
})
}
// 删除todo
deleteTodo = (id) => {
// 获取原todo
const { todos } = this.state
// 过滤
const newTodos = todos.filter((todo) => todo.id !== id)
// 更新状态
this.setState({
todos: newTodos,
})
}
// 全选
checkAllTodo = (isDone) => {
const { todos } = this.state
const newTodos = todos.map((todo) => {
return {
...todo,
isDone,
}
})
this.setState({
todos: newTodos,
})
}
// 清除已完成
clearAllDone = () => {
const { todos } = this.state
const newTodos = todos.filter((todo) => !todo.isDone)
this.setState({
todos: newTodos,
})
}
}
Header.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { nanoid } from 'nanoid'
import './index.css'
export default class Header extends Component {
// 对接收的props进行类型限制
static propTypes = {
addTodo: PropTypes.func.isRequired,
}
render() {
return (
<div className="todo-header">
<input
onKeyUp={this.handleKeyUp}
type="text"
placeholder="请输入你的任务名称,按回车键确认"
/>
</div>
)
}
// 当按下回车键时,触发事件
handleKeyUp = (event) => {
const { keyCode, target } = event
if (keyCode !== 13) return
if (target.value.trim() === '') {
alert('输入不能为空')
return
}
// 安装nanoid保证ID唯一
const obj = {
id: nanoid(),
name: target.value,
isDone: false,
}
this.props.addTodo(obj)
target.value = ''
}
}
List.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './index.css'
import Item from '../Item'
export default class List extends Component {
// 对接收的props进行类型限制
static propTypes = {
todos: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired,
}
render() {
const { todos, updateTodo, deleteTodo } = this.props
return (
<ul className="todo-main">
{todos.map((todo) => {
return (
<Item
key={todo.id}
{...todo}
updateTodo={updateTodo}
deleteTodo={deleteTodo}
/>
)
})}
</ul>
)
}
}
Item.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './index.css'
export default class Item extends Component {
// 对接收的props进行类型限制
static propTypes = {
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired,
}
state = {
isShow: false, // 控制删除按钮和高亮的显示
}
render() {
const { id, name, isDone } = this.props
const { isShow } = this.state
return (
<li
onMouseEnter={this.handleMouse(true)}
onMouseLeave={this.handleMouse(false)}
style={{ backgroundColor: isShow ? '#ddd' : '#fff' }}
>
<label>
<input
type="checkbox"
checked={isDone}
onChange={this.handleCheck(id)}
/>
<span>{name}</span>
</label>
<button
className="btn btn-danger"
style={{ display: isShow ? 'block' : 'none' }}
onClick={() => this.handleDelete(id)}
>
删除
</button>
</li>
)
}
// 鼠标移入移出事件
handleMouse = (flag) => {
return () => {
this.setState({
isShow: flag,
})
}
}
// 复选框勾选事件
handleCheck = (id) => {
return (event) => {
this.props.updateTodo(id, event.target.checked)
}
}
// 删除todo
handleDelete = (id) => {
if (window.confirm('确定删除吗?')) {
this.props.deleteTodo(id)
}
}
}
Footer.jsx
import React, { Component } from 'react'
import propTypes from 'prop-types'
import './index.css'
export default class Footer extends Component {
// 对接收的props进行类型限制
static propTypes = {
todos: propTypes.array.isRequired,
checkAllTodo: propTypes.func.isRequired,
clearAllDone: propTypes.func.isRequired,
}
render() {
const { todos } = this.props
// 计算已完成任务的数量
const doneCount = todos.reduce(
(pre, todo) => pre + (todo.isDone ? 1 : 0),
0
)
// 计算总任务数
const totalCount = todos.length
return (
<div className="todo-footer">
<label>
<input
type="checkbox"
checked={doneCount === totalCount && totalCount > 0}
onChange={this.handleCheckAll}
/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{totalCount}
</span>
<button className="btn btn-danger" onClick={this.handleClearDone}>
清除已完成任务
</button>
</div>
)
}
handleCheckAll = (event) => {
this.props.checkAllTodo(event.target.checked)
}
handleClearDone = () => {
this.props.clearAllDone()
}
}
5. 生命周期
概述
- 意义:组件的生命周期有助于理解组件的运行方式、完成更复杂的组件功能、分析组件错误原因等
- 组件的生命周期:组件从被创建到挂载到页面中运行,再到组件不用时卸载的过程
- 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机。
- 只有 类组件 才有生命周期。
5.1.整体说明
- 每个阶段的执行时机
- 每个阶段钩子函数的执行顺序
- 每个阶段钩子函数的作用
5.2. 挂载阶段
执行时机:组件创建时(页面加载时)
执行顺序:
钩子函数 | 触发时机 | 作用 |
---|---|---|
constructor | 创建组件时,最先执行 | 初始化state、创建Ref等 |
render | 每次组件渲染都会触发 | 渲染UI(注意:不能调用setState()) |
componentDidMount | 组件挂载(完成DOM渲染)后 | 发送网络请求、DOM操作 |
5.3. 更新阶段
- 执行时机:1. setState() 2. forceUpdate() 3. 组件接收到新的props
- 说明:以上三者任意一种变化,组件就会重新渲染
- 执行顺序
钩子函数 | 触发时机 | 作用 |
---|---|---|
render | 每次组件渲染都会触发 | 渲染UI(与挂载阶段是同一个render) |
componentDidUpdate | 组件更新(完成DOM渲染)后 | DOM操作,可以获取到更新后的DOM内容,不要调用setState |
5.4. 卸载阶段
执行时机:组件从页面中消失
钩子函数 | 触发时机 | 作用 |
---|---|---|
componentWillUnmount | 组件卸载(从页面中消失) | 执行清理工作(比如:清理定时器等) |
import { Component } from 'react'
import React from 'react'
import Child from './Child'
export default class Demo extends Component {
constructor(props) {
super(props)
console.log('1', 'constructor')
// 1.提供数据
this.state = {
msg: 'hello react',
count: 0,
}
// 2.提供ref
this.inputRef = React.createRef()
// 3.绑定事件this指向
this.handleClick = this.handleClick.bind(this)
}
// 渲染函数
render() {
// 打印日志
console.log('2', 'render')
// 返回一个div,包含state中的msg和count,以及一个按钮,点击按钮会调用handleClick函数
return (
<div>
{this.state.msg}-{this.state.count}
<button onClick={this.handleClick}>按钮</button>
{this.state.count && this.state.count < 3 && (
<Child count={this.state.count} />
)}
</div>
)
}
componentDidMount() {
console.log('3', 'componentDidMount')
}
componentDidUpdate() {
console.log('4', 'componentDidUpdate')
}
handleClick() {
this.setState({
count: this.state.count + 1,
})
}
}
import { Component } from 'react'
import React from 'react'
export default class Child extends Component {
render() {
return <div>{this.props.count}</div>
}
componentDidMount() {
console.log('Child挂载了', 'componentDidMount')
}
componentDidUpdate() {
console.log('Child更新了', 'componentDidUpdate')
}
componentWillUnmount() {
console.log('Child卸载了', 'componentWillUnmount')
}
}
6. react原理
6.1. setState
6.1.1. 更新数据
- setState()是异步更新数据的
- 注意:使用该语法时,后面的setState()不要依赖于前面的setState()
- 当你调用 setState 的时候,React.js 并不会马上修改 state (为什么)
- 而是把这个对象放到一个更新队列里面
- 稍后才会从队列当中把新的状态提取出来合并到 state 当中,然后再触发组件更新。
- 可以多次调用setState(),只会触发一次重新渲染
this.state = { count: 1 }
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 1
在使用 React.js 的时候,并不需要担心多次进行 setState
会带来性能问题。
6.1.2. 推荐语法
- 使用
setState((preState) => {})
语法 - 参数preState:React.js会把上一个setState的效果传入这个函数
this.setState((preState) => {
return {
count: preState.count + 1
}
})
console.log(this.state.count) // 1
这种语法依旧是异步的,但是state可以获取到最新的状态,适用于需要调用多次setState
6.1.3. 第二个参数
- 场景:在状态更新(页面完成重新渲染)后立即执行某个操作
- 语法:
setState(updater[, callback])
this.setState(
(state) => ({}),
() => {console.log('这个回调函数会在状态更新后立即执行')}
)
this.setState(
(state, props) => {},
() => {
document.title = '更新state后的标题:' + this.state.count
}
)
6.2. 组件更新机制
- setState()的两个作用:1.修改state 2.更新组件UI
- 过程:父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件)
6.3. 组件性能优化
6.3.1. 减轻state
- 减轻state:只存储跟组件渲染相关的数据(比如:count/列表数据/loading等)
- 注意:不用做渲染的数据不要放在state中,比如定时器id等
- 对于这种需要在多个方法中用到的数据,应该直接放在this中
this.xxx = 'aaa'
class Hello extends Component {
componentDidMount() {
// timerId存储到this中,而不是state中
this.timerId = setInterval(() => {}, 2000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() {...}
}
6.3.2. 避免不必要的重新渲染
- 组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰
- 问题:子组件没有任何变化时也会重新渲染(接收到的props没有发生任何的改变)
- 如何避免?
- 解决方式:使用钩子函数
shouldComponentUpdate(nextProps, nextState)
- 作用:通过返回值决定该组件是否重新渲染,返回true表示重新渲染,false表示不重新渲染
- 触发时机:更新阶段的钩子函数,组件重新渲染前执行(shouldComponentUpdate => render)
class Hello extends Component {
shouldComponentUpdate() {
// 根据条件,决定是否重新渲染组件
return false
}
render() {...}
}
6.4. 纯组件
- 纯组件:
React.PureComponent
与React.Component
功能相似 - 区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较
- 原理:纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件
class Hello extends React.PureComponent {
render() {
return (
<div>纯组件</div>
)
}
}
只有在性能优化的时候可能会用到纯组件,不要所有的组件都使用纯组件,因为纯组件需要消耗性能进行对比
6.4.1. 纯组件比较-值类型
- 说明:纯组件内部的对比是 shallow compare(浅层对比)
- 对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)
let number = 0
let newNumber = number
newNumber = 2
console.log(number === newNumber) // false
state = { number:0 }
setState({
number: Math.floor(Math.floor(Math.random() * 3))
})
// PureComponent内部对比:
最新的state.number === 上一次的state.number // false,重新渲染组件
6.4.2. 纯组件比较-引用类型
- 说明:纯组件内部的对比是 shallow compare(浅层对比)
- 对于引用类型来说:只比较对象的引用(地址)是否相同
const obj = { number: 0 }
const newObj = obj
newObj.number = 2
console.log(newObj === obj) // true
state = { obj: { number: 0 } }
// 错误做法
state.obj.number = 2
setState({ obj: state.obj })
// PureComponent内部比较:
最新的state.obj === 上一次的state.obj // true,不重新渲染组件
纯组件的最佳实践:
注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!
// 正确!创建新数据
const newObj = {...state.obj, number: 2}
setState({ obj: newObj })
// 正确!创建新数据
// 不要用数组的push / unshift 等直接修改当前数组的的方法
// 而应该用 concat 或 slice 等这些返回新数组的方法
this.setState({
list: [...this.state.list, {新数据}]
})