前端React笔记

React笔记

react概述

什么是react

react是一个用于构建用户界面的JavaScript库

react的特点

  • 声明式:只需要描述HTML看起来是什么样子
  • 基于组件:组件是react中最重要的内容,组件是页面中一部分的内容
  • 学习一次,随处使用:可以在多个领域使用

react基本使用

安装react

安装命令:npm i react react-dom

  • react包是核心,提供创建元素,组件等功能。
  • react-dom包提供操作DOM相关功能。

react的使用

  1. 引入react和react-dom

  2. 创建React元素

  3. 渲染react元素到页面中

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
  <!-- 引入js文件 -->
  <script src="./node_modules/react/umd/react.development.js"></script>
  <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
  
  <script>
    // 创建react元素
    // 参数一:元素名称
    // 参数二:元素属性{title: '我是标题'}
    // 第三个及其以后的参数:元素子节点
    const title = React.createElement('h1', null, 'Hello React')
    // 渲染react元素
    // 参数一:要渲染的react元素
    // 参数二:挂载点
    ReactDOM.render(title, document.querySelector('#root'))
  </script>
</body>
</html>

react脚手架的使用

react脚手架意义

脚手架是开发现代Web应用的必备

充分利用Webpack Babel ESLint等工具辅助项目开发

零配置,无需手动配置繁琐的工具即可使用

关注业务,而不是工具配置

使用React脚手架初始化项目

初始化项目命令:

npx create-react-app my-app

启动项目:切换到项目根目录下使用命令

npm start

在脚手架中使用react

  1. 导入react和react-dom两个包

  2. 调用React.createElement()方法创建React元素

  3. 调用ReactDOM.render()方法渲染React元素到页面中

找到src下index.js文件删除所有内容后编写如下内容

// 导入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.querySelector('#root'))

JSX的基本使用

createElement()方法的问题

  • 繁琐不简洁
  • 不直观,无法快速看出元素结构
  • 不优雅,程序员的苦恼

JSX简介

JSX是JavaScript XML的简写,表示在JavaScript中写XML(HTML)格式的代码

优势:声明式语法更加直观、与HTML结构相同,降低学习成本、提升开发效率

使用步骤

  1. 使用JSX语法创建React元素
  2. 使用ReactDOM.render()方法渲染React元素到页面中
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

// JSX创建react元素
const title = <h1>Hello JSX</h1>

// 渲染react元素
ReactDOM.render(title, document.querySelector('#root'))

为什么脚手架中可以使用JSX语法

默认使用babel进行编译处理,然后在浏览器环境中使用

注意点

  1. React元素的属性名使用驼峰命名法

  2. 特殊属性名:class->className、for->htmlFor、tabindex->tabIndex

  3. React元素没有子节点则可以 /> 结束

  4. 推荐使用小括号包裹JSX语法,从而避免一些问题

// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

// JSX创建react元素
const title = (
  <h1 className="title">Hello JSX</h1>
)

// 渲染react元素
ReactDOM.render(title, document.querySelector('#root'))

JSX中使用JavaScript表达式

当数据存储在JavaScript中时,我们需要在JSX中使用JavaScript表达式

语法:{JavaScript表达式}

// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

// 数据
const name = "Jack"

// JSX创建react元素
const dv = (
  <div>
    <h1 className="title">Hello JSX</h1>
    <div>你好,我叫{name}</div>
  </div>
)

// 渲染react元素
ReactDOM.render(dv, document.querySelector("#root"))

注意点:

  • 单大括号中可以使用任意的JavaScript表达式
  • JSX自身也是JavaScript表达式
  • JavaScript的对象是一个例外,一般只会出现style属性中
  • 不能在{}出现语句(if/for)

JSX的条件渲染

根据条件渲染特定的JSX结构

// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  条件渲染
*/
const isLoading = false;
const loadData = () => {
  if(isLoading) {
    return <div>loading...</div>
  }else {
    return <div>数据加载完成,此处显示加载完的数据</div>
  }
}

const title = (
  <h1>
    条件渲染:
    { loadData() }
  </h1>
)

ReactDOM.render(title, document.querySelector('#root'))

JSX的列表渲染

// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  列表渲染
*/
const songs = [
  {id: 1, name: '痴心绝对'},
  {id: 2, name: '像我这样的人'},
  {id: 3, name: '南山南'}
]

const list = (
  <ul>
    { songs.map(item => <li key={ item.id }>{ item.name }</li>) }
  </ul>
)

ReactDOM.render(list, document.querySelector('#root'))
  • 如果要渲染一组数据,可以使用数组的map()方法
  • 渲染列表时应该添加key属性,key属性的值要保证唯一
  • map遍历谁,就给谁添加key属性
  • 尽量避免使用索引号作为key

JSX的样式处理

行内样式

// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  样式处理
*/
// 行内样式
const list = (
  <h1 style={ {color: 'red'} }>
    JSX样式处理
  </h1>
)

ReactDOM.render(list, document.querySelector('#root'))

外联样式

新建css文件

.title {
  color: red;
  background-color: aqua;
}
// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'
// 导入css
import './css/index.css'

/* 
  样式处理
*/
// 外联样式
const list = (
  <h1 className='title'>
    JSX样式处理
  </h1>
)

ReactDOM.render(list, document.querySelector('#root'))

React组件

React组件介绍

组件是React的"一等公民",使用React就是在用组件

组件表示页面中的部分功能

组合多个组件实现完整的页面功能

特点:可复用,独立,可组合

React组件的两种创建方式

使用函数创建组件

  • 函数名称必须以大写字母开头
  • 函数组件必须有返回值,表示该组件的结构
  • 如果返回值为null,表示不渲染任何内容

渲染函数组件:使用函数名作为组件标签名

组件标签可以是单标签也可以是双标签

// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

/*
  函数组件
*/
function Hello() {
  return ( <div>这是我的第一个函数组件</div> )
}

// 渲染函数组件
ReactDOM.render(<Hello />, document.querySelector('#root'))

使用类创建组件

  • 类名称必须以大写字母开头

  • 类组件应该继承React.Component父类,从而可以使用父类的方法和属性

  • 类组件必须提供render()方法

  • render()方法必须有返回值,表示该组件的结构

// 导入react和react-dom
import React from 'react'
import ReactDOM from 'react-dom'

/*
  类组件
*/
class Hello extends React.Component {
  render() {
    return ( <div>这是我的第一个类组件</div> )
  }
}

// 渲染类组件
ReactDOM.render(<Hello />, document.querySelector('#root'))

将组件抽离为独立的js文件

  1. 创建Hello.js

  2. 在Hello.js中导入React

  3. 创建组件

  4. 在Hello.js中导出该组件

  5. 在index.js中导入该组件

  6. 渲染组件

Hello.js内容如下:

import React from 'react'

class Hello extends React.Component {
  render() {
    return ( <div>这是抽离出来的类组件</div> )
  }
}

// 导出Hello组件
export default Hello

index.js内容如下:

// 导入react-dom
import ReactDOM from 'react-dom'

/*
  抽离组件到独立的JS文件中
*/
import Hello from './js/Hello.js'

