2024React18学习笔记

282 阅读20分钟

React简单介绍

一句话:想进大厂都得会React

React开发环境搭建-create-react-app

create-react-app是一个创建React开发环境的工具,底层由WebPack构建,封装了配置细节

创建命令:npx create-react-app 你的项目名称

启动:npm start

目录结构讲解

介绍一下目录结构,整体结构如图所示: image.png

index.js(react的入口文件)

//React必要的两个核心包 
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <App />
);

JSX

概念和本质

什么是JSX:

JSX是JS和XML(HTML)的缩写,表示在JS代码中编写HTML模板结构,它是React中编写UI模板的方式

JSX的本质:JSX并不是标准的JS语法,它是JS的语法扩展,浏览器本身不能识别,需要通过Babel解析工具做解析之后才能在浏览器中运行。

JSX使用JS表达式

在JSX中可以通过大括号语法{}识别JS表达式,比如常见的变量、函数调用、方法调用等。

let name = "张三"
function getAge() {
  return 23
}
// 项目的根组件
function App() {
  return (
    <div className="App">
      this is App
      {/* 使用引号传递字符串 */}
      {'hello world'}
      {/* 使用js变量 */}
      姓名:{name}
      {/* 函数调用和方法调用 */}
      年龄:{getAge()}
      {/* 使用js对象 */}
      <span style={{ color: 'red' }}>足球</span>
    </div>
  );
}

export default App;

注意:if、switch、变量声明属于语句,不能出现在{}中

JSX实现列表渲染

const list = [
  {
    id: 1, name: '数学'
  },
  {
    id: 1, name: '语文'
  },
  {
    id: 1, name: '英语'
  },
  {
    id: 1, name: '物理'
  }
]
// 项目的根组件
function App() {
  return (
    <div className="App">
      <ul>
        {list.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </div>
  );
}

export default App;

条件渲染

略,本质也是使用js条件判断语句

React中的事件绑定

语法:on + 事件名称={事件处理程序}

const handleClickBtn = () => {
  console.log('点击按钮');
}
// 项目的根组件
function App() {
  return (
    <div className="App">
      <button onClick={handleClickBtn}>按钮</button>
    </div>
  );
}

export default App;

React组件基础使用

在React中,一个组件就是首字母大写的函数,内部存放了组件的逻辑和视图UI,渲染组件只需要把组件当成标签书写即可

useState基本使用

useState是一个React Hook,它允许我们向组件添加一个状态变量

本质:和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)

const [count,setCount]=useState(0)
  1. useState是一个函数,返回值是一个数组
  2. 数组中的第一个参数是状态变量,第二个参数是set函数用来修改状态变量
  3. useState的参数将作为count的初始值

案例:

import { useState } from "react";
// 项目的根组件
function App() {
  let [count, setCount] = useState(0)
  const handleMyButton = () => {
    setCount(count + 1)
  }
  return (
    <div className="App">

      <button onClick={handleMyButton}>点击增加{count}</button>
    </div>
  );
}

export default App;

useState修改状态的规则

状态不可变:在React,状态被认为是只读的,我们应该始终替换它而不是修改它,直接修改状态不能引发视图更新

修改对象状态:对于对象类型的状态变量,应该始终传给set方法一个全新的对象来进行修改

组件的样式处理

import { useState } from "react";
import "./App.css";
// 项目的根组件
function App() {
  let [count, setCount] = useState(0)
  let style = {
    color: 'blue'
  }
  const handleMyButton = () => {
    setCount(count + 1)
  }
  return (
    <div className="App">
      {/* 行内样式,不推荐 */}
      <span style={style}>测试一下</span>
      {/* 通过类名设置样式 */}
      <button onClick={handleMyButton} className="btn-text">点击增加{count}</button>
    </div>
  );
}

export default App;

classNames类名控制工具

简介:可以帮助我们更好、更直观地管理样式类名

安装后即可使用,案例:

