React基础学习

78 阅读17分钟

React学习

1、React 概述

1.1、什么是React?

​ React 是一个用于构建用户界面JavaScript 库,主要用于开发 HTML 页面和构建 Web 应用。如果从MVC的角度来看,React仅仅是视图层(V),也就是只负责试图的渲染,而并未提供了完整的M(Model模型)和C(Controller控制器)的功能。React 起源于 Facebook 的内部项目。最初用于构建 Instagram 的网站。并React 于 2013 年 5 月正式开源。

1.2、React的特点

  • 声明式编程
  • 基于组件
  • JSX 语法
  • 单向数据流
  • 跨平台开发
  • 高效的开发体验
  • 声明式 UI

2、React 的基本使用

2.1、安装React

打开项目终端

npm i react react-dom

2.2、React的使用

image-20240812150946392.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>01-react基本使用</title>
</head>
<body>
    <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>
     <!-- 2 创建React元素 -->
     <!-- 参数一:元素名称
     参数二:元素属性
     参数三:元素内容 -->
     <script>
     const title = React.createElement('h1', {title:'标题'}, 'Hello React')
     // 3 将元素渲染到页面
     ReactDOM.render(title, document.getElementById('root'))
     </script>
</body>
</html>

效果:

image-20240812150820993.png

3、React 脚手架的使用

3.1、使用React脚手架初始化项目

1、初始化项目命令:npx create-react-app myapp

2、启动项目,在项目根目录下执行命令:yarn start 启动成功后会自动打开浏览器

启动成功页面:

image-20240812151704259.png

3.2、在脚手架中使用React

src/index.js删除原来内容并写入下面代码

// 1.引入React和ReactDOM(ES6模块化语法导入)
import React from 'react';
import ReactDOM from 'react-dom';
// 2.创建组件
const title = React.createElement('h1', {title:'一级标题'}, 'Hello world!');
// 3.渲染组件(脚手架一级组件为root不用自己创建)
ReactDOM.render(title, document.getElementById('root'));

效果:image-20240812152343006.png

4、jsx学习

4.1、什么是JSX?

​ JSX(JavaScript XML)是一种 JavaScript 的语法扩展,主要用于在 React 中描述用户界面(UI)的结构。它看起来很像 HTML,但它实际上是在 JavaScript 中编写的代码。JSX 是 React 中构建组件的主要方式,能够让开发者以一种更加直观的方式定义 UI。

4.2、JSX的基本使用

src/index.js

// 1.引入React和ReactDOM
import React from 'react';
import ReactDOM from 'react-dom';
// 2.创建JSX组件
const title = (<h1>Hello, <span>JSX</span></h1>)
// 3.渲染组件
ReactDOM.render(title, document.getElementById('root'));

效果展示:

image-20240812153322378.png

4.3、JSX的注意点

  • React元素的属性名使用驼峰命名法。
  • 特殊属性名:class-> classNmae、for->htmlFor、tabindex->tabIndex。
  • 没有子节点的React元素可以使用/>结束。
  • 推荐:使用小括号包裹JSX,从而避免JS中的自动插入分号陷阱。

4.4、在JSX中使用JS表达式

// 1.引入React和ReactDOM
import React from 'react';
import ReactDOM from 'react-dom';
// 2.创建JSX组件
const name = 'linhao'
const title = (<h1>Hello, <span>{name}</span></h1>)
// 3.渲染组件
ReactDOM.render(title, document.getElementById('root'));

效果:image-20240812154027373.png

4.5、JSX的条件渲染

import React from 'react';
import ReactDOM from 'react-dom';
const isLoading = true
/**
 * 
 * @returns 条件渲染
 */
// const loadData = ()=>{
//   if(isLoading){
//     return <div>loading...</div>
//   }
//     return <div>数据加载完成</div>  
// }
//  ------------------------------------------------------------------------
/**
 * 三元表达式
 */
// const loadData = ()=>{
//   return isLoading ? <div>loading...</div>: <div>数据加载完成</div>
// }
//  ------------------------------------------------------------------------
/**
 * 逻辑运算符
 */
const loadData = ()=>{
    return isLoading && (<div>loading...</div>)
}
const title = (<h1>
条件渲染:{loadData()}
</h1>)
ReactDOM.render(title, document.getElementById('root'));

4.6、JSX的列表渲染