// 渲染类组件
ReactDOM.render(<Hello />, document.querySelector('#root'))

React的事件处理

事件绑定

语法:

onClick = { () => {} }
import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  render() {
    // 绑定一个单击事件
    return ( <button onClick = { function() {
      console.log('我被点击了')
    } }>点我</button> )
  }
}

ReactDOM.render(<App />, document.querySelector('#root'))

事件对象

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  render() {
    // 绑定一个单击事件,并获取事件对象
    return ( <a href="http://itcast.cn/" onClick = { function(e) {
      // 阻止默认行为
      e.preventDefault()
      console.log('我被点击了')
    } }>点我</a> )
  }
}

ReactDOM.render(<App />, document.querySelector('#root'))

有状态组件和无状态组件

函数组件又叫做无状态组件,类组件又叫做有状态组件

状态(state)即数据

函数组件没有自己的状态,只负责数据展示

类组件有自己的状态,负责更新UI

组件中的state和setState()

state的基本使用

  • 状态(state)即数据,是组件内部的私有数据,只能在组件内部使用

  • state的值是对象,表示一个组件中可以有多个数据

  • 获取state使用this.state

import React from 'react'
import ReactDOM from 'react-dom'

/*
  state的基本使用
*/
class App extends React.Component {
  // constructor() {
  //   super()
  //   // 初始化
  //   this.state = {
  //     count: 0
  //   }
  // }
  state = {
    count: 0
  }
  render() {
    return ( 
      <div>
        <h1>计数器:{ this.state.count }</h1>
      </div> 
    )
  }
}

ReactDOM.render(<App />, document.querySelector('#root'))

setState()修改状态

  • 状态是可变的
  • 语法:this.setState( {要修改的数据} )
  • 不要直接改state中的值,这是不好的,并且报错
  • setState()作用:1,修改state。2,更新UI
  • 思想:数据驱动视图
import React from 'react'
import ReactDOM from 'react-dom'

/*
  setState的基本使用
*/
class App extends React.Component {
  // constructor() {
  //   super()
  //   // 初始化
  //   this.state = {
  //     count: 0
  //   }
  // }
  state = {
    count: 0,
    test: '测试数据'
  }
  render() {
    return ( 
      <div>
        <h1>计数器:{ this.state.count }</h1>
        <button onClick = { () => {
          this.setState({
            count: this.state.count + 1
          })
        } }>+1</button>
      </div> 
    )
  }
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))

从JSX中抽离事件处理程序

  • JSX中掺杂过多JS逻辑代码,会显得非常混乱
  • 将逻辑抽离到单独的方法中,保证JSX结构清晰
import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  state = {
    count: 0,
    test: '测试数据'
  }
  
  //抽离事件处理程序
  onIncrement() {
    console.log(this) // undefined
    this.setState({
      count: this.state.count + 1
    })
  }
  
  render() {
    return ( 
      <div>
        <h1>计数器: { this.state.count }</h1>
        <button onClick = { this.onIncrement }>+1</button>
      </div> 
    )
  }
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))

事件绑定this指向问题

箭头函数改变this指向

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  state = {
    count: 0,
    test: '测试数据'
  }
  
  //抽离事件处理程序
  onIncrement() {
    console.log(this)
    this.setState({
      count: this.state.count + 1
    })
  }
  
  render() {
    return ( 
      <div>
        <h1>计数器:{ this.state.count }</h1>
        <button onClick = { () => this.onIncrement() }>+1</button>
      </div> 
    )
  }
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))

bind()方法改变this指向

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  state = {
    count: 0,
    test: '测试数据'
  }
  
  constructor() {
    super()
    this.onIncrement = this.onIncrement.bind(this)
  }
  
  //抽离事件处理程序
  onIncrement() {
    console.log(this)
    this.setState({
      count: this.state.count + 1
    })
  }
  
  render() {
    return ( 
      <div>
        <h1>计数器:{ this.state.count }</h1>
        <button onClick = { this.onIncrement }>+1</button>
      </div> 
    )
  }
}
// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))

class的实例方法使用箭头函数改变this指向

import React from 'react'
import ReactDOM from 'react-dom'

class App extends React.Component {
  state = {
    count: 0,
    test: '测试数据'
  }
  
  //抽离事件处理程序
  onIncrement = () => {
    console.log(this)
    this.setState({
      count: this.state.count + 1
    })
  }
  
  render() {
    return ( 
      <div>
        <h1>计数器:{ this.state.count }</h1>
        <button onClick = { this.onIncrement }>+1</button>
      </div> 
    )
  }
}

// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))

表单处理

受控组件

  • HTML的表单元素是可输入的,也就是有自己的可变状态
  • 而React中可变状态通常保存在state中,并且只能通过setState()方法来修改
  • React将state与表单元素值value绑定到一起,由state的值来控制表单元素的值
  • 受控组件:其值受到React控制的表单元素
受控组件的使用步骤
  1. 在state中添加一个状态,作为表单元素的value
  2. 给表单元素绑定一个change事件,将表单元素的值设置为state的值
import React from 'react'
import ReactDOM from 'react-dom'

/*
  受控组件:其值受到React控制的表单元素
  操作文本框的值
*/
class App extends React.Component {
  state = {
    // 文本框的值
    txt: ''
  }
  
  handleChange = (e) => {
    console.log(this)
    this.setState({
      txt: e.target.value
    })
  }
  
  render() {
    return (
      <input type="text" value={ this.state.txt } onChange={ this.handleChange } />
    )
  }
}

// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
受控组件的示例
import React from 'react'
import ReactDOM from 'react-dom'

/*
  受控组件示例
*/
class App extends React.Component {
  state = {
    // 文本框的值
    txt: '',
    // 富文本框的值
    content: '',
    // 下拉框
    city: 'bj',
    // 复选框
    isChecked: false
  }
  
  // 监听文本框
  handleChange = (e) => {
    // console.log(this)
    this.setState({
      txt: e.target.value
    })
  }
  
  // 监听富文本框
  handleContent = (e) => {
    // console.log(this)
    this.setState({
      content: e.target.value
    })
  }
  
  // 监听下拉框
  handleCity = (e) => {
    this.setState({
      city: e.target.value
    })
  }
  
  // 监听复选框
  handleCheck = (e) => {
    this.setState({
      isChecked: e.target.checked
    })
  }
  
  render() {
    return (
      <div>
        { /* 文本框 */ }
        <input type="text" value={ this.state.txt } onChange={ this.handleChange } /><br />
        { /* 富文本框 */ }
        <textarea value={ this.state.content } onChange= { this.handleContent }></textarea><br />
        { /* 下拉框 */ }
        <select value = {this.state.city} onChange = { this.handleCity }>
          <option value="sh">上海</option>
          <option value="bj">北京</option>
          <option value="gz">广州</option>
        </select><br />
        { /* 复选框 */ }
        <input type="checkbox" checked={ this.state.isChecked } onChange={ this.handleCheck } /><br />
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))
多表单元素优化
  • 问题:每个表单元素都有一个单独的事件处理程序处理太繁琐

  • 优化:使用一个事件处理程序同时处理多个表单元素

多表单元素优化步骤:

  1. 给表单元素添加name属性,名称与state相同
  2. 根据表单元素类型获取对应值
  3. 在change事件处理程序当中通过{name}来修改对应的state
import React from 'react'
import ReactDOM from 'react-dom'

/*
  受控组件示例
*/
class App extends React.Component {
  state = {
    // 文本框的值
    txt: '',
    // 富文本框的值
    content: '',
    // 下拉框
    city: 'bj',
    // 复选框
    isChecked: false
  }
  
  // 监听state
  handleChange = (e) => {
    // console.log(this)
    this.setState({
      [e.target.name]: e.target.type === "checkbox" ? e.target.checked : e.target.value
    })
  }
  
  render() {
    return (
      <div>
        { /* 文本框 */ }
        <input type="text" value={ this.state.txt } onChange={ this.handleChange } name="txt" /><br />
        { /* 富文本框 */ }
        <textarea value={ this.state.content } onChange= { this.handleChange } name="content"></textarea><br />
        { /* 下拉框 */ }
        <select value = {this.state.city} onChange = { this.handleChange } name="city">
          <option value="sh">上海</option>
          <option value="bj">北京</option>
          <option value="gz">广州</option>
        </select><br />
        { /* 复选框 */ }
        <input type="checkbox" checked={ this.state.isChecked } onChange={ this.handleChange } name="isChecked" /><br />
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))

非受控组件

  • 说明:借助于ref,使用原生DOM方式来获取表单元素值

  • ref作用:获取DOM或组件

非受控组件使用步骤
  1. 调用React.createRef()方法创建一个ref对象
  2. 将创建好的ref对象添加到文本框中
  3. 通过ref对象获取到文本框的值
import React from 'react'
import ReactDOM from 'react-dom'

/*
  非受控组件
*/
class App extends React.Component {
  constructor() {
    super()
    // 创建ref对象
    this.txtRef = React.createRef()
  }
  getTxt = () => {
    // 获取文本框的值
    console.log("文本框对应的值为:", this.txtRef.current.value)
  }
  
  render() {
    return (
      <div>
        {/* 将创建好的ref对象添加到文本框中 */}
        <input type="text" ref={ this.txtRef } />
        <button onClick= { this.getTxt }>获取文本框的值</button>
      </div>
    )
  }
}

// 渲染组件
ReactDOM.render(<App />, document.querySelector('#root'))

综合案例

index.js

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入样式表
import './css/index.css'

/* 
  评论列表案例
*/
class App extends React.Component {
  // 初始化状态
  state = {
    comments: [
      { id: 1, name: 'jack', content: '沙发!!!' },
      { id: 2, name: 'rose', content: '板凳~' },
      { id: 3, name: 'tom', content: '楼主好人' }
    ],
    // 评论人
    userName: '',
    // 评论内容
    userContent: '',
  }
  renderList() {
    return this.state.comments.length === 0 ? (
           // 渲染暂无评论
           <div className="no-comment">暂无评论,快去评论吧~</div>
        ) : (
          // 渲染评论列表
          <ul>
            { this.state.comments.map(item => (
              <li key= {item.id}>
                <h3>评论人: { item.name }</h3>
                <p>评论内容: { item.content}</p>
              </li>
            )) }
          </ul>
        )
  }
  
  // 处理表单元素值
  handleChange = (e) => {
    // 获取当前表单元素的name属性和value
    const {name, value} = e.target
    // 更新state
    this.setState({
      [name]: value
    })
  }
  
  // 发表评论
  addComment = () => {
    const {comments, userContent, userName} = this.state
    // const newComments = [{
    //   id: Math.random(), name: userName, content: userContent
    // }, ...comments]
    
    // 非空校验
    if(userName.trim() === '' || userContent.trim() === '') {
      alert('请输入评论人和评论内容')
      return
    }
    
    // 复制comments
    const newComments = comments.concat()
    // 添加评论内容
    newComments.push({id: Math.random(), name: userName, content: userContent})
    
    // 更新state
    this.setState({
      comments: newComments,
      userName: '',
      userContent: '',
    })
  }
  