import { useState } from "react";
import "./App.css";
import classNames from 'classnames';
// 项目的根组件
function App() {
  let [count, setCount] = useState(0)
  let style = {
    color: 'blue'
  }
  const handleMyButton = () => {
    setCount(count + 1)
  }
  return (
    <div className="App">
      {/* 行内样式,不推荐 */}
      <span style={style}>测试一下</span>
      {/* 通过类名设置样式 */}
      <button onClick={handleMyButton} className={classNames({ 'btn': true, 'odd': count % 2 === 0, 'even': count % 2 !== 0 })}>点击增加{count}</button>
    </div>
  );
}

export default App;

受控表单绑定

思路:

  • state绑定到input的value属性
  • 把input最新的value值设置给state
import { useState } from "react";
import "./App.css";
// import classNames from 'classnames';
// 项目的根组件
function App() {
  let [inputValue, setInputValue] = useState('');
  const inputChange = (e) => {
    setInputValue(e.target.value);
  }
  return (
    <div className="App">
      <input type="text" value={inputValue} onChange={inputChange} />
    </div>
  );
}

export default App;

React中获取Dom

在React中获取DOM需要使用useRef钩子函数

import { useState, useRef } from "react";
import "./App.css";
// import classNames from 'classnames';
// 项目的根组件
function App() {
  let [inputValue, setInputValue] = useState('');
  const inputChange = (e) => {
    setInputValue(e.target.value);
    console.log('input的dom', inputRef.current);
  }
  const inputRef = useRef(null);
  return (
    <div className="App">
      <input ref={inputRef} type="text" value={inputValue} onChange={inputChange} />
    </div>
  );
}

export default App;

组件通信

父传子-基础实现

  • 父组件传递数据-在子组件标签上绑定属性
  • 子组件接收数据-组件通过props参数接收数据
import { useState, useRef } from "react";
import "./App.css";

function Son(props) {
  // props:对象里面包含了父组件传递过来的所有数据
  console.log('props', props);
  return (
    <div>{props.name}</div>
  )
}
function App() {
  let name = "子组件名称"
  return (
    <div className="App">
      <Son name={name}></Son>
    </div>
  );
}

export default App;

父传子-props说明

  • props可以传递任意的数据(包括jsx)
  • props是只读对象,子组件只能读取props中的数据,不能直接进行修改,父组件的数据只能由父组件修改

父传子-特殊的prop:children

场景:当我们把内容嵌套在子组件标签中时,父组件会自动在名为children的prop属性中接收该内容3

import { useState, useRef } from "react";
import "./App.css";

function Son(props) {
  // props:对象里面包含了父组件传递过来的所有数据
  console.log('props', props);
  return (
    <div>{props.name}{props.children}</div>
  )
}
function App() {
  let name = "子组件名称"
  return (
    <div className="App">
      <Son name={name}>
        <span>this is a span</span>
      </Son>
    </div>
  );
}

export default App;

子传父

核心思路:在子组件中调用父组件中的函数并传递参数

import { useState, useRef } from "react";
import "./App.css";

function Son({ getSonMsg }) {
  const handleButton = () => {
    getSonMsg('this is sonMsg')
  }
  return (
    <button onClick={handleButton}>子组件按钮</button>
  )
}
function App() {
  const getSonMsg = (msg) => {
    console.log('msg', msg);
  }
  return (
    <div className="App">
      <Son getSonMsg={getSonMsg}></Son>
    </div>
  );
}

export default App;

使用状态提升实现兄弟组件通信

状态提升机制其实就是通过父组件进行兄弟组件之间的数据传递:A组件通过子传父的方式把数据传递给父组件,父组件再使用父传子的方式传递给B组件

案例:

import "./App.css";
import { useState } from "react";
// A组件
function A({ getAName }) {
  const handleClickBtn = () => {
    getAName('this is A name')
  }
  return (
    <button onClick={handleClickBtn}>send</button>
  )
}
// B组件
function B(props) {
  console.log('props---', props);
  return (
    <div>
      A组件传递的数据:{props.aName}
    </div>
  )
}