import React from 'react';
import ReactDOM from 'react-dom';
const songs = [
  {id: 1,name: '刘德华'},
  {id: 2,name: '王德发'},
  {id: 3,name: '刘德网'},
]
const title = (<ul>
 {songs.map(item =><li key={item.id}>{item.name}</li>)} 
</ul>)
ReactDOM.render(title, document.getElementById('root'));

4.7、JSX的样式处理

4.7.1、行内样式(Style)
import React from 'react';
import ReactDOM from 'react-dom';
const title = (<h1 style={{color: 'red' ,backgroundColor: 'skyblue'}}> JSX的行内样式处理</h1>)
ReactDOM.render(title, document.getElementById('root'));
4.7.2、类名(className)

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './css/index.css';
const title = (<h1 className='title'> JSX的行内样式处理</h1>)
ReactDOM.render(title, document.getElementById('root'));

src/css/index.css

.title{
    text-align: center;
    color: #0084ff;
}

5、React组件

5.1、React组件创建方式

5.1.1、函数组件创建

注意点:

  1. 函数名称必须大写字母开头
  2. 函数组件必须有返回值,表示该组件的结构
  3. 若返回值为null时,则表示不渲染任何内容
import React from 'react';
import ReactDOM from 'react-dom';
// function Hello(){
//   return <h1 >这是一个函数组件</h1>
// }
const Hello = () => <h1>这是一个函数组件</h1>;;
ReactDOM.render(<Hello />, document.getElementById('root'));
5.1.2、类组件创建

注意点:

  1. 类名称必须大写字母开头
  2. 类组件必须继承React.Component父类。从而可以使用父类中提供的方法或属性
  3. 类组件必须提供render()方法
  4. render()方法必须有返回值,表示该组件的结构
import React from 'react';
import ReactDOM from 'react-dom';
class Hello extends React.Component {
  render() {
    return <div>这是一个继承React.Component的类组件</div>;
  }
}
ReactDOM.render(<Hello />, document.getElementById('root'));
5.1.3、抽离为单独的js文件

src/js/Hello.js

import React from "react";
class Hello extends React.Component {
  render() {
    return (
      <div>
        Hello Class Component
      </div>
    );
  }
}
export default Hello;

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
// 1引入Hello.js文件
import Hello from './js/Hello';
// 2渲染Hello.js中的组件
ReactDOM.render(<Hello />, document.getElementById('root'));

5.2、React 的事件处理

5.2.1、事件绑定