  render() {
    // 获取评论人和评论内容
    const {userName, userContent} = this.state
    return (
      <div className="app">
        <div>
          <input className="user" type="text" placeholder="请输入评论人" value= { userName } name="userName" 
          onChange= { this.handleChange } />
          <br />
          <textarea
            className="content"
            cols="30"
            rows="10"
            placeholder="请输入评论内容"
            value= { userContent }
            name="userContent"
            onChange= { this.handleChange }
          />
          <br />
          <button onClick= { this.addComment }>发表评论</button>
        </div>
        
        { /* 通过条件渲染决定渲染什么内容 */ }
        { this.renderList() }
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

index.css

.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;
}

React组件进阶

组件通讯介绍

组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据,在组件化过程中,我们将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能,而在这个过程中,多个组件不可避免的要共享某个数据,为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通,这个过程就是组件通讯

组件的props

  • 组件是封闭的,要接收外部数据应该通过props来实现
  • props的作用:接收传递给组件的数据
  • 传递数据:给组件标签添加属性
  • 接收数组:函数组件通过参数props接收数据,类组件通过this.props接收数据
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// props 函数组件接收数据通过props参数
const Hello = (props) => {
  // props是一个对象
  console.log(props)
  return ( 
    <div>
      <h1>props: {props.name} </h1>
    </div>
  )
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Hello name="jack" />)
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// props 类组件接收数据通过this.props
class Hello extends React.Component {
  render() {
    console.log(this.props)
    return (
      <div>
        <h1>props: {this.props.name}</h1>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Hello name="jack" />)

特点:

  1. 可以给组件传递任意类型的数据

  2. props是只读的对象

  3. 使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取props

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

class App extends React.Component {
  constructor(props) {
    super(props)
    
    console.log(props)
  }
  
  render() {
    console.log(this.props)
    return (
      <div>
        props: {this.props.name}
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App name="zs" />)

组件通讯的三种方式

父组件传递数据给子组件

  1. 父组件要提供传递给子组件的数据
  2. 给子组件标签添加属性,值为state中的数据
  3. 子组件通过props接收父组件中传递的数据
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import './css/my.css'

// 父组件
class Father extends React.Component {
  state = {
    lastName: '老王'
  }
  render() {
    return (
      <div className="father">
        父组件:
        <Child name = { this.state.lastName } />
      </div>
    )
  }
}

// 子组件
const Child = props => {
  return (
    <div className="child">
      <p>子组件接收父组件的数据:{ props.name }</p>
    </div>
  )
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Father />)

my.css文件内容

.father {
  height: 200px;
  background-color: blue;
}

.child {
  height: 100px;
  background-color: yellowgreen;
}

子组件传递数据给父组件

思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数

  1. 在父组件中提供一个回调函数,用于接收数据

  2. 将该函数作为属性的值,传递给子组件

  3. 子组件通过props调用回调函数

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import './css/my.css'

// 父组件
class Father extends React.Component {
  state = {
    lastName: '老王'
  }
    
  // 提供回调函数用来接收数据
  getChildMsg = (msg) => {
    console.log("接收子组件传递过来的数据", msg)
  }
  
  render() {
    return (
      <div className="father">
        父组件:
        <Child getMsg = { this.getChildMsg } />
      </div>
    )
  }
}

// 子组件
const Child = props => {
  return (
    <div className="child">
      <p>子组件传递父组件数据:{ props.getMsg('张三') }</p>
    </div>
  )
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 传递数据
root.render(<Father />)

兄弟组件

  • 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态

  • 思想:状态提升

  • 公共父组件职责:1. 提供共享状态、2. 提供操作共享状态的方法

  • 要通讯的子组件只需通过props接收状态或操作状态的方法

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// 父组件
class Counter extends React.Component {
  // 父组件提供共享状态
  state = {
    count: 0
  }
  
  // 父组件提供修改共享状态的方法
  addCount = () => {
    this.setState({
      count: this.state.count + 1
    })
  }
  
  render() {
    return (
      <div>
        { /* 传递数据 */ }
        <Child1 count = { this.state.count } />
        <Child2 addCount = { this.addCount } />
      </div>
    )
  }
}

/*
  兄弟关系
*/
// 子组件
const Child1 = props => {
  return <h1>计数器: { props.count }</h1>
}

// 子组件
const Child2 = (props) => {
  return <button onClick= { props.addCount }>+1</button>
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<Counter />)

Context的基本使用

作用:跨组件传递数据

使用步骤:

  1. 调用React.createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
const {Provider, Consumer} = React.createContext()
  1. 使用Provider组件作为父节点
<Provider>
    <div className="App">
    	<Child1 />
    </div>
</Provider>
  1. 设置value属性,表示要传递的数据
<Provider value="pink"></Provider>
  1. 调用Consumer组件接收数据
<Consumer>
    { (data) => {
        return <span>
               	data参数表示接收到的数据:{ data }
               </span>
    } }
</Consumer>

完成代码如下:

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import './css/my.css'

// Context

// 创建Context
const {Provider, Consumer} = React.createContext()

class App extends React.Component {
  render() {
    return (
      <Provider value="pink">
        <div className="App">
          <Node />
        </div>
      </Provider>
    )
  }
}

const Node = (props) => {
  return (
    <div className="Node">
      <SubNode />
    </div>
  )
}

const SubNode = (props) => {
  return (
    <div className="SubNode">
      <Child />
    </div>
  )
}

const Child = (props) => {
  return (
    <div className="Child">
      我是子节点<br />
      <Consumer>
          { (data) => {
              return <span>
                     	data参数表示接收到的数据:{ data }
                     </span>
          } }
      </Consumer>
    </div>
  )
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

my.css文件如下:

.App {
  height: 400px;
  background-color: blue;
  padding-top: 10px;
}

.Node {
  height: 200px;
  background-color: yellowgreen;
  padding-top: 10px;
}

.SubNode {
  height: 100px;
  background-color: red;
  padding-top: 10px;
}

.Child {
  height: 50px;
  background-color: pink;
  padding-top: 10px;
}

props深入

children属性

  • children属性表示组件标签的子节点,当组件标签有子节点时,props就会有该属性
  • children属性与普通的props一样,值可以是任意值
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// children属性
const App = (props) => {
  console.log(props)
  return (
    <div>
      <h1>组件标签的子节点:</ h1>
      { props.children }
    </div>
  )
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App>我是子节点</App>)

props校验

对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据

  • props校验:允许在创建组件的时候,就指定props的类型,格式等
  • 作用:捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性

使用步骤:

  1. 安装包prop-types (npm i prop-types)

  2. 导入prop-types包

  3. 使用组件名.propTypes = {}来给组件的props添加校验规则

  4. 校验规则通过PropTypes对象来指定

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import PropTypes from 'prop-types'

// props校验
const App = (props) => {
  const arr = props.colors
  const list = arr.map((item, index) => <li key= { index }>{ item }</li>)
  
  return <ul> {list} </ul>
}

// 添加props校验
App.propTypes = {
  colors: PropTypes.array
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App colors={ ['red', 'blue'] } />)

props校验–约束规则

  1. 常见类型:array,bool,func,number,object,string
  2. React元素类型:element
  3. 必填项:isRequired
  4. 特定结构的对象
App.propTypes = {
    // 常见类型
	optionalFunc: PropTypes.func,
	// 必填
	requiredFunc: PropTypes.func.isRequired,
	// 特定结构对象
	optionalObjectWithShape: PropTypes.shape({
    	color: PropTypes.string,
    	fontSize: PropTypes.number
	})
}

props的默认值

场景:分页组件 —》每页显示条数

作用:给props设置默认值,在未传入props时生效

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// props的默认值
const App = (props) => {
  console.log(props)
  return (
    <div>
      <h1>此处显示props的默认值:{ props.pageSize }</h1>
    </div>
  )
}

// 添加props默认值
App.defaultProps = {
  pageSize: 10
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
// 不传入数据
root.render(<App />)

组件的生命周期

组件的生命周期概述

  • 意义:组件的生命周期有助于理解组件的运行方式,完成更复杂的组件功能,分析组件错误原因
  • 组件的生命周期:组件从被创建到挂载到页面中运行,在到组件不用时卸载的过程
  • 生命周期的每个阶段总是伴随着一些方法调用,这些方法就是生命周期的钩子函数
  • 钩子函数的作用:为开发人员在不同阶段操作组件提供了时机
  • 只有类组件才有生命周期

生命周期的三个阶段

学习目标
  1. 每个阶段的执行时机
  2. 每个阶段钩子函数的执行顺序
  3. 每个阶段钩子函数的作用

在这里插入图片描述

创建时(挂载阶段)
  • 执行时机:组件创建时,页面加载时

  • 钩子函数执行顺序:constructor —> render —> componentDidMount

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KnYN7Gy3-1679816063715)(image/image-20230225141403470.png)]

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

/*
  组件生命周期
*/
// 创建时
class App extends React.Component {
  // 最先执行
  constructor(props) {
    super(props)
    console.warn("生命周期钩子函数 constructor")
    // let title = document.querySelector('#title')
    // console.log(title)
  }
  
  // 其次执行
  render() {
    console.warn("生命周期钩子函数 render")
    // let title = document.querySelector('#title')
    // console.log(title)
    return (
      <div>
        <h1 id="title">统计豆豆被打的次数:</h1>
        <button id="btn">打豆豆</button>
      </div>
    );
  }
  
  // 最后执行
  componentDidMount() {
    console.warn("生命周期钩子函数 componentDidMount")
    // let title = document.querySelector('#title')
    // console.log(title)
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
更新时(更新阶段)

执行时机:1. setState()、2. forceUpdate()、3. 组件接收到新的props

以上三者任意一种变化,组件就会重新渲染

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

/*
  组件生命周期
*/
// 更新时
class App extends React.Component {
  constructor(props) {
    super(props)
    
    this.state = {
      count: 0
    }
  }
  
  handleClick = () => {
    // this.setState({
    //   count: this.state.count + 1
    // })
    // 演示强制更新
    this.forceUpdate()
  }
  
  render() {
    console.log("生命周期钩子函数 render")
    return (
      <div>
        <Counter count = { this.state.count } />
        <button id="btn" onClick= { this.handleClick }>打豆豆</button>
      </div>
    );
  }
}

class Counter extends React.Component {
  render() {
    console.log('子组件钩子函数 render')
    return <h1 id="title">统计豆豆被打的次数:{ this.props.count }</h1>
  }
}


// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

钩子函数执行顺序:render —》componentDidUpdate

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Bk4l6an-1679816063716)(image/image-20230225213050643.png)]

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

/*
  组件生命周期
*/
// 更新时
class App extends React.Component {
  constructor(props) {
    super(props)
    
    this.state = {
      count: 0
    }
  }
  
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })
    // 演示强制更新
    // this.forceUpdate()
  }
  
  render() {
    return (
      <div>
        <Counter count = { this.state.count } />
        <button id="btn" onClick= { this.handleClick }>打豆豆</button>
      </div>
    );
  }
}

class Counter extends React.Component {
  render() {
    console.log('子组件钩子函数 render')
    return <h1 id="title">统计豆豆被打的次数:{ this.props.count }</h1>
  }
  componentDidUpdate(prevProps) {
    console.log('子组件钩子函数 componentDidUpdate')
    console.log('上一次的props', prevProps, '当前的props', this.props)
    // 常用方法
    if(prevProps.count !== this.props.count) {
      // 如果要修改state
      this.setState({})
    }
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
卸载时(卸载阶段)
  • 执行时机:组件从页面中消失

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SycyjT7C-1679816063717)(image/image-20230226112508981.png)]

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

/*
  组件生命周期
*/
// 卸载时
class App extends React.Component {
  constructor(props) {
    super(props)
    
    this.state = {
      count: 0
    }
  }
  
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })
  }
  
  render() {
    return (
      <div>
        {this.state.count > 3 ? <p>豆豆被打死了</p> : <Counter count = { this.state.count } />}
        <button id="btn" onClick= { this.handleClick }>打豆豆</button>
      </div>
    );
  }
}

class Counter extends React.Component {
  componentDidMount() {
    this.timerId = setInterval(() => {
      console.log("定时器启动")
    }, 1000)
  }
  render() {
    return <h1 id="title">统计豆豆被打的次数:{ this.props.count }</h1>
  }
  componentWillUnmount() {
    clearInterval(this.timerId)
    console.log('钩子函数 componentWillUnmount')
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

render-props和高阶组件

React组件复用概述

思考:如果两个组件的部分功能相似或相同,该如何处理

处理方式:复用相似的功能(联想函数封装)

复用什么?

  1. state
  2. 操作state的方法(组件状态逻辑)

实现复用的两种方式:

  1. render props模式
  2. 高阶组件模式(HOC)

这两种模式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式

render props模式

思路分析:将要复用的state和操作state的方法封装到一个组件中

问题1:如何拿到组件中复用的state

  • 在使用组件时,添加一个值为函数的prop,通过函数参数来获取(需要组件内部实现)

问题2:如何渲染任意的UI

  • 使用该函数的返回值作为要渲染的UI内容
使用步骤
  1. 创建Mouse组件,在组件中提供复用的状态逻辑代码(1. 状态、2. 操作状态的代码)
  2. 将要复用的状态作为props.render(state)方法的参数,暴露到组件外部
  3. 使用props.render()的返回值作为要渲染的内容
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

/*
  render props模式
*/

// 1.创建Mouse组件
class Mouse extends React.Component {
  state = {
    // 鼠标横坐标
    x: 0,
    // 鼠标纵坐标
    y: 0
  }
  
  // 当鼠标移动改变state值使其等于鼠标坐标
  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY,
    })
  }
  