function App() {
  let [aName, setAName] = useState('')
  const getAName = (data) => {
    console.log('A组件传递的数据', data);
    setAName(data)
  }
  return (
    <div className="App">
      <A getAName={getAName}></A>
      <B aName={aName}></B>
    </div>
  );
}

export default App;

使用context机制跨层级传递数据(兄弟组件、跨层级组件都可以传值)

实现步骤:

  1. 使用createContext方法创建一个上下文对象Ctx
  2. 在顶层组件中通过Ctx.Provider组件提供数据
  3. 在底层组件中通过useContext钩子函数获取数据

useEffect

概念理解与基础使用

useEffect是一个Hook函数,用于在React组件创建不是由事件引起而是由渲染本身引起的操作,比如发送AJAX请求,更改DOM等。

image.png

语法:useEffect(()=>{},[])

  • 参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作
  • 参数2是一个数组(可选),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次

案例:在组件渲染完毕之后,立即从服务端获取频道列表数据并显示到页面中

import { useEffect, useState } from "react";
const URL = 'http://geek.itheima.net/v1_0/channels'
function App() {
  const [list, setList] = useState([])
  useEffect(() => {
    // 额外的操作,获取频道列表
    async function getList() {
      const res = await fetch(URL)
      const jsonRes = await res.json()
      setList(jsonRes.data.channels)
    }
    getList()
  }, [])
  return (
    <div className="App">
      <ul>
        {list.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </div>
  );
}

export default App;

useEffect依赖项参数说明

useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现

依赖项副作用函数执行时机
没有依赖项组件初始渲染+组件更新时执行
空数组依赖只在初始渲染时执行一次
添加特定依赖项组件初始渲染+特性依赖项变化时执行

清除副作用

在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用。

useEffect(() => {
    // 实现副作用操作逻辑
    return () => {
      // 清除副作用逻辑
    }
 }, [])

说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行

需求:在Son组件渲染时开启一个定时器,卸载时清除这个定时器

import { useEffect, useState } from "react";

function Son() {
  // 1.渲染时开启一个定时器
  useEffect(() => {
    setInterval(() => {
      console.log('定时器执行中----');
    }, 1000)
  }, [])
  return <div>this is son</div>
}

function App() {
  const [show, setShow] = useState(true)
  return (
    <div className="App">
      {show && <Son />}
      <button onClick={() => setShow(false)}>卸载Son组件</button>
    </div>
  );
}

export default App;

像上面这种情况哪怕卸载了Son组件,定时器依然存在,这时候就需要清除副作用

import { useEffect, useState } from "react";

function Son() {
  // 1.渲染时开启一个定时器
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('定时器执行中----');
    }, 1000)
    return () => {
      // 清除副作用(组件卸载时)
      clearInterval(timer)
    }
  }, [])
  return <div>this is son</div>
}

function App() {
  const [show, setShow] = useState(true)
  return (
    <div className="App">
      {show && <Son />}
      <button onClick={() => setShow(false)}>卸载Son组件</button>
    </div>
  );
}

export default App;

自定义hook函数

概念:自定义hook是以use打头的函数,通过自定义Hook函数可以用来实现逻辑的封装和复用

封装自定义hokk通用思路

  1. 声明一个use打头的函数
  2. 在函数体内封装可复用的逻辑(只要是可复用的逻辑)
  3. 把组件中用到的状态或者回调return出去(以对象或者数组)
  4. 在哪个组件中要用到这个逻辑,就执行这个函数,解构出来状态和回调进行使用

案例:

import { useState } from "react";

function useToggle() {
  const [state, setState] = useState(false)
  const handleToggle = () => {
    setState(!state)
  }
  return {
    state,
    handleToggle
  }
}