注意点:

  1. 语法:on+事件名称={事件处理程序},比如onClick={() => {}}
  2. React事件采用驼峰命名法,比如:onClick、onDoubleClick等
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  handleClick = () => {
    console.log('单击事件');
  }
  render() {
    return (
      <button onClick={this.handleClick}>点我</button>
    );
  }  
}
ReactDOM.render(<App />, document.getElementById('root'));
5.2.2、事件对象
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  handleClick = (e) => {
    e.preventDefault()
    console.log('事件对象', e)
  }
  render() {
    return (
      <a onClick={this.handleClick}>a标签</a>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

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

  • 函数组件叫做无状态组件,类组件叫做有状态组件(状态(state)就是数据)
  • 函数组件没有自己的状态,只负责数据展示(静)
  • 类组件有自己的状态,负责更新UI,让页面“动”起来
  • 状态(state)是组件内部的私有数据,只能组件内部使用
  • state的值是对象,表示一个组件中可以有多个数据

计数器案例

import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  // 初始化state
  // constructor() {
  //   super();
  //   this.state = {
  //     count: 0,
  //   };
  // }
  //简写
  state ={
    count: 0,
  }
  handleClick=()=>{
    this.setState({
      count: this.state.count + 1,
    });
  }
  render() {
    return (
      <div><h1>计数器:{this.state.count}</h1>
      <button onClick={this.handleClick}>count++</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

5.4、事件绑定this指向

5.4.1、箭头函数

利用箭头函数自身不绑定this的特点

import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  state ={
    count: 0,
  }
  handleClick(){
    this.setState({
      count: this.state.count + 1,
    });
  }
  render() {
    return (
      <div><h1>计数器:{this.state.count}</h1>
      <button onClick={()=>this.handleClick()}>count++</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));
5.4.2、bind方法

利用ES5中的bind方法,将事件处理方法中的this与组件绑定在椅子

import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  constructor() {
    super();
    this.handleClick = this.handleClick.bind(this);
    this.state = {count: 0};
  }
  handleClick(){
    this.setState({
      count: this.state.count + 1,
    });
  }
  render() {
    return (
      <div><h1>计数器:{this.state.count}</h1>
      <button onClick={this.handleClick}>count++</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));
5.4.3、class的实例方法

其实本质还是箭头函数只是使用的箭头函数的class实例方法

import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  constructor() {
    super();
    this.state = {count: 0};
  }
  handleClick=()=>{
    this.setState({
      count: this.state.count + 1,
    });
  }
  render() {
    return (
      <div><h1>计数器:{this.state.count}</h1>
      <button onClick={this.handleClick}>count++</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

5.5、React表单处理

5.5.1、受控组件

受控组件:组件值收到React 控制的表单元素(state和setState())

表单受控组件示例:

  • 给表单元素添加name属性,名称与state相同
  • 根据表单元素获取对应值,
  • 在change事件中通过[name] 修改对应值
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  constructor() {
    super();
    this.state = {
      txt: '',
      checkbox: false,
      textarea:'',
      city:'sh',
    };
  }
  handleChange=(e)=>{
    const target = e.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({
      [name]: value,
    });
  }
  render() {
    return (
      <div>
        <input type="text" name='txt' value={this.state.txt} onChange={this.handleChange}/>
        <input type='checkbox' name='checkbox' value={this.state.checkbox} onChange={this.handleChange}/>
        <input type='textarea' name='textarea' value={this.state.textarea} onChange={this.handleChange}/>
        <select value={this.state.city} name='city' onChange={this.handleChange} >
          <option value="bj">北京</option>
          <option value="sh">上海</option>
          <option value="sz">深圳</option>
          <option value="xa">西安</option>
        </select>

      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

效果展示:

image-20240813094618176.png

5.5.2、非受控组件(Ref)

使用ref使用原生DOM方式来获取表单元素值,ref作用:获取DOM或组件

import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  constructor() {
    super();
   this.txtRef = React.createRef();
  }
  render() {
    return (
      <div>
        <input type="text" ref={this.txtRef} />
        <button onClick={() => {
          console.log(this.txtRef.current.value);
        }}>点击按钮</button>
      </div>
    );
  }
}
ReactDOM.render(<App />, document.getElementById('root'));

5.6、React 组件案例

import React from 'react'
import ReactDOM from 'react-dom'
/* 
  评论列表案例

  comments: [
    { id: 1, name: 'jack', content: '沙发!!!' },
    { id: 2, name: 'rose', content: '板凳~' },
    { id: 3, name: 'tom', content: '楼主好人' }
  ]
*/
import './css/index.css'
class App extends React.Component {
  state = {
    name:'',
    content:'',
    comments: []
  }
  //获取受控组件值
  handleChange=(e)=>{
    const target = e.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({
      [name]: value,
    });
  }
  //渲染评论列表
  renderComments = () => {
    const { comments } = this.state
    return comments.length === 0 
    ?(<div className="no-comment">暂无评论,快去评论吧~</div>)
    :<div>
      <ul >
        {comments.map(item => 
        <li key={item.id}>
          <h3>评论人:{item.name}</h3>     
          <p>{item.content}</p>
          </li>)}
      </ul>
    </div>
  }
  //添加评论
  addContent= () => {
    const { name, content,comments } = this.state
    if(!name.trim() || !content.trim()){
      alert('请输入评论人或内容')
      return;
    }
    const newComments = [
      {
        id: Math.random(),
        name,
        content
      },...comments]
      this.setState({
        comments: newComments,
        name:'',
        content:''
      })
  }
  render() {
    const { name, content } = this.state
    return (
      <div className="app">
        <div>
          <input className="user" type="text" name='name' value={name} placeholder="请输入评论人" onChange={this.handleChange}/>
          <br />
          <textarea
            className="content"
            cols="30"
            rows="10"
            placeholder="请输入评论内容"
            name="content"
            value={content}
            onChange={this.handleChange}
          />
          <br />
          <button onClick={this.addContent}>发表评论</button>
        </div>
        {
          this.renderComments()
        }
      </div>
    )
  }
}
// 渲染组件
ReactDOM.render(<App />, document.getElementById('root'))

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

6、React组件通讯

​ 在React中,组件通讯是指不同的组件之间共享数据或相互协作的方式。因为React是基于组件构建用户界面的,每个组件通常会有自己独立的状态和逻辑,但有时候需要让多个组件之间互通信息或共享状态。组件通讯的方式主要取决于组件的层级关系和数据的流向。

6.1、组件的props

props作用:接收传递给组件的数据。只能读取不能修改

import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
  constructor(props) {
    super(props);
    props.fn();
  }
  render() {
    return (
      <div>
        <h1> name:{this.props.name},age:{this.props.age}</h1>
        {this.props.tag}
      </div>
    )
  }
}
// 渲染组件
ReactDOM.render(<App
   name="张三" age="18" colors={['red','green','blue']} 
   fn={()=> console.log("我是一个函数")}
   tag={<p>这是一个组件</p>}
   />, 
   document.getElementById('root'))

6.2、组件通讯的三种方式

6.2.1、父子通讯
import React from 'react'
import ReactDOM from 'react-dom'
import './css/index.css'
// 父组件
class Parent extends React.Component {
  state = {
    name: '张三'
  }

  render() {
    return (
      <div className="parent">
        父组件:
        <Child  name={this.state.name}/>
      </div>
    )
  }
}
// 子组件
const Child = (props) => {
  const name = props.name
  return (
    <div className="child">
      <p>子组件,接收到父组件的数据:{name}</p>
    </div>
  )
}
ReactDOM.render(<Parent />, document.getElementById('root'))

index.css

.parent {
  height: 100px;
  padding: 20px;
  background-color: skyblue;
}

.child {
  height: 50px;
  background-color: aquamarine;
}
6.2.2、子到父通讯
import React from 'react'
import ReactDOM from 'react-dom'
import './css/index.css'
// 父组件
class Parent extends React.Component {
  state = {
    parentMsg: ''
  }
  // 提供回调函数,用来接收数据
  getChildMsg = data => {
    console.log('接收到子组件中传递过来的数据:', data)
    this.setState({
      parentMsg: data
    })
  }
  render() {
    return (
      <div className="parent">
        父组件:{this.state.parentMsg}
        <Child getMsg={this.getChildMsg} />
      </div>
    )
  }
}
// 子组件
class Child extends React.Component {
  state = {
    msg: '刷知乎'
  }
  handleClick = () => {
    this.props.getMsg(this.state.msg)
  }
  render(){
    return (
      <div className="child">
        子组件:{' '}
        <button onClick={this.handleClick}>点我,给父组件传递数据</button>
      </div>
    )
  }

}
ReactDOM.render(<Parent />, document.getElementById('root'))
6.2.3、兄弟组件通讯
import React from 'react'
import ReactDOM from 'react-dom'
// 父组件
class Parent 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 => <h1>计数器:{props.count}</h1>
const Child2 = props => <button onClick={()=>props.onIncrement()}>+1</button>
ReactDOM.render(<Parent />, document.getElementById('root'))

6.3、Context的使用

使用上面的通讯方法,层级过多以后就会很繁琐,我们使用Contet进行跨组件传递数据(语言等)

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

const { Provider, Consumer } = React.createContext();

class App extends React.Component {
  state = {
    songs: {
      name: 'zhangsan',
      age: 18
    },
    logMessage: () => {
      console.log('This is a message from Context!');
    },
    CustomComponent: () => <div style={{ backgroundColor: 'lightblue'}}>我是一个来自 Context 的自定义组件</div>
  }

  render() {
    return (
      <Provider value={this.state}>
        <div className="app">
          <Node />
        </div>
      </Provider>
    )
  }
}

const Node = () => <div className="node"><SubNode /></div>

const SubNode = () => <div className="subnode"><Child /></div>

const Child = () => {
  return (
    <div className="child">
      <Consumer>
        {context => (
          <div>
            <span>我是子节点 -- 名字: {context.songs.name}, 年龄: {context.songs.age}</span>
            <button onClick={context.logMessage}>点击我执行函数</button>
            <context.CustomComponent />
          </div>
        )}
      </Consumer>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

6.4、children属性

  • children属性只有当组件有子节点时,props才会有该属性
  • children属性与普通props一样,值可以是任意值(文本,组件,React元素,函数方法)
import React from 'react';
import ReactDOM from 'react-dom';

const App = props =>{
  props.children()
  return (
    <div>
      <h1>Hello World</h1>
      <p>{props.children}</p>
    </div>
  )
}

ReactDOM.render(<App>{()=>{
  console.log("子节点children")
}}</App>, document.getElementById('root'));

6.5、props校验

作用:保证组件使用者传入的类型、格式限制,给出错误提示,增加代码健壮性

  1. 安装包 prop-types(yarn add prop-types/npmiprops-types )
  2. 引入import PropTypes from 'prop-types';文件
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

const App = props =>{
  const arr = props.colors;
  const list = arr.map(item => <li key={item}>{item}</li>);
  return (
    <div>
      <ul>{list}</ul>
    </div>
  )
}
/**
 * 基本数据类型
PropTypes.string: 检查 prop 是否为字符串类型。
PropTypes.number: 检查 prop 是否为数字类型。
PropTypes.boolean: 检查 prop 是否为布尔类型。
PropTypes.object: 检查 prop 是否为对象类型。
PropTypes.array: 检查 prop 是否为数组类型。
PropTypes.func: 检查 prop 是否为函数类型。
PropTypes.symbol: 检查 prop 是否为 symbol 类型。
PropTypes.node: 检查 prop 是否为任何可以被渲染的内容(字符串、数字、元素或数组等)。
PropTypes.element: 检查 prop 是否为 React 元素。
特殊类型
PropTypes.any: 可以是任意类型。
PropTypes.isRequired: 标记一个 prop 为必填项。
PropTypes.instanceOf(Class): 检查 prop 是否为指定类的实例。
PropTypes.oneOf(['Option1', 'Option2']): 检查 prop 是否为给定值列表中的一个值(枚举)。
PropTypes.oneOfType([PropTypes.string, PropTypes.number]): 检查 prop 是否为给定类型列表中的一个。
PropTypes.arrayOf(PropTypes.string): 检查 prop 是否为特定类型的数组。
PropTypes.objectOf(PropTypes.string): 检查 prop 是否为特定类型值的对象。
PropTypes.shape({ key: PropTypes.string, value: PropTypes.number }): 检查 prop 是否为具有指定键值对的对象。
PropTypes.exact({ key1: PropTypes.string, key2: PropTypes.number }): 检查 prop 是否为具有指定键值对且没有其他多余键的对象。
 */
App.propTypes = {
  colors: PropTypes.array,
}
ReactDOM.render(<App colors={[1,2,3,4]}/>, document.getElementById('root'));

具体类型可以查看React propsType 校验规则

6.6 props默认值

import React from 'react';
import ReactDOM from 'react-dom';
const App = props =>{
  return (
    <div>
      props默认值:{props.number}
    </div>
  )
}
App.defaultProps = {
  number:10,
}
ReactDOM.render(<App/>, document.getElementById('root'));

7、React组件的生命周期

只有类组件才有生命周期

image-20240813144907167.png

7.1、创建时(挂载阶段)

执行时机:页面加载时

执行顺序:

钩子函数触发时机作用
constructor创建组件时,最先执行初始化state
为函数方法绑定this
render每次组件渲染都会触发渲染UI(不能调用setState()
componentDidMount组件挂载完(完成DOM渲染后)发送网络请求
DOM操作

image-20240813145342586.png

import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
  constructor(props){
    super(props);
    console.warn('constructor钩子执行了');
  }
  componentDidMount(){
    console.warn('componentDidMount钩子执行了');
  }
  render() {
    console.warn('render钩子执行了');
    return (
      <div>
        <h1>Hello, world!</h1>
        <button id="btn">点我</button>
      </div>
    );
  }

}
ReactDOM.render(<App/>, document.getElementById('root'));


结果:

image-20240813150720219.png

7.2、更新时

执行顺序:

image-20240814095139815.png

render钩子执行时机:

  1. setState()

  2. forceUpdate()

  3. 组件接收到新的props

    任意一种都会导致render钩子执行

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

/* 
  组件生命周期
*/

class App extends React.Component {
  constructor(props) {
    super(props)

    // 初始化state
    this.state = {
      count: 0
    }
  }

  // 打豆豆
  handleClick = () => {
    // this.setState({
    //   count: this.state.count + 1
    // })

    // 演示强制更新:
    this.forceUpdate()
  }

  render() {
    console.warn('生命周期钩子函数: render')
    return (
      <div>
        <Counter count={this.state.count} />
        <button onClick={this.handleClick}>打豆豆</button>
      </div>
    )
  }
}

class Counter extends React.Component {
  render() {
    console.warn('--子组件--生命周期钩子函数: render')
    return <h1>统计豆豆被打的次数:{this.props.count}</h1>
  }
}

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

钩子函数触发时机作用
render每次组件渲染都会触发渲染UI
componentDidUpdate组件更新(完成DOM渲染后)发送网络请求
DOM操作(setState()必须放在一个if条件中)
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件生命周期
*/

class App extends React.Component {
  constructor(props) {
    super(props)

    // 初始化state
    this.state = {
      count: 0
    }
  }

  // 打豆豆
  handleClick = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    return (
      <div>
        <Counter count={this.state.count} />
        <button onClick={this.handleClick}>打豆豆</button>
      </div>
    )
  }
}

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

  // 注意:如果要调用 setState() 更新状态,必须要放在一个 if 条件中
  // 因为:如果直接调用 setState() 更新状态,也会导致递归更新!!!
  componentDidUpdate(prevProps) {
    console.warn('--子组件--生命周期钩子函数: componentDidUpdate')

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

    // 错误演示!!!
    // this.setState({})

    // 获取DOM
    // const title = document.getElementById('title')
    // console.log(title.innerHTML)
  }
}

ReactDOM.render(<App />, document.getElementById('root'))
7.3、卸载时

执行时机:组件从页面消失

钩子函数触发时机作用
componentWillUnmount卸载组件执行清理工作(比如:清除定时器等)
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  组件生命周期
*/

class App extends React.Component {
  constructor(props) {
    super(props)

    // 初始化state
    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 onClick={this.handleClick}>打豆豆</button>
      </div>
    )
  }
}

class Counter extends React.Component {
  componentDidMount() {
    // 开启定时器
    this.timerId = setInterval(() => {
      console.log('定时器正在执行~')
    }, 500)
  }

  render() {
    return <h1>统计豆豆被打的次数:{this.props.count}</h1>
  }

  componentWillUnmount() {
    console.warn('生命周期钩子函数: componentWillUnmount')

    // 清理定时器
    clearInterval(this.timerId)
  }
}

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

8、React组件复用案例

8.1、props render 模式
import React from 'react'
import ReactDOM from 'react-dom'
import img from './images/cat.png'
/* 
  render props 模式
*/

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

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

  // 监听鼠标移动事件
  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 => {
            return (
              <img  src={img} alt='猫' style={{
                position: 'absolute',
                left: mouse.x-64,
                top: mouse.y-64
              }}/>
            )
          }}
        />
      </div>
    )
  }
}

ReactDOM.render(<App />, document.getElementById('root'))
8.2、props children模式
import React from 'react'
import ReactDOM from 'react-dom'
import img from './images/cat.png'
import propTypes from 'prop-types'
/* 
  render props 模式
*/

// 创建Mouse组件
class Mouse extends React.Component {
  // 鼠标位置state
  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 this.props.children(this.state)
  }
}
//对children进行类型检查
Mouse.propTypes={
  children: propTypes.func.isRequired
}
class App extends React.Component {