  // 当组件创建并渲染DOM后
  componentDidMount() {
    // 添加鼠标移动事件
    window.addEventListener("mousemove", this.handleMouseMove)
  }
  
  // 暴露
  render() {
    // return null
    return this.props.render(this.state)
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props 模式</h1>
        <Mouse render = { (mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p> } />
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
演示组件复用
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入图片
import image from './image/cat.png'

/*
  render props模式
*/

// 1.创建Mouse组件
class Mouse extends React.Component {
  state = {
    // 鼠标横坐标
    x: 0,
    // 鼠标纵坐标
    y: 0
  }
  
  // 当鼠标移动改变state值使其等于鼠标坐标
  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY,
    })
  }
  
  // 当组件创建并渲染DOM后
  componentDidMount() {
    // 添加鼠标移动事件
    window.addEventListener("mousemove", this.handleMouseMove)
  }
  
  // 暴露
  render() {
    // return null
    return this.props.render(this.state)
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props 模式</h1>
        <Mouse render = { (mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p> } />
        <Mouse render = { (mouse) => {
            return <img src={ image } alt="猫" style={{
              position: 'absolute',
              top: mouse.y - 64,
              left: mouse.x - 64
            }} /> 
          }
        } />
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
children代替render属性
  • 注意:并不是该模式叫render props就必须使用名为render的prop,实际上可以使用任意名称的prop
  • 把prop是一个函数并且告诉组件要渲染什么内容的技术叫做render props 模式
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入图片
import image from './image/cat.png'

/*
  render props模式
*/

// 1.创建Mouse组件
class Mouse extends React.Component {
  state = {
    // 鼠标横坐标
    x: 0,
    // 鼠标纵坐标
    y: 0
  }
  
  // 当鼠标移动改变state值使其等于鼠标坐标
  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY,
    })
  }
  
  // 当组件创建并渲染DOM后
  componentDidMount() {
    // 添加鼠标移动事件
    window.addEventListener("mousemove", this.handleMouseMove)
  }
  
  // 暴露
  render() {
    // return null
    return this.props.children(this.state)
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props 模式</h1>
        <Mouse>{(mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p>}</Mouse>
        <Mouse>{(mouse) => {
            return <img src={ image } alt="猫" style={{
              position: 'absolute',
              top: mouse.y - 64,
              left: mouse.x - 64
            }} /> 
          }}</Mouse>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
代码优化
  1. 推荐:给render props添加props校验
  2. 应该在组件卸载时解除mousemove事件绑定
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
// 导入prop-types
import PropTypes from 'prop-types'
// 导入图片
import image from './image/cat.png'

/*
  render props模式
*/

// 1.创建Mouse组件
class Mouse extends React.Component {
  state = {
    // 鼠标横坐标
    x: 0,
    // 鼠标纵坐标
    y: 0
  }
  
  // 当鼠标移动改变state值使其等于鼠标坐标
  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY,
    })
  }
  
  // 当组件创建并渲染DOM后
  componentDidMount() {
    // 添加鼠标移动事件
    window.addEventListener("mousemove", this.handleMouseMove)
  }
  
  // 当组件卸载时移除事件绑定
  componentWillUnmount() {
    window.removeEventListener('mousemove', this.handleMouseMove)
  }
  