function App() {
  const { state, handleToggle } = useToggle()
  return (
    <div className="App">
      {state && <div>this is a div</div>}
      <button onClick={handleToggle}>toggle</button>
    </div>
  );
}

export default App;

React Hooks使用规则

  • 只能在组件中或者其他自定义Hook函数中调用
  • 只能在组件的顶层调用,不能嵌套在if、for、其他函数中

Redux

什么是Redux?

Redux是React最常用的集中状态管理工具,类似于Vue的Vuex和pinia,可以独立于框架运行

环境准备

在React中使用redux,官方要求安装两个插件-Redux Toolkit 和 react-redux

  1. Redux Toolkit(RTX)-官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式

优点如下:

  • 简化store的配置方式
  • 内置immer支持可变式状态修改
  • 内置thunk更好地异步创建
  1. react-redux-用来连接Redux和React组件的中间件 image.png

配置基础环境:npm i @reduxjs/toolkit react-redux

store目录结构设计

image.png

  1. 通常集中状态管理的部分都会单独创建一个单独的store目录
  2. 应用中有很多个store子模块,所以创建一个modules目录,在内部编写业务分类的子store
  3. store中的入口文件index.js的作用是组合modules中所有子模块,并导出store

案例-实现counter

思维导图: image.png

在Redux中修改数据的唯一方式是提交一个action

首先需要创建store counterStore.js

import { createSlice } from '@reduxjs/toolkit'

const counterStore = createSlice({
    name: 'counter',
    // 初始化state
    initialState: {
        count: 0
    },
    // 修改状态的方法 同步方法 支持直接修改
    reducers: {
        inscrement(state) {
            state.count++
        },
        decrement(state) {
            state.count--
        }
    }
})

// 解构出来actionCreater函数
const { inscrement, decrement } = counterStore.actions
// 获取reducer
const reducer = counterStore.reducer
// 以按需导出的方式导出actionCreater
export { inscrement, decrement }
// 以默认导出的方式导出reducer
export default reducer

store/index.js

import { configureStore } from "@reduxjs/toolkit";

// 导入子模块reducer
import counterReducer from './modules/counterStore'

const store = configureStore({
    reducer: {
        counter: counterReducer
    }
})

export default store

使用react-reduxReact注入store:react-redux负责把React与Redux连接起来,内置Provider组件通过store参数把创建好的store实例注入到应用中,链接正式建立

//React必要的两个核心包 
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 建立Redux与React的连接
import { Provider } from 'react-redux';
import store from './store'
// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

React组件使用store中的数据:在React组件中使用store中的数据,需要用到一个钩子函数-useSelector,它的作用是把store中的数据映射到组件中

import { useSelector } from "react-redux";
function App() {
  const { count } = useSelector(state => state.counter)
  return (
    <div className="App">
      {count}
    </div>
  );
}

export default App;

React组件修改store中的数据:React组件中修改store中的数据需要借助另外一个hook函数-useDispatch,它的作用是生成提交action对象的dispatch函数:

import { useSelector, useDispatch } from "react-redux";
// 导入actionCreater
import { inscrement, decrement } from './store/modules/counterStore'
function App() {
  const { count } = useSelector(state => state.counter)
  const dispatch = useDispatch()
  return (
    <div className="App">
      <button onClick={() => dispatch(decrement())}>-</button>
      {count}
      <button onClick={() => dispatch(inscrement())}>+</button>
    </div>
  );
}

export default App;

提交action传参

在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性image.png

异步状态操作

image.png

具体步骤:

  1. 创建store的写法保持不变,配置好同步修改状态的方法
  2. 单独封装一个函数,在函数内部return一个新函数,在新函数中封装异步请求获取数据,调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
  3. 组件中dispatch的写法保持不变

channelStore.js

import { createSlice } from '@reduxjs/toolkit'
import axios from 'axios'
const channelStore = createSlice({
    name: 'channel',
    // 初始化state
    initialState: {
        channelList: []
    },
    // 修改状态的方法 同步方法 支持直接修改
    reducers: {
        setChannels(state, action) {
            state.channelList = action.payload
        }
    }
})

