React

发布于:2025-03-07 ⋅ 阅读:(15) ⋅ 点赞:(0)

1. React脚手架


1.1. 使用React脚手架创建项目

  • 命令:npx create-react-app react-basic
    • npx create-react-app 是固定命令,create-react-app 是 React 脚手架的名称
    • react-basic 表示项目名称,可以修改
  • 启动项目:yarn start or npm start
  • npx 是 npm v5.2 版本新添加的命令,用来简化 npm 中工具包的使用
    • 原始:1 全局安装npm i -g create-react-app 2 在通过脚手架的命令来创建 React 项目
    • 现在:npx 调用最新的 create-react-app 直接创建 React 项目

1.2. React基本使用

由于创建的react版本都是18以上,需要降到17,我删除node_modulespackage-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. 父传子

  1. 父组件提供要传递的state数据
  2. 给子组件标签添加属性,值为state中的数据
  3. 子组件中通过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. 子传父

  1. 父组件提供一个回调函数(用于接收数据)
  2. 将该函数作为属性的值,传递给子组件
  3. 子组件通过props调用回调函数
  4. 将子组件的数据作为参数传递给回调函数

父组件:

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. 兄弟组件传值

  1. 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
  2. 公共组件职责:
    • 提供共享状态
    • 提供操作共享状态的方法
  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

在这里插入图片描述

总结:

  1. 如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯
  2. Context提供了两个组件:Provider 和 Consumer
  3. Provider组件:用来提供数据
  4. 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>
    )
  }
}

约束规则

  1. 常见类型:array、bool、func、number、object、string
  2. React元素类型:element
  3. 必填项:isRequired
  4. 特定结构的对象: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()
  1. 当你调用 setState 的时候,React.js 并不会马上修改 state (为什么)
  2. 而是把这个对象放到一个更新队列里面
  3. 稍后才会从队列当中把新的状态提取出来合并到 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.PureComponentReact.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, {新数据}]
})

7. 路由


React Router

8. Redux


Redux