  // 暴露
  render() {
    // return null
    return this.props.children(this.state)
  }
}

// 添加props校验
Mouse.propTypes = {
  children: PropTypes.func.isRequired
}

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props 模式</h1>
        <Mouse>{(mouse) => <p>鼠标位置: X:{ mouse.x }, Y:{ mouse.y }</p>}</Mouse>
        <Mouse>{(mouse) => {
            return <img src={ image } alt="猫" style={{
              position: 'absolute',
              top: mouse.y - 64,
              left: mouse.x - 64
            }} /> 
          }}</Mouse>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

高阶组件

高阶组件概述

目的:实现状态逻辑复用

采用包装(装饰)模式,通过包装组件,增强组件功能

  • 高阶组件是一个函数,接收要包装的组件,返回包装后的组件
  • 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给被包装组件
使用步骤
  1. 创建一个函数,名称约定with开头
  2. 指定函数参数,参数应该以大写字母开头,作为要渲染的组件
  3. 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
  4. 在该组件中渲染参数组件,同时将状态通过prop传递给参数组件
  5. 调用该高阶组件,传入要增强的组件,通过返回值,拿到增强后的组件,并将其渲染到页面中
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

/*
  高阶组件
*/

// 创建高阶组件
function withMouse(WrappedComponent) {
  
  // 提供复用的逻辑
  class Mouse extends React.Component {
    // 鼠标坐标
    state = {
      x: 0,
      y: 0,
    }
    
    handleMouseMove = e => this.setState({x: e.clientX, y: e.clientY})
    
    componentDidMount() {window.addEventListener('mousemove', this.handleMouseMove)}
    
    componentWillUnmount() {window.removeEventListener('mousemove', this.handleMouseMove)}
    
    render() {
      return <WrappedComponent {...this.state} />
    }
  }
  return Mouse
}

// 用来测试高阶组件
const Position = props => {
  console.log(props)
  return <p>鼠标当前位置:(X:{props.x}, Y:{props.y})</p>
}

const MousePosition = withMouse(Position)

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件:<MousePosition /></h1>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
设置displayName

使用高阶组件存在的问题:得到的两个组件名称相同

原因:默认情况下,React使用组件名称作为displayName

解决方式:为高阶组件设置displayName便于调试时区分不同的组件

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import image from './image/cat.png'

/*
  高阶组件
*/

// 创建高阶组件
function withMouse(WrappedComponent) {
  
  // 提供复用的逻辑
  class Mouse extends React.Component {
    // 鼠标坐标
    state = {
      x: 0,
      y: 0,
    }
    
    handleMouseMove = e => this.setState({x: e.clientX, y: e.clientY})
    
    componentDidMount() {window.addEventListener('mousemove', this.handleMouseMove)}
    
    componentWillUnmount() {window.removeEventListener('mousemove', this.handleMouseMove)}
    
    render() {
      return <WrappedComponent {...this.state} />
    }
  }
  // 设置displayName
  Mouse.displayName = `withMouse${getDisPlayName(WrappedComponent)}`
  
  return Mouse
}

// 设置displayName
function getDisPlayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

// 用来测试高阶组件
const Position = props => {
  console.log(props)
  return <p>鼠标当前位置:(X:{props.x}, Y:{props.y})</p>
}

const Cat = props => {
  return <img src={image} alt="猫" style = {
    {
      position: 'absolute',
      top: props.y - 64,
      left: props.x - 64
    }
  } />
}

// 增强组件(包装组件)
const MousePosition = withMouse(Position)
const MouseCat = withMouse(Cat)

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件:<MousePosition /></h1>
        <MouseCat />
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
高阶组件传递props

问题:有props丢失

原因:高阶组件没有向下传递props

解决方式:渲染WrappedComponent时,将state和props一起传递给组件

传递方式如下:

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'
import image from './image/cat.png'

/*
  高阶组件
*/

// 创建高阶组件
function withMouse(WrappedComponent) {
  
  // 提供复用的逻辑
  class Mouse extends React.Component {
    // 鼠标坐标
    state = {
      x: 0,
      y: 0,
    }
    
    handleMouseMove = e => this.setState({x: e.clientX, y: e.clientY})
    
    componentDidMount() {window.addEventListener('mousemove', this.handleMouseMove)}
    
    componentWillUnmount() {window.removeEventListener('mousemove', this.handleMouseMove)}
    
    render() {
      // 传递props
      return <WrappedComponent {...this.state} {...this.props} />
    }
  }
  // 设置displayName
  Mouse.displayName = `withMouse${getDisPlayName(WrappedComponent)}`
  
  return Mouse
}

// 设置displayName
function getDisPlayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

// 用来测试高阶组件
const Position = props => {
  console.log(props)
  return <p>鼠标当前位置:(X:{props.x}, Y:{props.y})</p>
}

const Cat = props => {
  return <img src={image} alt="猫" style = {
    {
      position: 'absolute',
      top: props.y - 64,
      left: props.x - 64
    }
  } />
}

// 增强组件(包装组件)
const MousePosition = withMouse(Position)
const MouseCat = withMouse(Cat)

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件:<MousePosition a="1" /></h1>
        <MouseCat />
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

React原理揭秘

setState()的说明

更新数据

  • setState是异步更新数据的

  • 使用该语法时,后面的setState不要依赖于前面的setState

  • 如果setState调用多次,只会触发一次render()

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// setState是异步更新数据的
class App extends React.Component {
  state = {
    count: 0
  }
  handleCount = () => {
    // 异步更新
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count) // 0
  }
  render() {
    return (
      <div>
        <h1>计数器: {this.state.count}</h1>
        <button onClick = {this.handleCount}>+1</button>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

推荐语法

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// setState是异步更新数据的
class App extends React.Component {
  state = {
    count: 0
  }
  handleCount = () => {
    // 异步更新
    // 推荐语法
    // 第一个参数是最新的state
    // 第二个参数是最新的props
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    })
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    })
    console.log(this.state.count)
  }
  render() {
    return (
      <div>
        <h1>计数器: {this.state.count}</h1>
        <button onClick = {this.handleCount}>+1</button>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// setState是异步更新数据的
class App extends React.Component {
  state = {
    count: 0
  }
  handleCount = () => {
    // 异步更新
    // 推荐语法
    this.setState((state, props)=> {
      return {
        count: state.count + 1
      }
    })
    this.setState((state, props)=> {
      return {
        count: state.count + 1
      }
    }, () => {
      console.log('状态更新完成', this.state.count)
      console.log(document.querySelector('#title').innerText)
    })
    console.log(this.state.count)
  }
  render() {
    return (
      <div>
        <h1 id="title">计数器: {this.state.count}</h1>
        <button onClick = {this.handleCount}>+1</button>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)

JSX语法转化过程

  • JSX仅仅是createElement()方法的语法糖(简化语法)
  • JSX语法被@babel/preset-react插件编译为createElement()方法
  • React元素是一个对象,用来描述你希望在屏幕上看到的内容

组件更新机制

  • 父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树(当前组件及其所有子组件)

组件性能优化

减轻state

  • 减轻state:只存储跟组件渲染相关的数据

  • 不用做渲染的数据不要放在state中,比如定时器id等

  • 对于这种需要在多个方法中用到的数据,应该放在this中

避免不必要的重新渲染

  • 组件更新机制:父组件更新会引起子树更新,这种思路清晰

  • 问题:子组件没有任何变化时也会重新渲染

  • 如何避免不必要的重新渲染呢?

  • 解决方式:使用钩子函数shouldComponentUpdate(nextProps, nextState)

  • 作用:通过该钩子函数的返回值决定是否重新渲染组件,true表示需要重新渲染,反之则不需要重新渲染

  • 触发时机:更新阶段的钩子函数,组件更新渲染前执行

// 导入react
import React from 'react'
import { createRoot } from 'react-dom/client'

// 避免不必要的重新渲染
class App extends React.Component {
  state = {
    count: 0
  }
  
  handleCount = () => {
    this.setState((state, props)=> {
      return {
        count: state.count + 1
      }
    })
  }
  
  shouldComponentUpdate(nextProps, nextState) {
    // 最新状态
    console.log('最新的state:', nextState)
    // 更新前的状态,props亦然
    console.log(this.state)
    // 避免组件重新渲染
    return false;
  }
  
  render() {
    return (
      <div>
        <h1 id="title">计数器: {this.state.count}</h1>
        <button onClick = {this.handleCount}>+1</button>
      </div>
    )
  }
}

// 渲染组件
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
案例:随机数
import React from 'react'
import {createRoot} from 'react-dom/client'

/**
 * 生成随机数
 */
class App extends React.Component {
  state = {
    number: 0
  }
  
  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3)
      }
    })
  }
  