// 异步请求部分
const { setChannels } = channelStore.actions
const fetchChannelList = () => {
    console.log('fetchChannelList');
    return async (dispatch) => {
        const res = await axios.get('http://geek.itheima.net/v1_0/channels')
        console.log('res-----', res);
        dispatch(setChannels(res.data.data.channels))
    }
}
export { fetchChannelList }

const reducer = channelStore.reducer

export default reducer

store/index.js

import { configureStore } from "@reduxjs/toolkit";
// 导入子模块reducer
import counterReducer from './modules/counterStore'
import channelReducer from './modules/channelStore'
const store = configureStore({
    reducer: {
        counter: counterReducer,
        channel: channelReducer
    }
})

export default store

App.js组件

import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
// 导入actionCreater
import { fetchChannelList } from './store/modules/channelStore'

function App() {
  const { channelList } = useSelector(state => state.channel)
  const dispatch = useDispatch()
  // 使用useEffect触发异步
  useEffect(() => {
    dispatch(fetchChannelList())
  }, [dispatch])
  return (
    <div className="App">
      <ul>
        {channelList.map(item => <li key={item.id}>{item.name}</li>)}
      </ul>
    </div>
  );
}

export default App;

React Router

快速开始

image.png

简单的一个路由案例:

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

// 1.创建router实例对象并且配置路由对应关系
const router = createBrowserRouter([
  {
    path: '/',
    element: <div>我是首页</div>
  },
  {
    path: '/login',
    element: <div>我是登陆页</div>
  },
  {
    path: '/article',
    element: <div>我是文章页</div>
  }
])

// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <RouterProvider router={router}></RouterProvider>
  </Provider>
);

抽象路由模块

image.png

第一步:将页面抽离为一个单独的模块

image.png 第二步: 创建路由模块

import Login from '../page/Login'
import Article from '../page/Article'

import { createBrowserRouter } from 'react-router-dom'

const router = createBrowserRouter([
    {
        path: '/',
        element: <div>首页</div>
    },
    {
        path: '/login',
        element: <Login />
    },
    {
        path: '/article',
        element: <Article />
    }
])

export default router

第三步:入口文件渲染RouterProvider

//React必要的两个核心包 
import React from 'react';
import ReactDOM from 'react-dom/client';
// 导入项目的根组件
import App from './App';
// 建立Redux与React的连接
import { Provider } from 'react-redux';
import store from './store'

import { RouterProvider } from 'react-router-dom';

import router from '../src/router'

// 把App根组件渲染到id为root的div元素中
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <RouterProvider router={router}></RouterProvider>
  </Provider>
);

路由导航跳转

声明式导航

image.png

import { Link } from 'react-router-dom'
const Login = () => {
    return (
        <div>
            我是登陆页
            <Link to="/article">跳转到文章页</Link>
        </div>
    )
}
export default Login

编程式导航

image.png

import { useNavigate } from "react-router-dom"
const Login = () => {
    const navigate = useNavigate()
    return (
        <div>
            我是登陆页
            <button onClick={() => navigate('/article')}>跳转到文章页</button>
        </div>
    )
}
export default Login

导航路由传参

image.png

searchParams传参

参数传递

import { useNavigate } from "react-router-dom"
const Login = () => {
    const navigate = useNavigate()
    return (
        <div>
            我是登陆页
            <button onClick={() => navigate('/article')}>跳转到文章页</button>
            <button onClick={() => navigate('/article?id=100&name=jack')}>searchParams传参</button>
        </div>
    )
}
export default Login

接收参数

import { useSearchParams } from "react-router-dom"
const Article = () => {
    const [params] = useSearchParams()
    let id = params.get('id')
    let name = params.get('name')
    return <div>我是文章页 id:{id} name:{name}</div>
}
export default Article

params传参

