React学习日志

React学习日志

一、React 基础

1、React 概述

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

MVC角度,React 仅仅是视图层(V)

特点

  • 声明式
  • 基于组件
  • 学习一次,随处使用

2、React 基本使用

2.1 React 的安装

npm i react react-dom
  • react 包是核心,提供创建元素、组件等功能
  • react-dom 包提供 DOM 相关功能

2.2 React 的基本使用

  <div id="root"></div>

  <!-- 1 引入 js 文件 -->
  <script src="./node_modules/react/umd/react.development.js"></script>
  <script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
  <script>
    // 2 创建 react 元素
    // 参数:元素名称、元素属性、元素子节点(第三个及其以后得子节点)
    // const title = React.createElement('h1', null, 'Hello react !')
    const title = React.createElement(
      'p',
      { title: '我是标题', id: 'p1' },
      'Hello react !',
      React.createElement('span', null, '我是span节点')
    )

    // 3 渲染 react 元素
    // 参数:要渲染的react元素、挂载点
    ReactDOM.render(title, document.getElementById('root'))
  </script>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2Bm27ts-1692620120415)(D:\PerfectCode\前端学习\React\Image\创建元素.PNG)]

3、React 脚手架的使用

3.1 React 脚手架意义

  1. 脚手架是开发 现代 Web 应用的必备
  2. 充分利用 Webpack、Babel、ESLint 等工具辅助项目开发
  3. 零配置,无需手动配置繁琐的工具即可使用
  4. 关注业务,而不是工具配置

3.2 使用 React 脚手架初始化项目

  1. 初始化项目:npx create-react-app 项目名称

  2. 启动项目,在项目根目录中执行:npm start

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hmQdorfo-1692620120416)(D:\PerfectCode\前端学习\React\Image\npx.PNG)]

特别

还可以用 npm init react-app my-appyarn create react-app my-app 创建

3.3 在脚手架中使用 React

  1. 导入 react 和 react-dom 两个包
import React from 'react'
import ReactDOM from 'react-dom'
  1. 调用 React.createElement() 方法创建 react 元素
  2. 调用 ReactDOM.render() 方法渲染 react 元素到页面中

特别地(最新版)

  1. 导入 react 和 react-dom 两个包
import React from 'react'
import ReactDOM from 'react-dom/client'
  1. 调用 ReactDOM.createRoot(document.getElementById(‘root’)); 获取 root 节点

  2. 使用 JSX 语法创建 react 元素

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

3.4 最新版

网址:https://reactjs.org/link/switch-to-createroot

import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) 使用 TypeScript 语法
root.render(<App tab="home" />);

脚手架中:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

二、JSX

1、JSX 的基本使用

1.1 createElement() 的问题

  1. 繁琐不简洁
  2. 不直观
  3. 不优雅,用户体验不爽

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nvVvSpJw-1692620120417)(D:\PerfectCode\前端学习\React\Image\createElement问题.PNG)]

1.2 JSX 简介

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

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

1.3 使用步骤

// 1 导入 react
import React from 'react';
import ReactDOM from 'react-dom';
// import ReactDOM from 'react-dom/client';

// 最新版
// const root = ReactDOM.createRoot(document.elementById('root'))

// 2 使用 JSX 语法创建 react 元素
const title = (
  <h1 className="title">Hello JSX 
    <span>这是span</span>
    <p/>
  </h1>
);

// 3 渲染 react 元素
ReactDOM.render(title, document.getElementById('root'));

// 最新版
root.render(title)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQ5z0ZYh-1692620120417)(D:\PerfectCode\前端学习\React\Image\为什么可以使用JSX.PNG)]

1.4 注意点

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

  2. 特殊属性名:calss -> calssName、for -> htmlFor、tabindex -> tabIndex

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

  4. 推荐:使用 小括号包裹 JSX,从而避免 JS 中的自动插入分层陷阱

const dv = (
  <div className="hello"> 
     Hello JSX 
     <p />
  </div>
)

2、JSX 中使用 JavaScript 表达式

嵌入 JS 表达式

  • 数据存储在 JS 中
  • 语法:{ JavaScript 表达式 }
  • 注意:语法中是 单大括号,不是爽大括号!
const name = '张三';
const p = <p>这是p</p>;
const sayHi = () => 'Hi~'
const title = (
  <h2 className="title">Hello JSX ,
    <span>这是span,{name}</span>
    {p}, {1}, {'abc'}
    {3>5?'大于':'小于'}
    <p/>
    {sayHi()}
  </h2>
);

注意点

  • 单大括号 中可是使用任意的 JavaScript 表达式
  • JSX 吱声也是 JS 表达式
  • 注意:JS 中的对象是一个例外,一般只会出现在 style 属性中
  • 注意:不能再 {} 中出现语句(比如:if/for 等)
    {/* 错误演示 */}
    {/* <p>{ {a: '6'} }</p> */}
    {/* { if (true) { <p>这是if</p> } } */}
    {/* { for (let i = 0; i < 10; i++) { <p>这是for</p> } } */}

3、JSX 的条件渲染

  • 场景:loading效果
  • 条件渲染:根据条件渲染特定的 JSX 结构
  • 可以使用 if/else三元表达式逻辑与运算符来实现
const isLoading = true;
const loadData = () => {
  // if else方式
  // if (isLoading) {
  //   return <div>loading...</div>
  // }
  // return <div>data...</div>

  // 三元表达式
  return isLoading ? <div>loading...</div> : <div>data...</div>

  // 逻辑与
  // return isLoading && <div>loading...</div>
}

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

4、JSX 的列表渲染

  • 如果要渲染一组数据,应该使用数组的 map() 方法
  • 注意:渲染列表时应该添加 key 属性,key 属性的值要保证唯一
  • 原则:map() 遍历谁,就给谁添加 key 属性
  • 注意:尽量避免使用索引号作为 key
const songs = [
  { id: 1, name: '歌曲1' },
  { id: 2, name: '歌曲2' },
  { id: 3, name: '歌曲3' }
]

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

5、JSX 的样式处理

5.1 行内样式——style

<h2 style={{ color:'red', textAlign: 'center' }}>JSX style 样式</h2>

5.2 类名——className ( 推荐 )

<h2 className="title">JSX className 样式</h2>

注意:要提前引入 css 样式文件

三、React 组件基础

1、React 组件介绍

  • 组件是 React 的 一等公民,使用 React 就是在用组件
  • 组件表示页面中的部分功能
  • 组合多个组件实现完整的页面功能
  • 特点:可复用 、独立、可组合

2、React 组件的两种创建方式

2.1 使用函数创建组件

  • 函数组件:使用 JS 的函数(或箭头函数)创建的组件
  • 约定1:函数名称必须以 大写字母开头,React 据此区分 组件 和 普通的 React 元素
  • 约定2:函数组件 必须有返回值 ,表示该组件的结构
  • 如果返回值为 null ,表示不渲染任何内容
// function HelloMessage(props) {
//     return <h1>Hello World!</h1>;
// }

// function HelloMessage(props) {
//   return null
// }

const HelloMessage = (props) => <h1>Hello World!</h1>
  • 渲染函数组件:用函数名作为组件标签名
  • 组件标签可以是单标签也可以是双标签
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<HelloMessage />);

可以使用嵌套组件的方式或者**<React.StrictMode></React.StrictMode>**来同时渲染多个组件