  render() {
    return (
        <Mouse >
         {mouse => {
            return (
              <img  src={img} alt='猫' style={{
                position: 'absolute',
                left: mouse.x-64,
                top: mouse.y-64
              }} />
            )
          }}</Mouse>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))
8.3、高阶组件复用
  1. 创建一个函数,名称为with开头
  2. 指定函数参数,参数以大写字母开头(为要渲染的组件)
  3. 在函数中创建一个类组件,提供复用的代码,并返回
  4. 在该组件中渲染参数组件,同时将状态通过props传递给参数组件
  5. 调用高阶组件,传入要增强的组件,通过返回值拿到增强后组件,将其返回页面
  6. 设置displayName解决多个使用增强后组件的 名称重复问题
  7. 将props也传递给增强组件
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  高阶组件
*/

// 创建高阶组件
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() {
      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 MousePosition = withMouse(Position)
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件</h1>
        <MousePosition q="q" />
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

9、优化代码写法

9.1、setState()优化

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

/* 
  setState() 异步更新数据
*/

class App extends React.Component {
  state = {
    count: 1
  }

  handleClick = () => {
    // 此处,更新state
    // 注意:异步更新数据的!!!
    this.setState({
      count: this.state.count + 1
    })
    console.log('count:', this.state.count) // 1
    this.setState({
      count: this.state.count + 1 // 1 + 1
    })
    console.log('count:', this.state.count) // 1
  }

  render() {
    console.log('render')
    return (
      <div>
        <h1>计数器:{this.state.count}</h1>
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}

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

由于setState()方法是异步执行的导致我们不能立即拿到更新后的状态所以我们要优化下setstate()方法从而拿到最新的状态代码如下:

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

class App extends React.Component {
  state = {
    count: 1
  }

  handleClick = () => {
    this.setState((state, props) => {
      return {
        count: state.count + 1 
      }
    }, () => {
      console.log('第一次更新后:', this.state.count); // 2
      this.setState((state, props) => {
        return {
          count: state.count + 1
        }
      }, () => {
        console.log('第二次更新后:', this.state.count); // 3
        console.log(document.getElementById('title').innerText);
        document.title = '更新后的count为:' + this.state.count;
      });
    });

    console.log('count:', this.state.count); // 1,异步更新,因此这里的count依然是1
  }

  render() {
    return (
      <div>
        <h1 id="title">计数器:{this.state.count}</h1>
        <button onClick={this.handleClick}>+1</button>
      </div>
    )
  }
}

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

9.2、React组件更新机制

React更新机制是 只更新当前组件和其子组件其他组件不会更新

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

/* 
  组件更新机制
*/

import './index.css'

// 根组件
class App extends React.Component {
  state = {
    color: '#369'
  }

  getColor() {
    return Math.floor(Math.random() * 256)
  }

  changeBG = () => {
    this.setState(() => {
      return {
        color: `rgb(${this.getColor()}, ${this.getColor()}, ${this.getColor()})`
      }
    })
  }

  render() {
    console.log('根组件')
    return (
      <div className="app" style={{ backgroundColor: this.state.color }}>
        <button onClick={this.changeBG}>根组件 - 切换颜色状态</button>
        <div className="app-wrapper">
          <Parent1 />
          <Parent2 />
        </div>
      </div>
    )
  }
}

// ------------------------左侧---------------------------

class Parent1 extends React.Component {
  state = {
    count: 0
  }

  handleClick = () => {
    this.setState(state => ({ count: state.count + 1 }))
  }
  render() {
    console.log('左侧父组件')
    return (
      <div className="parent">
        <h2>
          左侧 - 父组件1
          <button onClick={this.handleClick}>点我({this.state.count})</button>
        </h2>
        <div className="parent-wrapper">
          <Child1 />
          <Child2 />
        </div>
      </div>
    )
  }
}

class Child1 extends React.Component {
  render() {
    console.log('左侧子组件 - 1')
    return <div className="child">子组件1-1</div>
  }
}
class Child2 extends React.Component {
  render() {
    console.log('左侧子组件 - 2')
    return <div className="child">子组件1-2</div>
  }
}

// ------------------------右侧---------------------------

class Parent2 extends React.Component {
  state = {
    count: 0
  }

  handleClick = () => {
    this.setState(state => ({ count: state.count + 1 }))
  }

  render() {
    console.log('右侧父组件')
    return (
      <div className="parent">
        <h2>
          右侧 - 父组件2
          <button onClick={this.handleClick}>点我({this.state.count})</button>
        </h2>
        <div className="parent-wrapper">
          <Child3 />
          <Child4 />
        </div>
      </div>
    )
  }
}

class Child3 extends React.Component {
  render() {
    console.log('右侧子组件 - 1')
    return <div className="child">子组件2-1</div>
  }
}
class Child4 extends React.Component {
  render() {
    console.log('右侧子组件 - 2')
    return <div className="child">子组件2-2 </div>
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

9.3、 shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState){}钩子函数可以让组件进行不必要的更新

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

/* 
  组件性能优化:
*/

// 生成随机数
class App extends React.Component {
  state = {
    number: 0
  }

  handleClick = () => {
    this.setState(() => {
      return {
        number: Math.floor(Math.random() * 3)
      }
    })
  }

  // 因为两次生成的随机数可能相同,如果相同,此时,不需要重新渲染
  // shouldComponentUpdate(nextProps, nextState) {
  //   console.log('最新状态:', nextState, ', 当前状态:', this.state)
  //   return nextState.number !== this.state.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) {
    console.log('最新props:', nextProps, ', 当前props:', this.props)
    // 如果前后两次的number值相同,就返回false,不更新组件
    return nextProps.number !== this.props.number

    // if (nextProps.number === this.props.number) {
    //   return false
    // }
    // return true
  }
  render() {
    console.log('子组件中的render')
    return <h1>随机数:{this.props.number}</h1>
  }
}

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

10、React路由

10.1、基本使用

  1. 安装:yarn add react-router-dom
  2. 导入路由的三个核心组件 import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
  3. 使用Router组件包裹整个应用
  4. Link组件 指定路由入口
  5. Route组件指定路由出口
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';

const First = () => <p>页面一的内容</p>;

const App = () => (
  <Router>
    <div>
      <h1>React路由基础</h1>
      {/* 指定路由入口 */}
      <Link to="/first">页面一</Link>

      {/* 指定路由出口 */}
      <Routes>
        {/* 点击链接后才显示 First 组件 */}
        <Route path="/first" element={<First />} />
      </Routes>
    </div>
  </Router>
);

ReactDOM.render(<App />, document.getElementById('root'));

10.2、路由的执行过程

  1. 点击 Link 组件(a标签),修改了浏览器地址栏中的 url 。
  2. React 路由监听到地址栏 ur 的变化。
  3. React 路由内部遍历所有 Route 组件,使用路由规则( path)与 pathname 进行匹配。
  4. 当路由规则(path)能够匹配地址栏中的pathname 时,就展示该 Route 组件的内容。
import React from 'react'
import ReactDOM from 'react-dom'

/* 
  路由的执行过程
*/

import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

const First = () => <p>页面一的内容</p>
const Home = () => <h2>这是Home组件的内容</h2>

// 使用Router组件包裹整个应用
const App = () => (
  <Router>
    <div>
      <h1>React路由基础</h1>
      <div>
        {/* 指定路由出口 */}
        <Route path="/first" component={First} />
        <Route path="/home" component={Home} />
      </div>
      {/* 指定路由入口 */}
      <Link to="/first">页面一</Link>
      <br />
      <Link to="/home">首页</Link>
    </div>
  </Router>
)

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

10.3、js实现页面跳转和返回上一级以及默认路由

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom';

// 页面一
const FirstPage = () => {
  const navigate = useNavigate(); // useNavigate钩子函数用于导航

  return (
    <div>
      <h2>这是页面一</h2>
      <button onClick={() => navigate('/second')}>跳转到页面二</button>
    </div>
  );
};

// 页面二
const SecondPage = () => {
  const navigate = useNavigate(); // useNavigate钩子函数用于导航

  return (
    <div>
      <h2>这是页面二</h2>
      <button onClick={() => navigate(-1)}>返回上一级</button>
    </div>
  );
};

// 应用主组件
const App = () => (
  <Router>
    <div>
      <h1>React 路由示例</h1>
      {/* 导航链接 */}
      <nav>
        <Link to="/">首页</Link>
        <br />
        <Link to="/first">页面一</Link>
        <br />
        <Link to="/second">页面二</Link>
        <br />

      </nav>

      {/* 路由出口 */}
      <Routes>
        {/* 默认路由 */}
        <Route path="/" element={<h2>欢迎来到首页</h2>} />
        {/* 指定路由出口 */}
        <Route path="/first" element={<FirstPage />} />
        <Route path="/second" element={<SecondPage />} />
      </Routes>
    </div>
  </Router>
);

ReactDOM.render(<App />, document.getElementById('root'));

10.3 、路由匹配模式

在React Router v6之前路由默认是模糊匹配模式,即当你定义了一个路径,它将匹配所有以该路径开头的 URL。

要是要使用精确匹配模式需要使用 exact关键字

v6以后没有模糊匹配模式 只有精确匹配 如果需要模糊匹配需要使用通配符进行匹配

精确模式(v6以前):

import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';

const Home = () => <h2>首页</h2>;
const About = () => <h2>关于</h2>;

const App = () => (
  <Router>
    <div>
      <nav>
        <Link to="/">首页</Link> | <Link to="/about">关于</Link>
      </nav>
      <Switch>
        {/* 精确匹配 */}
        <Route path="/" exact component={Home} />
        {/* 默认模糊匹配 */}
        <Route path="/about" component={About} />
      </Switch>
    </div>
  </Router>
);

export default App;

模糊匹配模式(v6以前):

import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';

const Home = () => <h2>首页</h2>;
const About = () => <h2>关于</h2>;

const App = () => (
  <Router>
    <div>
      <nav>
        <Link to="/">首页</Link> | <Link to="/about">关于</Link>
      </nav>
      <Switch>
        {/* 模糊匹配 */}
        <Route path="/about" component={About} />
        {/* 由于没有 `exact`,此路由会匹配以 `/about` 开头的所有路径 */}
        <Route path="/" component={Home} />
      </Switch>
    </div>
  </Router>
);

export default App;