import { useNavigate } from "react-router-dom"
const Login = () => {
    const navigate = useNavigate()
    return (
        <div>
            我是登陆页
            <button onClick={() => navigate('/article')}>跳转到文章页</button>
            <button onClick={() => navigate('/article?id=100&name=jack')}>searchParams传参</button>
            <button onClick={() => navigate('/article/1001/jack')}>Params传参</button>
        </div>
    )
}
export default Login
import { useParams } from "react-router-dom"
const Article = () => {
    const params = useParams()
    let id = params.id
    let name = params.name
    return <div>我是文章页 id:{id} name:{name}</div>
}
export default Article

嵌套路由

image.png

默认二级路由配置

image.png

404路由配置

image.png

两种路由模式

image.png

项目实战技巧

配置别名路径@

image.png

路径解析配置

CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件--craco

配置步骤:

  1. 安装craco npm i -D @craco/craco
  2. 项目根目录下创建配置文件 craco.config.js
  3. 配置文件中添加路径解析配置
  4. 包文件中配置启动和打包命令

image.png

联想路径配置

image.png

{
    "compilerOptions": {
        "baseUrl": "./",
        "paths": {
            "@/*": [
                "src/*"
            ]
        }
    }
}

数据mock

image.png

json-server实现数据mock

json-server是一个node包,可以在不到30秒内获得零编码的完整的mock服务

实现步骤:

  1. 项目中安装json-server npm i -D json-server
  2. 准备一个json文件
  3. 添加启动命令
  4. 访问接口进行测试 image.png

useReducer

useReducer-基础用法

  1. 定义一个reducer函数(根据不同的action返回不同的新状态)
  2. 在组件中调用useReducer,并传入reducer函数和状态的初始值
  3. 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)

image.png

useReducer-分派action时传参

image.png

小结

image.png

useMemo

作用:在组件每次重新渲染的时候缓存计算的结果

背景

React的渲染机制:在React中,当组件的状态(state)或属性(props)发生变化时,React会触发组件的重新渲染,这是React的核心机制之一,确保组件能够响应数据的变化并更新UI。

可是这种渲染机制容易造成性能的浪费,组件只要有一个数据发生变化,整个组件都得重新渲染。

下面案例可以体现这一点:

import { useState, useEffect } from 'react';

function App() {
  const [count, setCount] = useState(100);
  const [curTime, setCurTime] = useState('');

  const useTime = () => {
    useEffect(() => {
      const intervalId = window.setInterval(() => {
        let time = new Date();
        setCurTime(time.toLocaleString());
      }, 1000);

      return () => {
        window.clearInterval(intervalId);
      };
    }, []);

    return curTime;
  };

  const time = useTime();

  const arr = () => {
    console.log('计算偶数数组');
    const temp = [];
    for (let i = 0; i < count; i++) {
      if (i % 2 === 0) {
        temp.push(i);
      }
    }
    return temp;
  };

  console.log('组件渲染');

  return (
    <div>
      <form>
        <div>{time}</div>
        <label htmlFor="num">Your number:</label>
        <input
          type="number"
          value={count}
          onChange={(event) => {
            // 设置最大值为 100000
            let num = Math.min(100_000, Number(event.target.value));
            setCount(num);
          }}
        />
      </form>
      <p>
        有{arr().length}个偶数在 0 到 {count} 之间:<span>{arr().join(', ')}</span>
      </p>
    </div>
  );
}

export default App;

计算偶数数组组件渲染日志都会间隔一秒就会打印,原因就在于curTime每秒发生了变化,导致整个组件得重新渲染,偶数就得重新计算,但是偶数的计算只依赖count值,所以造成了性能的浪费。

为了解决上述问题,useMemo应运而生。

useMemo基本介绍