2.2 使用类创建组件

  • 类组件:使用 ES6 的 class 创建组件
  • 约定1:类名称也必须以 大写字母开头
  • 约定2:类组件应该继承 React.Component 父类,从而可以使用父类中提供的方法或属性
  • 约定3:类组件必须提供 render() 方法
  • 约定4:render() 方法 必须有返回值,表示该组件的结构
  • render() 方法如果返回值为 null ,表示不渲染任何内容
// 类组件
class Hello extends React.Component {
  render() {
    return <div>我是一个类组件</div>;
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<HelloMessage />);
root.render(<React.StrictMode>
              <HelloMessage />
              <Hello />
            </React.StrictMode>)

可以使用嵌套组件的方式或者**<React.StrictMode></React.StrictMode>**来同时渲染多个组件

2.3 抽离为独立 JS 文件

  1. 创建 Hello.js 组件
  2. 在 Hello.js 中导入 React
  3. 创建组件(函数 或者 类)
  4. 在 Hello.js 中导出该组件
  5. 在 index.js 中导入 Hello 组件
  6. 渲染组件
// 导入React
import React from 'react';

// 创建组件
class Hello1 extends React.Component {
  render() {
    return (
      <div>我是一个类组件,这是第一个抽离到js文件中的组件</div>
    )
  }
}

// 导出组件
export default Hello1

//-----------------index.js-------------------
// 导入组件
import Hello1 from './Hello';
const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<HelloMessage />);
root.render(<React.StrictMode>
              <Hello1 />
            </React.StrictMode>)

3、React 事件处理

3.1 事件绑定

  • React 事件绑定语法与 DOM 事件语法相似
  • 语法:on + 事件名称 = {事件处理程序},比如:onClick = {() => {}}
  • 注意:React 事件采用驼峰命名法,比如:onMouseEnter、onFocus
  • 函数和类中略有不同
// 在类组件中绑定事件处理程序
class App1 extends React.Component {
  // 事件处理程序
  handleClick() {
    console.log('点击了按钮');
  }
  render() {
    return <button onClick={this.handleClick}>点击</button>;
  }
}

// 在函数组件中绑定事件处理程序
const App2 = () => {
  function handleClick() {
    console.log('点击了按钮');
  }
  return <button onClick={handleClick}>点击</button>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<HelloMessage />);
root.render(<React.StrictMode>
              <App1 />
              <App2 />
            </React.StrictMode>)

3.2 事件对象

  • 可以通过 事件处理程序的参数 获取到事件对象
  • React 中的事件对象叫做:合成事件(对象)
  • 合成事件:兼容所有浏览器,无须担心跨浏览器兼容性问题
// 事件对象
class App3 extends React.Component {
  handleClick(e) {
    e.preventDefault();
    console.log(e.target.href);
  }
  render() {
    return (
      <a href="https://www.baidu.com" onClick={this.handleClick} style={{textDecoration: 'none', color: 'green'}}>点击</a>
    );
  }
}

4、有状态组件和无状态组件

  • 函数组件又叫做 无状态组件,类组件又叫做 有状态组件
  • 状态(state)即 数据
  • 函数组件没有自己的状态,只负责 数据展示(静)
  • 类组件有自己的状态,负责更新UI,让页面 “动” 起来

5、组件中的 state 和 setState()

5.1 state 的基本使用

  • 状态(state)即数据,是组件内部的 私有 数据,只能在组件内部使用
  • state 的值是对象,表示一个组件仲可以有多个数据
  • 状态是私有的,只能在组件内部使用
  • 通过 this.state 来获取状态
class App4 extends React.Component {
/*   constructor() {
    super()
    // 初始化 state
    this.state = {
      count: 0
    }
  } */

  // 简化写法
  state = {
    count: 0
  }

  render() {
    return (
      <div>
        <h2>计数器:{ this.state.count }</h2>
      </div>
    )
  }
}

5.2 setState() 修改状态

  • 状态是可改变的
  • 语法:this.setState({ 要修改的数据 })
  • 注意:不要直接修改 state 中的值,这是错误的!!!
  • setState() 作用:1.修改 state 2.更新 UI
  • 思想:数据驱动视图
// state 的基本使用
class App4 extends React.Component {
/*   constructor() {
    super()
    // 初始化 state
    this.state = {
      count: 0
    }
  } */
  // 简化写法
  state = {
    count: 0
  }

  render() {
    return (
      <div>
        <h2>计数器:{ this.state.count }</h2>
        <button onClick={() => {
          this.setState({
            count: this.state.count + 1
          })
        }}>+1</button>
      </div>
    )
  }
}

5.3 从 JSX 中抽离事件处理程序

  • JSX 中掺杂过多 JS 逻辑代码,会显得非常混乱
  • 推荐:将逻辑抽离到单独的方法中,保证 JSX 结构清晰

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMlzZMIZ-1692620120418)(D:\PerfectCode\前端学习\React\Image\this报错.PNG)]

  • 原因:事件处理程序中的 this 的值为 undefined
  • 希望:this 指向组件实例(render 方法中的 this,即为组件实例)
// state 的基本使用
class App4 extends React.Component {
/*   constructor() {
    super()
    // 初始化 state
    this.state = {
      count: 0
    }
  } */
  // 简化写法
  state = {
    count: 0
  }

  // 事件处理程序
  onIncrement() {
    console.log('事件处理程序中的this:', this) // undefined
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <div>
        <h2>计数器:{ this.state.count }</h2>
        {/* <button onClick={() => {
          this.setState({
            count: this.state.count + 1
          })
        }}>+1</button> */}
        <button onClick={this.onIncrement.bind(this)}>+1</button>
        {/* 不加bind(this)会报错,因为这个this指向事件处理函数,而不是render */}
      </div>
    )
  }
}

6、事件绑定 this 指向

6.1 箭头函数

  • 利用箭头函数自身不绑定 this 的特点
  • render()方法中的 this 为组件实例,可以获取到 setState()
  • 可以为标签绑定 ref 属性,并通过 this.refs.属性名 来操作dom
// 事件处理程序
onIncrement() {
    this.setState({ ... })
}
render () {
    return (
        {/* 1. 箭头函数解决 */}
        <button onClick={() => this.onIncrement()}>+1</button>
    )
}

6.2 Function.prototype.bind()

  • 利用 ES5 中的 bind 方法,将事件处理程序中的 this 与组件实例绑定到一起
  • 两种使用方法
class App4 extends React.Component {
  constructor() {
    super()
      
    // 事件处理程序中的this指向问题
    this.onIncrement = this.onIncrement.bind(this)
      
    // 初始化 state
    this.state = {
      count: 0
    }
  }

  // 事件处理程序
  onIncrement() {
    console.log('事件处理程序中的this:', this) // undefined
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <div>
        <h2>计数器:{ this.state.count }</h2>
        {/* 2. bind 方法 */}
        {/* 直接调用 */}
        {/* <button onClick={this.onIncrement.bind(this)}>+1</button> */}
        {/* 提前绑定 */}
        <button onClick={this.onIncrement}>+1</button>
      </div>
    )
  }
}

6.3 class 的实例方法(推荐)

  • 利用箭头函数形式的 class 实例方法
  • 注意:该语法是实验性语法,但是,由于 babel 的存在可以直接使用
class App4 extends React.Component { 
  // 初始化 state,简写
  state = {
      count: 0
  }