  // 因为两次生成的随机数可能相同,如果相同则不重新渲染组件
  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextState.number)
    console.log(this.state.number)
    // return this.state.number === nextState.number ? false : true
    return !(this.state.number === nextState.number)
  }
  
  render() {
    console.log('render')
    return (
      <div>
        <h1>随机数:{ this.state.number }</h1>
        <button onClick = { this.handleClick }>重新生成</button>
      </div>
    )
  }
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
import React from 'react'
import {createRoot} from 'react-dom/client'

/**
 * 生成随机数
 */
class App extends React.Component {
  state = {
    number: 0
  }
  
  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3)
      }
    })
  }
  
  // 因为两次生成的随机数可能相同,如果相同则不重新渲染组件
  // shouldComponentUpdate(nextProps, nextState) {
  //   console.log(nextState.number)
  //   console.log(this.state.number)
  //   // return this.state.number === nextState.number ? false : true
  //   return !(this.state.number === nextState.number)
  // }
  
  render() {
    // console.log('render')
    return (
      <div>
        <NumberBox number = { this.state.number } />
        <button onClick = { this.handleClick }>重新生成</button>
      </div>
    )
  }
}

class NumberBox extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    console.log(nextProps.number)
    console.log(this.props.number)
    // return this.props.number === nextProps.number ? false : true
    return !(this.props.number === nextProps.number)
  }
  render() {
    console.log('render')
    return <h1>随机数:{ this.props.number }</h1>
  }
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)

纯组件

  • 纯组件:PureComponent与React.Component功能相似
  • 区别:PureComponent内部自动实现了shouldComponentUpdate钩子,不需要手动比较
  • 原理:纯组件内部通过分别对比前后两次props和state的值,来决定是否重新渲染组件
import React from 'react'
import {createRoot} from 'react-dom/client'

/**
 * 生成随机数--纯组件
 */
class App extends React.PureComponent {
  state = {
    number: 0
  }
  
  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3)
      }
    })
  }
  
  render() {
    console.log('render')
    return (
      <div>
        <h1>随机数:{ this.state.number }</h1>
        <button onClick = { this.handleClick }>重新生成</button>
      </div>
    )
  }
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
  • 说明:纯组件内部的对比是shallow compare(浅层对比)
import React from 'react'
import {createRoot} from 'react-dom/client'

/**
 * 生成随机数
 */
class App extends React.PureComponent {
  state = {
    obj: {
      number: 0
    }
  }
  
  handleClick = () => {
    // const newObj = this.state.obj
    // newObj.number = Math.floor(Math.random() * 3)
    // console.log(newObj)
    // console.log(this.state.obj)
    // this.setState(() => {
    //   return {
    //     obj: newObj
    //   }
    // })
    const newObj = {...this.state.obj, number: Math.floor(Math.random() * 3)}
    this.setState(() => {
      return {
        obj: newObj
      }
    })
  }
  
  render() {
    console.log('render')
    return (
      <div>
        <h1>随机数:{ this.state.obj.number }</h1>
        <button onClick = { this.handleClick }>重新生成</button>
      </div>
    )
  }
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)

虚拟DOM和Diff算法

  • React更新视图的思想是:只要state变化就重新渲染视图
  • 特点:思路清晰
  • 问题:组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染到页面中?不是
  • 理想状态:只更新变化的内容
  • 问题:React是如何做到部分更新的?虚拟DOM和Diff算法配合
虚拟DOM
  • 本质上就是一个js对象,用来描述你希望在屏幕上看到的内容
Diff算法
  • 初次渲染时,React会根据初始的state,创建一个虚拟DOM对象
  • 根据虚拟DOM生成真正的DOM树,渲染到页面中
  • 当数据变化后,重新根据新的数据,创建新的虚拟DOM对象
  • 与上一次得到的虚拟DOM对象使用Diff算法进行对比(找不同),得到需要更新的内容
  • 最终,React只将变化的内容更新到DOM中,重新渲染到页面

React路由基础

React路由介绍

现在的前端应用大多都是SPA(单页应用程序),也就是只有一个HTML页面的应用程序,因为它的用户体验更好,对服务器的压力更小,所以更受欢迎,为了有效的使用单个页面来管理原来多页面的功能,前端路由应运而生

  • 前端路由的功能:让用户从一个视图导航到另一个视图
  • 前端路由是一套映射规则,在React中,是URL路径与组件的对应关系
  • 使用React路由简单来说,就是配置路径和组件(配对)

路由的基本使用

使用步骤

  1. 安装:npm i react-router-dom
  2. 导入路由的三个核心组件:Router/Route/Link
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
  1. 使用Router组件包裹整个应用

  2. 使用Link组件作为导航菜单(路由入口)

  3. 使用Route组件配置路由规则和要展示的组件(路由出口)

import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'

const First = () => {
  return (
    <div>页面一的页面内容</div>
  )
}
const App = () => {
  return (
    <Router>
      <div>
        <h1>React路由基础</h1>
        <Link to="/first">页面一</Link>
        <Route path="/first" component = { First }></Route>
      </div>
    </Router>
  )
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)

新版本(V6)有所变化:情况如下

import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
// V6多了一个Routes
import {BrowserRouter as Router, Route, Routes, Link} from 'react-router-dom'