useMemo(()=>{执行函数},[count1])
  • 接收两个参数,第一个是执行函数,第二个是依赖项

  • 把创建函数和依赖数组项作为参数传入 useMemo,它仅仅会在数组依赖项中的值改变时才会重新计算值

  • 这种优化有助于避免在每次渲染时都进行高开销的计算

  • useMemo 的函数在渲染期间执行,所以不该在此期间做的操作请去除

  • 如果没有提供依赖数据,每次都会重新计算值,相当于没有优化了

案例优化

import { useState, useEffect, useMemo } from 'react'
function App() {
  const [count, setCount] = useState(100);
  const [curTime, setCurTime] = useState('');

  const useTime = () => {
    useEffect(() => {
      const intervalId = window.setInterval(() => {
        let time = new Date();
        setCurTime(time.toLocaleString());
      }, 1000);

      return () => {
        window.clearInterval(intervalId);
      };
    }, []);

    return curTime;
  };
  const time = useTime();

  const arr = useMemo(() => {
    console.log('useMemo: 计算偶数数组');
    const temp = []
    for (let i = 0; i < count; i++) {
      if (i % 2 === 0) {
        temp.push(i);
      }
    }
    return temp
  }, [count]);
  console.log('组件渲染');

  return (
    <div>
      <form>
        <div>{time}</div>
        <label htmlFor="num">Your number:</label>
        <input
          type="number"
          value={count}
          onChange={(event) => {
            // 设置最大值为 100000
            let num = Math.min(100_000, Number(event.target.value));
            setCount(num);
          }}
        />
      </form>
      <p>
        有{arr.length}个偶数在 0 到 {count} 之间:<span>{arr.join(', ')}</span>
      </p>
    </div>
  );
}

export default App

加了useMemo就会发现,useMemo: 计算偶数数组这个日志只会因为count的变化而打印,不会因为整个组件的渲染而导致打印。

React.memo

作用:允许组件在Props没有改变的情况下跳过渲染

背景

React组件默认的渲染机制:只要父组件重新渲染子组件就会重新渲染。如果Son组件本身并不需要做渲染更新,就会存在浪费

只要点击父组件的按钮改变count值,我是子组件,我重新渲染了日志就会打印

import { useState } from "react";
function Son() {
  console.log('我是子组件,我重新渲染了');
  return <div>this is Son</div>
}

function App() {
  const [count, setCount] = useState(0)
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <Son />
    </div>
  );
}

export default App

React.memo-基础语法

const MemoComponent=memo(function SomeComponent(props){...})

说明:经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染

使用memo进行优化

我是子组件,我重新渲染了日志只会在初始化种打印一次,点击父组件按钮不会触发子组件的重新渲染

import { useState, memo } from "react";


const MemoSon = memo(function Son() {
  console.log('我是子组件,我重新渲染了');
  return <div>this is Son</div>
})

function App() {
  const [count, setCount] = useState(0)
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <MemoSon />
    </div>
  );
}

export default App

React.memo-props的比较机制

机制:在使用memo缓存组件之后,React会对每一个prop使用Object.is比较新值和旧值,返回true,表示没有变化

image.png

简单类型的prop机制我就不举例了,很容易理解。主要看一下引用类型的prop比较机制案例。

import { useState, memo } from "react";

const MemoSon = memo(function Son({ list }) {
  console.log('我是子组件,我重新渲染了');
  return <div>this is Son{list}</div>
})

function App() {
  const [count, setCount] = useState(0)
  const list = [1, 2, 3]
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <MemoSon list={list} />
    </div>
  );
}

export default App

按理说list就是一个固定的数组,可是当我点击父组件按钮时,我是子组件,我重新渲染了也会打印。原因在于当我点击按钮时,count值改变,父组件会重新渲染,const list = [1, 2, 3]也就会重新执行了,list的引用值就会改变,从而触发子组件的重新渲染。

所以为了解决这个问题,可以使用useMemo保证prop引用稳定,这个API可以在组件渲染的过程中缓存一个值

import { useState, memo, useMemo } from "react";


const MemoSon = memo(function Son({ list }) {
  console.log('我是子组件,我重新渲染了');
  return <div>this is Son{list}</div>
})