  // 事件处理程序,箭头函数解决this
  onIncrement = () => {
    console.log('事件处理程序中的this:', this) // undefined
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <div>
        <h2>计数器:{ this.state.count }</h2>
        {/* 3. 箭头函数解决 */}
        <button onClick={this.onIncrement}>+1</button>
      </div>
    )
  }
}

7、表单处理

7.1 受控组件

  • HTML 中的表单元素是可输入的,也就是有自己的可变状态
  • 而,React 中可变状态通常保存在 state 中,并且只能通过 setState() 方法来修改
  • React 将 state 与表单元素值 value 绑定到一起,由 state 的值来控制表单元素的值
  • 受控组件:其值受到 React 控制的表单元素

步骤

  1. 在 state 中添加一个状态,作为表单元素的 value 值(控制表单元素值的来源)
  2. 给表单元素绑定 change 事件,将表单元素的值 设置为 state 的值(控制表单元素值的变化)
state = { val: '' }
render() {
    return (
      <div>
        <input type="text" value={this.state.val} onChange={this.handleChange} />
        <p>{this.state.val}</p>
      </div>
    )
  }

例子

// 受控组件
class App5 extends React.Component {
  state = {
    val: '',
    content: '',
    city: '2',
    isChecked: true
  }
  handleChange = (e) => {
    this.setState({
      val: e.target.value
    })
  }
  handleContent = (e) => {
    this.setState({
      content: e.target.value
    })
  }
  handleCity = (e) => {
    this.setState({
      city: e.target.value
    })
  }
  handleChecked = (e) => {
    this.setState({
      isChecked: e.target.checked
    })
  }
  render() {
    return (
      <div>
        {/* 文本框 */}
        <input type="text" value={this.state.val} onChange={this.handleChange} />
        <p>{this.state.val}</p>
        <br />
        {/* 富文本框 */}
        <textarea value={this.state.content} onChange={this.handleContent} />
        <p>{this.state.content}</p>
        {/* 下拉框 */}
        <select value={this.state.city} onChange={this.handleCity}>
          <option value="1">北京</option>
          <option value="2">上海</option>
          <option value="3">广州</option>
          <option value="4">深圳</option>
        </select>
        {/* 复选框 */}
        <input type="checkbox" checked={this.state.isChecked} onChange={this.handleChecked} />
      </div>
    )
  }
}

多表单元素优化

  1. 给表单元素添加 name 属性,名称与 state 相同
  2. 根据表单元素类型获取对应的值
  3. 在 change 事件处理程序中通过 [name] 来修改对应的state
// 受控组件
class App5 extends React.Component {
  state = {
    val: '',
    content: '',
    city: '2',
    isChecked: true
  }
    
  handleForm = (e) => {
    // 获取当前DOM对象
    const target = e.target
    // 根据类型获取值
    const value = target.type === 'checkbox' ? target.checked : target.value
    // 获取name
    const name = target.name

    // es6 属性表达式
    this.setState({
      [name]: value
    })
  }

  render() {
    return (
      <div>
        {/* 文本框 */}
        <input type="text" name='val' value={this.state.val} onChange={this.handleForm} />
        <p>{this.state.val}</p>
        <br />
        {/* 富文本框 */}
        <textarea name='content' value={this.state.content} onChange={this.handleForm} />
        <p>{this.state.content}</p>
        {/* 下拉框 */}
        <select name='city' value={this.state.city} onChange={this.handleForm}>
          <option value="1">北京</option>
          <option value="2">上海</option>
          <option value="3">广州</option>
          <option value="4">深圳</option>
        </select>
        {/* 复选框 */}
        <input type="checkbox" name='isChecked' checked={this.state.isChecked} onChange={this.handleForm} />
      </div>
    )
  }
}

7.2 非受控组件(DOM方式)

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

使用步骤

  1. 调用 React.createRef() 方法创建一个 ref 对象
constructor() {
    super()
    this.txtRef = React.createRef()
}
  1. 将创建好的 ref 对象添加到文本框中
<input type="text" ref={this.txtRef} />
  1. 通过 ref 对象获取到文本框的值
console.log(this.txtRef.current.value)
class App6 extends React.Component {
  constructor() {
    super()
    // 创建 ref
    this.txtRef = React.createRef()
  }
  // 获取文本框的值
  getTxt = () => {
    console.log('文本框的值:', this.txtRef.current.value)
  }
  render() {
    return (
      <div>
        <input type="text" ref={this.txtRef} />
        <button onClick={this.getTxt}>获取文本框的值</button>
      </div>
    )
  }
}

7.3 评论列表案例

import React from 'react';

class App extends React.Component  {
  // 初始化状态
  state = {
    // 评论列表
    comments: [
      {id: 1, name: '张三', content: '今天天气真好'},
      {id: 2, name: '李四', content: '今天天气真好'},
      {id: 3, name: '王五', content: '今天天气真好'},
      {id: 4, name: '赵六', content: '今天天气真好'},
      {id: 5, name: '田七', content: '今天天气真好'},
    ],
    // 评论人
    userName: '',
    // 评论内容
    userContent: ''
  }
  // 通过函数来渲染
  renderList() { 
    const { comments } = this.state;
    if (comments.length > 0) {
      return (
        <ul>
          { this.state.comments.map(item => (
            <li key={item.id}>
              <h3>评论人:{item.name}</h3>
              <p>评论内容:{item.content}</p>
            </li>
          ))}
        </ul>
      )
    }
    return <div className='no-comment'>暂无评论,快去评论吧~</div>
    
    // return (
    //   this.state.comments.length > 0
    //   ? <ul>
    //       { this.state.comments.map(item => (
    //         <li key={item.id}>
    //           <h3>评论人:{item.name}</h3>
    //           <p>评论内容:{item.content}</p>
    //         </li>
    //       ))}
    //     </ul>
    //   : <div className='no-comment'>暂无评论,快去评论吧~</div>
    // )
  }

  // 受控表单元素值
  handleForm = e => {
    const { name, value } = e.target;
    this.setState({
      [name]: value
    })
  }

  // 添加评论
  addComment = () => {
    // 获取评论人和评论内容
    const { comments, userName, userContent } = this.state;
    
    // 判断是否为空
    if (!userName.trim() || !userContent.trim()) {
      alert('评论人或评论内容不能为空');
      return;
    }

    // 生成评论对象
    const newComment = {
      id: Date.now(),
      name: userName,
      content: userContent
    }
    // 更新状态, 同时清空输入框
    this.setState({
      comments: [newComment, ...comments],
      userName: '',
      userContent: ''
    })
  }

  render() {
    const { userName, userContent } = this.state;
    return (
      <div>
        <div>
          <input type='text' placeholder='请输入评论人' value={userName} name='userName' onChange={this.handleForm} />
          <br />
          <textarea cols='30' rows='10' placeholder='请输入评论内容' value={userContent} name='userContent' onChange={this.handleForm} />
          <br />
          <button onClick={this.addComment}>发表评论</button>
        </div>
        {/* 通过条件渲染来判断 */}
        {/* {
          this.state.comments.length > 0
          ? <ul>
              { this.state.comments.map(item => (
                <li key={item.id}>
                  <h3>评论人:{item.name}</h3>
                  <p>评论内容:{item.content}</p>
                </li>
              ))}
            </ul>
          : <div className='no-comment'>暂无评论,快去评论吧~</div>
        }  */}
        {/* 通过函数来渲染 */}
        {this.renderList()}
      </div>
    )
  }
}