const First = () => {
  return (
    <div>页面一的页面内容</div>
  )
}
const Second = () => {
  return (
    <div>页面二的页面内容</div>
  )
}
const App = () => {
  return (
    <Router>
      <div>
        <h1>React路由基础</h1>
        <Link to="/first">页面一</Link>
        <Link to="/second" >页面二</Link>
        {/* V6必须使用Routes包裹Route */}
        <Routes>
          <Route path="/first" element =  { <First /> } ></Route>
          <Route path="/second" element= {<Second />} ></Route>
        </Routes>
      </div>
    </Router>
  )
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)
  1. 在react-router-dom的V6版本中必须使用Routes组件包裹Route组件
  2. Route组件的component属性替换为element属性,并使用标签的形式进行组件渲染
  3. 下面代码中index的作用是标记路由为索引路由,访问父组件时会同时展示该路由,如果我们想达到路由继承的目的,则在父组件中使用占位组件(Outlet组件),将子组件的内容渲染到父组件中
<Routes>
  <Route path="/" element={ <AppLayout /> }>
    <Route index element={ <Home /> }></Route>
    <Route path="user" element={ <User /> }></Route>
    <Route path="/user/detail/:id" element={ <Detail /> } />
  </Route>
  <Route path="/login" element={ <Login /> } />
</Routes>
  1. 在下面代码中可以实现路由跳转
import { Button } from 'antd'
import { useNavigate } from 'react-router-dom'
function Login() {
    const navigate = useNavigate()
    const login = () => {
        navigate("/")
    }
    return (
    	<div>
        	<Button type="primary" onClick = { login }>登录</Button>
        </div>
    )
}
  1. 下面代码是将路由地址作为参数获取的方式
import { useParams } from 'react-router-dom'

function Detail() {
    const params = useParams()
    return (
    	<div>
        	user detail - { params.id }
        </div>
    )
}
<Route path="/user/detail/:id" element={ <Detail /> } />
<Link to="/user/detail/1">用户1</Link>
  1. 集中式路由的渲染方式
import { RouteObject } from 'react-router-dom'
const router: RouterObject[] = [
    {
        path: "/",
        element: <AppLayout />,
        children: [
        	{index: true, element: <Home />},
    		{path: "user", element: <User />},
            {path: "/user/detail/:id", element: <Detail />}
        ]
    },
    {path: "/login", element: <Login />}
]
// 导入上面写的代码
import router from './router'
import { useRoutes } from 'react-router-dom'
function App() {
    const element = useRoutes(router)
    return element
}
  1. 页面懒加载
import { lazy } from 'react'
// 导入Home组件
import Home from './home'
// 让Home组件懒加载
const Home = lazy(() => import('./home'))
import router from './router'
import { useRoutes } from 'react-router-dom'
import {Suspense} from 'react'
function App() {
    const element = (
    	<Suspense fallback={<>loading</>}>
        	useRoutes(router)
        </Suspense>
    )
    return element
}

常用组件说明

  • BrowserRouter组件:包裹整个应用,一个React应用只需要使用一次,使用H5的history API实现
  • HashRouter组件:使用URL的哈希值实现,与BrowserRouter的作用一样
  • Link组件:用于指定导航链接(a标签)
  • NavLink组件:与Link组件功能一样,但方便修改样式
  • Route组件:指定路由展示组件相关信息

路由执行过程

  • 点击Link组件(a标签),使得浏览器地址栏的URL发生改变
  • React路由监听到地址栏URL的变化
  • React路由内部会遍历所有Route组件,使用路由规则(path)与pathname进行匹配
  • 当路由规则path能够与地址栏中的pathname匹配时,就展示该Route组件的内容

编程式导航

  • 场景:点击登录按钮,登录成功后,通过代码跳转到后台首页,如何实现?

  • 编程式导航:通过JS代码来实现页面跳转

  • history是React路由提供的,用于获取浏览器历史记录的相关信息

  • push(path):跳转到某个页面,参数path表示要跳转的路径

  • go(n):前进或后退到某个页面,参数n表示前进或后退页面数量

import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'

class Login extends React.Component {
  handleLogin = () => {
    console.log(this)
    // 使用编程式导航实现路由跳转
    this.props.history.push("/home")
  }
  render() {
    return (
      <div>
        <p>登录页面</p>
        <button onClick = { this.handleLogin }>登录</button>
      </div>
    )
  }
}

const Home = (props) => 
  <div>
    <h2>我是后台首页</h2>
    <button onClick= {() => {
      props.history.go(-1)
    }}>返回登录页面</button>
  </div>

const App = () => (
  <Router>
    <div>
      <h1>编程式导航</h1>
      <Link to="/login">去登录页面</Link>
      <Route path="/login" component = { Login }></Route>
      <Route path="/home" component = { Home  }></Route>
    </div>
  </Router>
)

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)

默认路由

  • 问题:现在的路由都是点击导航菜单栏后展示的,如何在进入页面的时候就展示呢?

  • 默认路由:表示进入页面是就会匹配的路由

  • 默认路由path为"/"

<Route path="/" component= {Home}></Route>
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'

const Home = () => <p>进入页面的时候你能看到我吗?</p>

const App = () => {
  return (
    <Router>
      <div>
        <h1>默认路由</h1>
        <Route path="/" component= {Home}></Route>
      </div>
    </Router>
  )
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)

匹配模式

模糊匹配模式

  • 问题:当Link组件的to属性值为"/login"时,为什么默认路由也被匹配成功

  • 回答:默认情况下,React的路由是模糊匹配模式

  • 模糊匹配规则:只要pathname(Link组件的to属性)以path(Route组件的path属性)开头就会匹配成功

import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'

const Home = () => <p>进入页面的时候你能看到我吗?</p>

const Login = () => <p>我是Login组件的内容</p>

const App = () => {
  return (
    <Router>
      <div>
        <h1>默认路由</h1>
        <Link to="/login">登录页面</Link>
        {/* 模糊匹配模式默认的 */}
        {/*只要to以path开头就能匹配成功*/}
        <Route path="/" component={Home}></Route>
        <Route path="/login" component={Login}></Route>
      </div>
    </Router>
  )
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)

精确匹配模式

  • 问题:默认路由任何情况下都会展示,如何避免这种问题
  • 给Route组件添加exact属性,让其变为精确匹配模式
  • 精确匹配规则:只有当pathname和path完全一致时才会展示该路由(v18默认该模式)
import React from 'react'
import {createRoot} from 'react-dom/client'
// 导入react-router-dom三个核心包
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'

const Home = () => <p>进入页面的时候你能看到我吗?</p>

const Login = () => <p>我是Login组件的内容</p>

const App = () => {
  return (
    <Router>
      <div>
        <h1>默认路由</h1>
        <ul>
          <li>
            <Link to="/">首页</Link>
          </li>
          <li>
            <Link to="/login">登录页面</Link>
          </li>
        </ul>
        {/* 精确匹配 */}
        <Route exact path="/" component={Home} />
        <Route path="/login" component={Login} />
      </div>
    </Router>
  )
}

const container = document.getElementById('root')
const root = createRoot(container)
root.render(<App />)

React路由基础总结

  1. React路由可以有效的管理多个视图(组件)实现SPA
  2. Router(BrowserRouter)组件包裹整个应用,只需要使用一次
  3. Link组件是入口,Route组件是出口
  4. 通过props.history实现编程式导航
  5. 默认模糊匹配,添加exact变精确匹配
  6. React路由的一切都是组件,可以像思考组件一样思考路由
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

草莓小子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值