function App() {
  const [count, setCount] = useState(0)
  const list = useMemo(() => {
    return [1, 2, 3]
  }, [])
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>+{count}</button>
      <MemoSon list={list} />
    </div>
  );
}

export default App

useCallback

使用背景

memo组件的props如果是个引用类型,上文说可以使用useMemo来包裹父组件的props保证稳定,避免子组件进行不必要的渲染。但是如果下面这个场景怎么办,这个场景本质上是一个子传父,父组件传入的是一个函数。

import { useState, memo, useMemo } from "react";

const Input = memo(function Input({ onChange }) {
  console.log('子组件重新渲染了');
  return <input type="text" onChange={(e) => onChange(e.target.value)} />
})

function App() {
  // 传递给子组件的函数
  const changeHandler = (value) => console.log(value)
  // 触发父组件重新渲染的函数
  const [count, setCount] = useState(0)
  return (
    <div className="App">
      <Input onChange={changeHandler} />
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </div>
  );
}

export default App

为了保证changeHandler稳定,需要使用useCallback

基础用法

作用:在组件多次渲染时缓存函数

const changeHandler = useCallback((value) => console.log(value), [])

这样就可以避免子组件多余的重复渲染。

React.forwardRef

使用ref暴露DOM节点给父组件

forwardRef-场景说明

image.png

默认情况父组件拿不到子组件的dom

import { forwardRef, useRef } from "react";

// 子组件
function Son() {
  return <input type="text" />
}

// 父组件
function App() {
  const sonRef = useRef(null)
  const showRef = () => {
    console.log(sonRef); //默认情况下拿不到子组件的dom
  }
  return (
    <div className="App">
      <Son ref={sonRef}></Son>
      <button onClick={showRef}>focus</button>
    </div>
  );
}

export default App

所以就需要用到forwardRef

import { forwardRef, useRef } from "react";

// 子组件
const Son = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />
})
// 父组件
function App() {
  const sonRef = useRef(null)
  const showRef = () => {
    console.log(sonRef); //默认情况下拿不到子组件的dom
    sonRef.current.focus()
  }
  return (
    <div className="App">
      <Son ref={sonRef}></Son>
      <button onClick={showRef}>focus</button>
    </div>
  );
}

export default App

useInperativeHandle

通过ref暴露子组件中的方法

场景

通过ref调用子组件内部的focus方法实现聚焦 image.png

import { forwardRef, useRef, useImperativeHandle } from "react";

// 子组件
const Son = forwardRef((props, ref) => {
  // 实现聚焦逻辑
  const inputRef = useRef(null)
  const focusHandler = () => {
    inputRef.current.focus()
  }
  // 把聚焦方法暴露出去
  useImperativeHandle(ref, () => {
    return {
      focusHandler
    }
  })
  return <input type="text" ref={inputRef} />
})
// 父组件
function App() {
  const sonRef = useRef(null)
  const showRef = () => {
    console.log(sonRef); //暴露的方法在这里
    sonRef.current.focusHandler()
  }
  return (
    <div className="App">
      <Son ref={sonRef}></Son>
      <button onClick={showRef}>focus</button>
    </div>
  );
}

export default App

class类组件

类组件基础结构

类组件就是通过JS中的类来组织组件的代码

  1. 通过类属性state定义状态数据
  2. 通过setState方法来修改状态数据
  3. 通过render来写UI模板(JSX语法一致)
class Counter extends Component {
  // 1.定义状态变量
  state = {
    count: 0
  }
  // 2.定义事件回调修改状态数据
  setCount = () => {
    // 修改状态数据
    this.setState({
      count: this.state.count + 1
    })
  }
  render() {
    return <button onClick={this.setCount}>{this.state.count}</button>
  }
}

类组件的生命周期函数

image.png

类组件的组件通信

概念:类组件和Hooks编写的组件在组件通信的思想上完全一致

image.png

zustand

后面再学