export default App;

四、React 组件进阶

1、组件通讯介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gs21hZG9-1692620120418)(D:\PerfectCode\前端学习\React\Image\组件通讯.PNG)]

2、组件的props

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

/* // 函数组件props
function Hello(props) {
  return <h2>Hello, {props.name}--{props.age}</h2>;
} */

// 类组件props
class Hello extends React.Component {
  render() {
    return <h2>Hello, {this.props.name}--{this.props.age}</h2>;
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Hello name='zs' age={23} />
  </React.StrictMode>
);

特点

  1. 可以给组件传递任意类型的数据
  2. props 是只读的对象,只能读取属性的值,无法修改对象
  3. 注意:使用类组件时,如果写了构造函数,应该将 props 传递给 super(),否则,无法在构造函数中获取到 props!
/* // 函数组件props
function Hello(props) {
  return <h2>Hello, {props.name}--{props.age}</h2>;
} */

// 类组件props
class Hello extends React.Component {
  // 推荐在写类组件时,构造函数要携带props参数
  constructor(props) {
    super(props);
    console.log(props);
  }
  render() {
    console.log('props: ', this.props);
    return (
      <div>
        <h2>Hello, {this.props.name}--{this.props.age}</h2>
        {this.props.tag}
      </div>
    )
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Hello
      name='zs'
      age={23} 
      colors={['red', 'green']} 
      fn={() => console.log('这是一个函数')}
      tag={<p>这是一个p标签</p>}
    />
  </React.StrictMode>
);

3、组件通讯的三种方式

3.1 父组件传递数据给子组件

  1. 父组件提供要传递的 state 数据
  2. 给子组件标签添加属性,值为 state 中的数据
  3. 子组件通过 props 接收父组件中传递的数据
// 父组件
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      msg: 'hello!'
    }
  }
  render() {
    return (
      <div>
        <h2>父组件</h2>
        <Child data={this.state.msg} />
      </div>
    )
  }
}

// 子组件
const Child = (props) => {
  return (
    <div>
      <h3>子组件,接收到父组件的数据:{props.data}</h3>
    </div>
  )
}

3.2 子组件传递数据给父组件

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

  1. 父组件提供一个回调函数(用于接收数据)
  2. 将该函数作为属性的值,传递给子组件
// 父组件
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      msg: 'hello!',
      parentMsg: ''
    }
  }
  // 提供回调函数,用来接收数据
  getChildData = (data) => {
    console.log('父组件接收到子组件的数据:', data)
    this.setState({
      parentMsg: data
    })
  }
  render() {
    return (
      <div>
        <h2>父组件</h2>
        <h3>父组件接收到子组件的数据:{this.state.parentMsg}</h3>
        <Child1 getMsg={this.getChildData} />
      </div>
    )
  }
}

// 子组件
class Child1 extends React.Component {
  state = {
    msg: 'hello! hello!'
  }
  handleClick = () => {
    // 调用父组件传递过来的回调函数,将数据传递给父组件
    this.props.getMsg(this.state.msg)
  }
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>点击向父组件传递数据</button>
      </div>
    )
  }
}

3.3 兄弟组件传递数据

  • 共享状态提升到最近的公共组件中,有公共父组件管理这个状态
  • 思想:状态提升
  • 公共父组件职责:1. 提供共享状态 2. 提供操作共享状态的方法
  • 要通讯的子组件只需要通过 props 接收状态或操作状态的方法
import React from "react";

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

const Child1 = (props) => {
  return <h2>计数器:{props.count}</h2>
}

const Child2 = (props) => {
  return <button onClick={() => props.onIncrement()}>+1</button>
}

export default Counter;

4、Context

实现跨组件传递数据

使用步骤

  1. 调用 React.createContext() 创建 Provider (提供数据)和 Consumer(消费数据)两个组件
  2. 使用 Provider 组件作为父节点
  3. 设置 value 属性,表示要传递的数据
  4. 调用 Consumer 组件接收数据,函数参数接收
import React from "react";

// 创建 context 得到两个组件
const { Provider, Consumer } = React.createContext('default')

class Children extends React.Component {
  render () {
    return (
      // Provider 传递 value
      <Provider value="pink">
        <div>
          <h1>Parent ----- </h1>
          <Node />
        </div>
      </Provider>
    )
  }
}

const Node = props => {
  return (
    <div>
      <h2>Node -------- </h2>
      <SubNode />
    </div>
  )
}

const SubNode = props => {
  return (
    <div>
      <h3>SubNode -------- </h3>
      <Child />
    </div>
  )
}

const Child = props => {
  return (
    <div>
      <h4>Child ------ 
        {/* Consumer 消费 value */}
        <Consumer>
          { data => <span style={{color: data}}> {data} </span> }
        </Consumer>
      </h4>
    </div>
  )
}

export default Children

5、props 深入

5.1 children 属性

  • children 属性:表示组件标签的子节点。当组件标签有子节点时,props 就会有该属性
  • children 属性与普通的 props 一样,值可以是任意值(文本、React元素、组件,甚至是函数)
const AppChildren = props => {
  console.log('props: ', props);
  return (
    <div>
      <h1>
        组件标签子节点:{props.children}
      </h1>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <AppChildren>我是文本子节点</AppChildren>
    <AppChildren><button>我是一个标签子节点</button></AppChildren>
    {/* <AppChildren>{() => {console.log('我是一个函数子节点');}}</AppChildren> */}
  </React.StrictMode>
);

5.2 props 校验

  • 对于组件来说,props 是外来的,无法保证组件使用者传入什么格式的数据
  • props 校验:允许在创建组件的时候,就指定 props 的类型、格式等
  • 作用:捕获使用组件时因为 props 导致的错误,给出明确的错误提示,增加组件的健壮性

使用步骤

  1. 安装包 prop-types(yarn add prop-types / npm i prop-types)
  2. 导入 prop-types 包
  3. 使用 组件名.propTypes = {} 来给组件的 props 添加检验规则
  4. 校验规则 通过 PropTypes 对象来指定
// 导入 PropTypes
import PropTypes from 'prop-types';

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

// 添加校验规则
AppYz.propTypes = {
  colors: PropTypes.array
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <AppYz colors={['red', 'green']} />
  </React.StrictMode>
);

约束规则

  1. 常见类型:array、bool、func、number、object、string
  2. React 元素类型:element
  3. 必选项:isRequired
  4. 特定结构对象:shape({})
const App1 = props => {
  return (
    <div>
      <h1>props校验: </h1>
    </div>
  )
}

App1.propTypes = {
  a: PropTypes.number,
  fn: PropTypes.func.isRequired,
  tag: PropTypes.element,
  filter: PropTypes.shape({ // 指定为对象结构
    area: PropTypes.string,
    price: PropTypes.number
  }),
}

5.3 props 的默认值

  • 场景:分页组件 --> 每页显示条数
  • 作用:给 props 设置默认值,在未传入 props 时生效
const App2 = props => {
  return (
    <div>
      <h1>props校验: {props.pageSize}</h1>
    </div>
  )
}

// 设置默认值
App2.defaultProps = {
  pageSize: 10,
}

6、组件的生命周期

6.1 组件的生命周期概述

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

6.2 生命周期的三个阶段

注意

组件的一次更新流程,在视图真正刷新之前的部分都是可能被多次调用的,因而这些部分中不能出现副作用,开发环境下会刻意触发两次以使得开发者能注意到误用的副作用。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g3ladAO9-1692620120418)(D:\PerfectCode\前端学习\React\Image\React生命周期.PNG)]

  • 执行顺序:

constructor() --> render() --> componentDidMount

钩子函数触发时机作用
constructor创建组件时,最先执行1.初始化state
2.为事件处理程序绑定 this
render每次组件渲染都会触发渲染 UI ( 注意:不能直接调用 setState()
componentDIdMount组件挂载(完成DOM渲染)后1.发送网络请求
2.DOM 操作
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    // 处理this指向
    // this.handleClick = this.handleClick.bind(this);

    console.warn('生命周期函数:constructor');
  }
  componentDidMount() {
    const btn = document.querySelector('#btn');
    console.log(btn);
    console.warn('生命周期函数:componentDidMount');
  }
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })

    // 强制更新
    // this.forceUpdate()
  }
  render() {
    console.warn('生命周期函数:render');

    // 不要再render中调用setState,会造成死循环
    // this.setState({
    //   count: this.state.count + 1
    // })

    return (
      <div>
        <Counter count={this.state.count} />
        <button id="btn" onClick={this.handleClick}>打豆豆</button>
      </div>
    )
  }
}
  1. 更新时(更新阶段)
  • 执行时机:1.setState() 2.forceUpdate() 3.组件接收到新的 props
  • 说明:以上三者任意一种变化,组件就会重新渲染
  • 执行顺序:render() --> componentDidUpdate()
钩子函数触发时机作用
render每次组件渲染都会触发渲染UI(与 挂载阶段 是同一个render)
componentDidUpdate组件更新(完成DOM渲染)后1.发送网络请求
2.DOM 操作
注意:如果要setState()必须放在一个 if 条件中
class Counter extends React.Component {
  render() {
    console.warn('生命周期钩子函数:Counter的 render');
    return <h1 id='title'>统计豆豆被打的次数:{this.props.count}</h1>
  }
  // 注意:如果要调用 setState,必须放在一个条件判断中
  // 否则会造成死循环
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.warn('生命周期钩子函数:Counter的 componentDidUpdate');
    const title = document.querySelector('#title');
    console.log(title);

    // 比较更新前后的props是否相同,来决定是否重新渲染组件
    console.log('上一次的props:', prevProps);
    console.log('当前的props:', this.props);
    if (prevProps.count !== this.props.count) {
      this.setState({})
      // 发送ajax请求
    }
  }
}
  1. 卸载时(卸载阶段)
  • 触发时机:组件从页面中消失
钩子函数触发时机作用
componentWillUnmount组件卸载(从页面消失)执行清理工作(比如:清理定时器等)
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    // 处理this指向
    // this.handleClick = this.handleClick.bind(this);

    console.warn('生命周期函数:constructor');
  }
  componentDidMount() {
    const btn = document.querySelector('#btn');
    console.log(btn);
    console.warn('生命周期函数:componentDidMount');
  }
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })

    // 强制更新
    // this.forceUpdate()
  }
  render() {
    console.warn('生命周期函数:render');

    // 不要再render中调用setState,会造成死循环
    // this.setState({
    //   count: this.state.count + 1
    // })

    return (
      <div>
        { this.state.count > 3 ? <h1>豆豆被打死了~</h1> : <Counter count={this.state.count} /> }
        <button id="btn" onClick={this.handleClick}>打豆豆</button>
      </div>
    )
  }
}

class Counter extends React.Component {
  componentDidMount() {
    // 开启定时器
    this.timerId = setInterval(() => {
      console.log('定时器正在执行啦~')
    }, 500)
  }
  render() {
    console.warn('生命周期钩子函数:Counter的 render');
    return <h1 id='title'>统计豆豆被打的次数:{this.props.count}</h1>
  }
  // 注意:如果要调用 setState,必须放在一个条件判断中
  // 否则会造成死循环
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.warn('生命周期钩子函数:Counter的 componentDidUpdate');
    const title = document.querySelector('#title');
    console.log(title);

    // 比较更新前后的props是否相同,来决定是否重新渲染组件
    console.log('上一次的props:', prevProps);
    console.log('当前的props:', this.props);
    if (prevProps.count !== this.props.count) {
      this.setState({})
      // 发送ajax请求
    }
  }

  // 组件卸载之前调用生命周期函数
  componentWillUnmount() {
    console.warn('生命周期钩子函数:Counter的 componentWillUnmount');
    // 清理定时器
    clearInterval(this.timerId)
    console.log('定时器被清空了!');
  }
}

6.3 不常用的钩子函数介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fa59FRSZ-1692620120419)(D:\PerfectCode\前端学习\React\Image\React生命周期图.png)]

注意:componentWillMount、componentWillReceiveProps、componentWillUpdate已经被弃用,不推荐使用

新版

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PYvAFaaI-1692620120419)(D:\PerfectCode\前端学习\React\Image\React新版生命周期图.png)]

getDerivedStateFromProps、getSnapshotBeforeUpdate不推荐使用

7、render-props 和高阶组件

7.1 React组件复用概述

  • 处理方式:复用相似功能
  • 复用什么:1.state 2.操作 state 的方法(组件状态逻辑)
  • 两种方式:1. render props 模式 2.高阶组件(HOC)
  • 注意:这两种方式 不是新的API,而是利用 React 自身特点的编码技巧,演化而成的固定模式(写法)

7.2 render props 模式

思路分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DraeQJtJ-1692620120419)(D:\PerfectCode\前端学习\React\Image\render props思路.PNG)]

使用步骤

  1. 创建 mouse 组件,在组件中提供复用的状态逻辑代码(1.状态 2.操作状态的方法)
  2. 将要 复用的状态 作为 props.render(state) 方法的参数,暴露到组件外部
  3. 使用 props.render() 的 返回值 作为要渲染的内容
import React from 'react';

/* 
  render props 模式
*/

// 创建 Mouse 组件
class Mouse extends React.Component {
  // 鼠标位置 state
  state = {
    x: 0,
    y: 0
  }

  // 监听鼠标移动事件
  componentDidMount() {
    window.addEventListener('mousemove', this.handleMouseMove)
  }

  // 鼠标移动事件处理程序
  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY
    })
  }

  render() {
    // return null
    return this.props.render(this.state)
  }
}

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

        {/* 猫捉老鼠 */}
        <Mouse render={mouse => {
          return <img src="https://www.baidu.com/img/bd_logo1.png" alt="猫" style={{ position: 'absolute', left: mouse.x - 240, top: mouse.y - 180 }} />
        }} />
      </div>
    );
  }
}

export default App;

children 代替 render 属性

  • 注意:并不是该模式叫 render props 就必须使用名为 render 的 prop,实际上可以使用任意名称的 prop
  • 把 prop 是一个函数并且告诉组件要渲染什么内容的技术叫做:renderprops 模式
  • 推荐:使用 children 代替 render 属性
import React from 'react';

/* 
  render props 模式
*/

// 创建 Mouse 组件
class Mouse extends React.Component {
  // 鼠标位置 state
  state = {
    x: 0,
    y: 0
  }

  // 监听鼠标移动事件
  componentDidMount() {
    window.addEventListener('mousemove', this.handleMouseMove)
  }

  // 鼠标移动事件处理程序
  handleMouseMove = (e) => {
    this.setState({
      x: e.clientX,
      y: e.clientY
    })
  }

  render() {
    // return null
    // return this.props.render(this.state)
    return this.props.children(this.state)
  }
}

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

        {/* 猫捉老鼠 */}
        {/* <Mouse render={mouse => {
          return <img src="https://www.baidu.com/img/bd_logo1.png" alt="猫" style={{ position: 'absolute', left: mouse.x - 240, top: mouse.y - 180 }} />
        }} /> */}
        {/* children 方式 */}
        <Mouse>
          {mouse => {
            return <img src="https://www.baidu.com/img/bd_logo1.png" alt="猫" style={{ position: 'absolute', left: mouse.x - 240, top: mouse.y - 180 }} />
          }}
        </Mouse>
      </div>
    );
  }
}

export default App;

代码优化

  1. 推荐:给 render props 模式添加 props 校验
// 添加 props 校验
Mouse.propTypes = {
  children: PropTypes.func.isRequired
}
  1. 应该在组件卸载时解除 mousemove 事件绑定
  // 在 Mouse 组件卸载时,移除事件监听
  componentWillUnmount() {
    window.removeEventListener('mousemove', this.handleMouseMove)
  }

7.3 高阶组件

概述

  • 目的:实现状态逻辑使用
  • 采用 包装(装饰)模式,比如说:手机壳
  • 高阶组件就相当于手机壳,通过包装组件,增强组件功能

思路分析

  • 高阶组件(HOC,Higher-OrderComponent)是一个函数,接受要包装的组件,返回增强后的组件
  • 高阶组件内部 创建一个类组件,在这个类组件中 提供复用的状态逻辑 代码,通过 prop 将复用的状态传递给被包装组件 WrappedComponent

使用步骤

  1. 创建一个函数,名称约定 以 with 开头
  2. 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
  3. 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
  4. 再该组件中,渲染参数组件,同时将状态通过 prop 传递给参数组件
  5. 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
import React from 'react';

/* 
  高阶组件
*/

// 创建高阶组件
function withMouse(WrappedComponent) {
  // 该组件提供复用的状态逻辑
  class Mouse extends React.Component {
    // 鼠标状态
    state = {
      x: 0,
      y: 0
    }

    // 监听鼠标移动事件
    componentDidMount() {
      window.addEventListener('mousemove', this.handleMouseMove)
    }

    // 鼠标移动事件处理程序
    handleMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY
      })
    }

    // 组件卸载时,移除事件监听
    componentWillUnmount() {
      window.removeEventListener('mousemove', this.handleMouseMove)
    }

    render() {
      return (
        <WrappedComponent {...this.state} />
      )
    }
  }

  // 返回增强后的组件
  return Mouse
}

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

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

// 获取增强后的组件
const MousePosition = withMouse(Position)

// 调用高阶组件来增强猫捉老鼠组件
const MouseCat = withMouse(Cat)

class ComponentGao extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件</h1>
        {/* 渲染增强后的组件 */}
        <MousePosition />
        <MouseCat />
      </div>
    )
  }
}

export default ComponentGao;

设置 diaplayName

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

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

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

  • displayName的作用:用于设置调试信息(React Developer Tools 信息)

  • 设置方式:

      // 设置高阶组件的名称 displayName
      Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
      
    function getDisplayName(WrappedComponent) {
        return WrappedComponent.displayName || WrappedComponent.name || 'Component'
    }
    

传递 props

  • 问题:props 丢失

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

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

  • 传递方式:

    <WrappedComponent {...this.state} {...this.props} />
    
import React from 'react';

/* 
  高阶组件
*/

// 创建高阶组件
function withMouse(WrappedComponent) {
  // 该组件提供复用的状态逻辑
  class Mouse extends React.Component {
    // 鼠标状态
    state = {
      x: 0,
      y: 0
    }

    // 监听鼠标移动事件
    componentDidMount() {
      window.addEventListener('mousemove', this.handleMouseMove)
    }

    // 鼠标移动事件处理程序
    handleMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY
      })
    }

    // 组件卸载时,移除事件监听
    componentWillUnmount() {
      window.removeEventListener('mousemove', this.handleMouseMove)
    }

    render() {
      console.log('Mouse', this.props);
      return (
        <WrappedComponent {...this.state} {...this.props} />
      )
    }
  }

  // 设置高阶组件的名称 displayName
  Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`

  // 返回增强后的组件
  return Mouse
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

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

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

// 获取增强后的组件
const MousePosition = withMouse(Position)

// 调用高阶组件来增强猫捉老鼠组件
const MouseCat = withMouse(Cat)

class ComponentGao extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件</h1>
        {/* 渲染增强后的组件 */}
        <MousePosition a="1" />
        <MouseCat />
      </div>
    )
  }
}

export default ComponentGao;

五、React 原理

1、setState() 说明

1.1 更新数据

  • setState() 是 异步 更新数据的
  • 注意:使用该语法时,后面的 setState() 不要依赖于前面的 setState();(也就是合并成一个 setState() )
  • 可以多次调用 setState(),只会触发一次重新渲染

1.2 推荐语法

  • 推荐:使用 setState( (state,props) => {}) 语法
  • 参数 state:表示最新的 state
  • 参数 props:表示最新的 props
import React from 'react';

class App extends React.Component {
  state = {
    count: 1
  }
  handleClick = () => {
    // 此处,更新 state
    /* this.setState({
      count: this.state.count + 1 // 1
    })
    this.setState({
      count: this.state.count + 1 // 1 + 1
    }) */

    // 推荐语法
    // 异步的
    this.setState((state, props) => {
      return {
        count: state.count + 1 // 1 + 1
      }
    })
    // 第二次调用 setState 时,会使用上一次的 state
    this.setState((state, props) => {
      console.log('第二次调用: ', state.count) // 2
      return {
        count: state.count + 1 // 2 + 1
      }
    })
    console.log('count: ', this.state.count) // 1 拿到的还是上一次的 state
  }
  render() {
    return (
      <div>
        <p>count: {this.state.count}</p>
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}

export default App;

1.3 第二个参数

  • 场景:在状态更新(页面完成重新渲染)后立即执行某个操作
  • 语法:setState(updater[,callback])
    this.setState((state, props) => {
      console.log('第二次调用: ', state.count) // 2
      return {
        count: state.count + 1 // 2 + 1
      }
    }, () => { // 回调函数,状态更新完成后执行
      console.log('状态更新完成: ', this.state.count);
      console.log(document.getElementById('title').innerHTML);
      document.title = '更新完成'
    })

2、JSX 语法的转化过程

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iXLgvJPd-1692620120420)(D:\PerfectCode\前端学习\React\Image\JSX语法.PNG)]

3、组件更新机制

  • setState() 两个作用:1.修改 state 2.更新组件(UI)
  • 过程:父组件重新渲染时,也会重新渲染子组件。但只会渲染 当前组件子树 (当前组件及其所有子组件)

4、组件性能优化

4.1 减轻 state

  • 减轻 state:只存储根组件渲染相关的数据(比如:count/列表数据/loading等)
  • 注意:不用做渲染的数据不要放在 state 中,比如定时器 id 等
  • 对于这种需要多个方法中用到的数据,应该放在 this 中

4.2 避免不必要的重新渲染

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

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

  • 解决方式:使用 **钩子函数 shouldComponentUpdate(nextProps,nextState)**两个参数表示最新的内容

  • 作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染

  • 触发时机:更新阶段的钩子函数,组件重新渲染前执行(shouldComponentUpdate --> render)

    class Hello extends Component {
        shouldComponentUpdate() {
            //  根据条件,决定是否重新渲染组件
            return false
        }
        render() {...}
    }
    
  • 案例:随机数

    import React from 'react';
    
    /* 
      组件性能优化
    */
    
    // 生成随机数
    class App1 extends React.Component {
      state = {
        num: 0
      }
      handleClick = () => {
        this.setState(() => {
          return {
            num: Math.floor(Math.random() * 3)
          }
        })
      }
    
      // 1. 因为两次生成的随机数可能是一样的,所以需要优化,利用nextState参数判断
      shouldComponentUpdate(nextProps, nextState) {
        console.log('最新状态:', nextState, '当前状态:', this.state);
        /* if (nextState.num === this.state.num) {
          return false
        }
        return true */
        return nextState.num !== this.state.num
      }
    
      render() {
        console.log('App1 render');
        return (
          <div>
            <NumBox num={this.state.num} />
            <button onClick={this.handleClick}>生成随机数</button>
          </div>
        )
      }
    }
    
    class NumBox extends React.Component {
      // 2. 优化,利用nextProps参数判断
      shouldComponentUpdate(nextProps, nextState) {
        return nextProps.num !== this.props.num
      }
      render() {
        console.log('NumBox render');
        return (
          <div>
            <h2>随机数:{this.props.num}</h2>
          </div>
        )
      }
    }
    
    export default App1;
    
    

4.3 纯组件

  • 纯组件:PureComponent 与 React.Component 功能相似

  • 区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子函数,不需要手动比较

  • 原理:纯组件内部通过分别 对比 前后量词 props 和 state 的值,来决定是否重新渲染组件

    import React from 'react';
    
    /* 
      组件性能优化
    */
    
    // 生成随机数
    class App1 extends React.PureComponent {
      state = {
        num: 0
      }
      handleClick = () => {
        this.setState(() => {
          return {
            num: Math.floor(Math.random() * 3)
          }
        })
      }
    
      render() {
        console.log('App2 render');
        return (
          <div>
            <NumBox num={this.state.num} />
            <button onClick={this.handleClick}>生成随机数</button>
          </div>
        )
      }
    }
    
    class NumBox extends React.PureComponent {
      render() {
        console.log('NumBox render');
        return (
          <div>
            <h2>随机数:{this.props.num}</h2>
          </div>
        )
      }
    }
    
    export default App1;
    
    
  • 说明:纯组件内部的对比是 shallow compare(浅层对比)

  • 对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)

  • 对于 引用类型 来说:只比较对象的引用(地址)是否相同

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OmupZmtW-1692620120420)(D:\PerfectCode\前端学习\React\Image\纯组件原理.PNG)]

  • 注意:state 或 props 中的属性值为引用类型时,应该创建新数据,不要直接原数据!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jleQPIY9-1692620120420)(D:\PerfectCode\前端学习\React\Image\纯组件引用类型做法.PNG)]

import React from 'react';

/* 
  组件性能优化
*/

// 生成随机数
class App3 extends React.PureComponent {
  state = {
    obj: {
      num: 0
    }
  }
  handleClick = () => {
    // 正确做法:创建一个新对象,修改新对象中的数据
    // es6语法:对象的扩展运算符,并且修改新对象中的数据
    const newObj = { ...this.state.obj, num: Math.floor(Math.random() * 3) };
    this.setState(() => {
      return {
        obj: newObj
      }
    })

    // 错误做法:直接修改原始对象state中的数据
    /* const newObj = this.state.obj;
    newObj.num = Math.floor(Math.random() * 3);
    this.setState(() => {
      return {
        obj: newObj
      }
    }) */
  }

  render() {
    console.log('App2 render');
    return (
      <div>
        <NumBox num={this.state.obj.num} />
        <button onClick={this.handleClick}>生成随机数</button>
      </div>
    )
  }
}

class NumBox extends React.PureComponent {
  render() {
    console.log('NumBox render');
    return (
      <div>
        <h2>随机数:{this.props.num}</h2>
      </div>
    )
  }
}

export default App3;

5、虚拟 DOM 和 Diff 算法

  • React 更新视图的思想是:只要 state 变化就重新渲染视图
  • 特点:思路非常清晰
  • 问题:组件中只有一个 DOM 元素需要更新时,也得把整个组件的内容重新渲染到页面中?不是
  • 理想状态:部分更新,只更新变化的地方
  • 问题:React 是如何做到部分更新的?虚拟 DOM 配合 Diff 算法

虚拟DOM:本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容(UI)

执行过程

  1. 初次渲染师,React 会根据初始 state(Model),创建一个 虚拟 DOM 对象(树)
  2. 根据虚拟 DOM 生成真正的 DOM,渲染到页面中
  3. 当数据变化后(setState()),重新根据新的数据,创建新的虚拟 DOM 对象(树)
  4. 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容
  5. 最终,React 直降 变化的内容 更新(patch)到 DOM 中,重新渲染到页面

代码演示

  • 组件 render() 调用后,根据 状态JSX结构 生成虚拟 DOM 对象
class NumBox extends React.PureComponent {
  render() {
    // 虚拟DOM对象, el
    const el = (
      <div>
        <h2>随机数:{this.props.num}</h2>
      </div>
    )
    console.log(el);
    
    return el
  }
}

六、React 路由

1、React 路由介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDNo0sC1-1692620120421)(D:\PerfectCode\前端学习\React\Image\路由介绍.PNG)]

2、路由的基本使用

2.1 使用步骤

  1. 安装:yarn add react-router-dom/npm i react-router-dom

  2. 导入路由的三个核心组件:Router/Routes/Route/Link

    import { BrowserRouter as Router, Routes, Route, Link } from ‘react-router-dom’

  3. 使用 Router 组件 包裹整个应用(重要)

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

  5. 使用 Route 组件 配置路由规则和要展示的组件(路由出口),v6版本必须要使用 Routes 组件 包裹

import React from 'react';
// 2. 导入组件
import { BrowserRouter as Router, Route, Link, Routes } from 'react-router-dom';

/* 
  react-router-dom 的基本使用
*/

const About = () => {
  return <h2>关于</h2>
}
const User = () => {
  return <h2>用户</h2>
}

const App = () => {
  return (
    // 3. Router 组件是路由的根组件,所有的路由组件都应该包裹在 Router 组件中
    <Router>
      <div>
        <h1>React路由基础</h1>
        <ul>
          {/* 4. 指定路由的入口 */}
          <li><Link to="/about">关于</Link></li>
          <li><Link to="/user">用户</Link></li>
        </ul>
        <hr />
        {/* 5. 指定路由出口 */}
        {/* v6 必须用 Routes 包裹 */}
        <Routes>
          {/* v6 版本把component改为element */}
          <Route path="/about" element={<About />} />
          <Route path="/user" element={<User />} />
        </Routes>
        <hr />
      </div>
    </Router>
  );
}

export default App;

2.2 常用组件说明

  • Router 组件:包裹整个应用,一个 React 应用只需要使用一次

  • 两种常用 Router:HashRouter 和 BrowserRouter

  • HashRouter:使用 URL 的 hash 值实现( #/*** )

  • (推荐)BrowserRouter:使用 H5 的 history API 实现( /*** )

  • Link 组件:用于指定导航链接(a 标签)

    to 属性:浏览器地址栏中的 pathname(location.pathname)

  • Route 组件:指定路由展示的组件相关信息

    path 属性:路由规则

    element 属性:展示的组件

    Route 组件写在哪,渲染出来的组件就展示在哪

3、路由的执行过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KLED1EJn-1692620120421)(D:\PerfectCode\前端学习\React\Image\路由执行过程.PNG)]

4、编程式导航

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

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

  • push(path):跳转到指定页面(有历史记录),参数 path 表示要跳转的路径(v5版本)

    this.props.history.push(‘/login’)

  • //V5版本
    import { useHistory } from "react-router-dom";
    const history = useHistory();
    history.path()
    history.go(n);  // 前进或者后退到某个页面,n代表页数
    history.goback(); // 返回
    history.forward(); // 前进
    history.replace(); // 没有历史记录
    
    //V6版本
    import {useNavigate} from 'react-router-dom';
    const navigate = useNavigate();
    navigate("/home"); // 跳转路由
    navigate('/home', {replace: true});
    navigate(-1);  //后退
    navigate(1); //前进
    

注意一:v5和v6版本略有不同,可参考下面内容

react-router-dom V6版本使用 - 简书 (jianshu.com)

注意二:useNavigate只能用在函数组件中,要想class组件也能使用,可以包装,如下:

高阶组件包装使得hook在class组件中使用

import { useNavigate } from 'react-router-dom'
// 高阶组件包装
function widthHook(WrapCompontent) {
  // 设置别名
  WrapCompontent.displayName = `WithHook${getDisplayName(WrapCompontent)}`
  return function QiLincompont() {
    const navigate = useNavigate()
    return <WrapCompontent to={navigate}></WrapCompontent>
  }
}

// 设置别名
function getDisplayName(WrapCompontent) {
  return WrapCompontent.displayname || WrapCompontent.name || 'Component'
}

export default widthHook

此时高阶组件函数已经写好了,只需要对自己写的组件进行包装即可**(看个人习惯,可以把 to 改为 navigate)**

import React from 'react'
import withHook from 'withHook'

class IndexSwiper extends React.PureComponent {
    toPath = (path) => {
        this.props.to(path)
        // this.props.to(path,{replace:true})
    }
    render() {
        return <button onClick={ this.toPath('/news') }>路由跳转</button>
    }
}

const QilinCompont= withHook(IndexSwiper)

export default QilinCompont

改改路径可以直接使用

案例

import React from 'react';
import widthHook from './widthHook';
// 2. 导入组件
import { BrowserRouter as Router, Route, Link, Routes, useNavigate } from 'react-router-dom';

/* 
  react-router-dom 的基本使用
*/

const About = () => {
  return <h2>关于</h2>
}
const User = () => {
  return <h2>用户</h2>
}
const Home = () => {
  const navigate = useNavigate();
  const handleBack = () => {
    // 使用编程式导航,返回登录页
    navigate(-1);  //后退
  }
  return (
    <div>
      <h2>首页</h2>
      <button onClick={handleBack}>返回</button>
    </div>
    )
}
class Login extends React.Component {
  handleLogin = () => {
    // 使用编程式导航
    // to 是高级组件封装 navigate 的方法
    // 带历史记录的跳转
    this.props.to(`/home`)
    // 不带历史记录的跳转
    // this.props.to(`/home`,{replace:true})
  }
  render() {
    return (
      <div>
        <h2>登录页面</h2>
        <button onClick={this.handleLogin}>登录</button>
      </div>
    )
  }
}

const LoginWithHook = widthHook(Login)

const App = () => {
  return (
    // 3. Router 组件是路由的根组件,所有的路由组件都应该包裹在 Router 组件中
    <Router>
      <div>
        <h1>React路由基础</h1>
        <p><Link to="/login">去登录</Link></p>
        <ul>
          {/* 4. 指定路由的入口 */}
          <li><Link to="/about">关于</Link></li>
          <li><Link to="/user">用户</Link></li>
        </ul>
        <hr />
        {/* 5. 指定路由出口 */}
        {/* v6 必须用 Routes 包裹 */}
        <Routes>
          {/* v6 版本把component改为element */}
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/user" element={<User />} />
          <Route path="/login" element={<LoginWithHook />} />
          <Route path="/home" element={<Home />} />
        </Routes>
        <hr />
      </div>
    </Router>
  );
}

export default App;

5、默认路由(重定向)、路由嵌套

5.1 默认路由(重定向)

  • 进入页面就会匹配的路由
  • path:/

​ <Route path=“/” element={<Home />} />

  • 也可以使用重定向来实现默认路由
//V5版本
import { Redirect } from 'react-router-dom';
//重定向到首页
return <Redirect to="/home" component={Home} />

//V6版本
import { Navigate } from 'react-router-dom';
//重定向到首页
return <Navigate to="/home" element={<Home />} />

5.2 路由嵌套

  • 父:../*
  • 子:没有/
//  路由使用
<Routes>
  <Route path='/home/*'  element={<Home />} 
        <Route path='home-page' element={<div>我是嵌套内容</div>} />
  <Route/>
</Routes>
  • 使用 Outlet 组件,此组件是一个占位符,告诉 React Router 嵌套的内容应该放到哪里。
//路由中
<Routes>
  <Route path='/user/*'  element={<User />} 
   	    <Route path='user-item' element={<div>我是嵌套子项</div>} />
  <Route/>
</Routes>

//User组件中
import {Outlet} from 'react-router-dom';
const User = () => {
	return 
	<section>
	  <h1>我是外容器</h1>
	  <Outlet />
	</section>
}
export default User;

  • useRoutes代替react-router-config
export const routes = [
    {
        path: '/',
        element: <Layout />,
        children: [
            {
                path: 'home',
                meta: {
                    title: '首页',
                    icon: ‘’,
                },
                children: [
                    {
                        path: 'homepage1',
                        element: <Homepage />,
                        meta: {
                            title: 'homepage1',
                        }
                    }
                ]
            }
        ]
    },
    {
        path: '/login',
        element: <Login />,
        meta: {
            title: '登录',
            hideMenu: true
        }
    },
    {
        path: '*',
        element: <Page404 />,
        meta: {
            title: '404',
            hideMenu: true
        }
    },
];
const Routes = () => (
    useRoutes(routes)
)
export default Routes;

6、匹配模式

6.1 模糊匹配模式(v5)

  • 问题:当 Link 组件的 to 属性值为 ‘/login’ 时,为什么默认路由 也会被匹配成功
  • 默认情况下,React 路由是 模糊匹配模式
  • 模糊匹配模式:只要 pathname 以 path 开头就会被匹配成功(to以path开头)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OphzwQJi-1692620120421)(D:\PerfectCode\前端学习\React\Image\模糊匹配.PNG)]

6.2 精确匹配

注意:不用Route标签包裹子组件,可以直接使用element属性,并且不需要exact来表示精准匹配,V6版本内部算法改变,它默认就是匹配完整路径,先后顺序不再重要,它能够自动找出最优匹配路径

  • v5 中给 Route 组件添加 exact 属性,让其变为 精确匹配模式

  • 精确匹配:只有当 path 和 pathname 完全匹配才会展示相应路由

推荐:使用v6版本

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Pluto_ssy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值