项目构建命令
npx create-react-app react-basic
npx:node语法
create-react-app:项目模板
react-basic:项目名称
项目结构
项目打包和本地预览
- 项目打包
npm run build
- 本地预览(模拟服务器运行项目)
- 安装本地服务包
npm i -g serve
- 指定服务启动路径,如./build:
serve -s ./build
- 安装本地服务包
打包优化 - CDN配置
- craco.config.js
javascript">// 扩展webpack的配置const path = require("path");
// 引入辅助函数
const { whenProd, getPlugin, pluginByName } = require("@craco/craco");module.exports = {// webpack 配置webpack: {// 配置别名alias: {// 约定:使用 @ 表示 src 文件所在路径"@": path.resolve(__dirname, "src"),},// 配置CDNconfigure: (webpackConfig) => {let cdn;whenProd(() => {// key: 不参与打包的包(由dependencies依赖项中的key决定)// value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下webpackConfig.externals = {react: "React","react-dom": "ReactDOM",};// 配置现成的cdn资源地址// 实际开发的时候 用公司自己花钱买的cdn服务器cdn = {js: ["https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js","https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js",],};});// 通过 htmlWebpackPlugin插件 在public/index.html注入cdn资源urlconst { isFound, match } = getPlugin(webpackConfig,pluginByName("HtmlWebpackPlugin"));if (isFound) {// 找到了HtmlWebpackPlugin的插件match.options.cdn = cdn;}return webpackConfig;},},
};
- index.html:动态创建script标签,导入js文件
<body><div id="root"></div><% htmlWebpackPlugin.options.cdn.js.forEach(cdnURL=> { %><script src="<%= cdnURL %>"></script><% }) %>
</body>
包体积可视化分析
- 安装插件:
npm i source-map-explorer
- 配置命令指定要分析的js文件:一般为打包后的js文件
package.json:
"scripts": {"analyze": "source-map-explorer 'build/static/js/*.js'"
},
配置路径别名
CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件 - craco
配置步骤:
- 安装craco:
npm i -D @craco/craco
- 项目根目录下创建配置文件
craco.config.js
- 配置文件中添加路径解析配置
- 包文件中配置启动和打包命令
javascript">const path = require('path')module.exports = {// webpack配置webpack: {// 配置别名alias: {// 约定:使用 @ 表示 src 文件所在路径'@': path.resolve(__dirname, 'src'),},},
}
"scripts": {"start": "craco start","build": "craco build"
},
联系路径配置
VsCode的联想配置,需要我们在项目目录下添加jsconfig.json文件,加入配置之后Vscode会自定读取配置帮助我们自动联想提示
配置步骤:
- 根目录下新增配置文件 - jsconfig.json
- 添加路径提示配置
javascript">{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}}
}
React调试插件
浏览器插件:React Developer Tools
JSX基础
概念和本质
概念:jsx是javaScript和XML(HTML)的缩写,表示在js代码中编写html模板结构,它是react中编写UI模板的方式
本质:JSX并不是标准的js语法,它是js的语法扩展,浏览器本身不能识别,需要通过解析工具做解析之后才能在浏览器中运行
优势:
- HTML的声明式模板语法
- JS的可编程能力
JSX中使用JS表达式
在JSX中可以通过 大括号语法{} 识别JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等
- 使用引号传递字符串
- 使用JavaScript变量
- 函数调用和方法调用
- 使用JavaScript对象
注意:if语句、switch语句、变量声明属于语句,不是表达式,不能出现在{}中
javascript">const count = 100const getName = () => {return 'Bob'
}
function App() {return (<div className="App">{/* 使用引号传递字符串 */}{ '这是一个字符串' }{/* 使用JavaScript变量 */}{ count }{/* 函数调用 */}{ getName() }{/* 方法调用 */}{ new Date().getDate() }{/* 使用JavaScript对象 */}<div style={ {color:'red',fontSize:'30px'} }>this is div</div></div>);
}export default App;
JSX中实现列表渲染
语法:在JSX中可以使用原生JS中的map方法遍历渲染列表
- 循环哪个结构在map中就return哪个结构
- 结构上需要加上key字段,可以是字符串、number类型
- key的作用:React框架内部使用,提升更新性能
javascript">const list = [{id:1,name:'Vue'},{id:2,name:'React'},{id:3,name:'Angle'}
]
function App() {return (<div className="App">{/* 渲染列表 */}<ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul></div>);
}export default App;
JSX实现条件渲染
语法:在React中,可以通过逻辑与运算符&&、三元里表达式(?:)实现基础的条件渲染
{flag && <span>this is span</span>}
:当flag为false时,不显示;当flag为true时,显示标签
{loading ? <span>loading...</span> : <span>this is span</span>}
:当loading为true时显示第一个标签;当loading为false时显示第二个标签
javascript">const isLogin = true
function App() {return (<div className="App">{/* 条件渲染 */}{/* 逻辑与 && */}{isLogin && <div>未登录</div>}{/* 三元运算符 */}{isLogin ? <div>已登录(jack)</div> : <div>用户未登录</div>}</div>);
}export default App;
复杂的条件渲染:
javascript">const count = 1 // 0 1 2const getCount = () => {if(count === 0){return '当前为0'}else if(count === 1){return '当前为1'}else if(count === 2){return '当前为2'}
}
function App() {return (<div className="App">{/* 复杂的条件渲染 */}{getCount()}</div>);
}export default App;
事件绑定
基础事件绑定
语法:on+事件名称={事件处理程序},整体上遵循驼峰命名法
javascript">function App() {const handleClick = () => {console.log('BUTTON被点击了')}return (<div className="App">{/* 基础事件绑定 */}<button onClick={handleClick}>click事件</button></div>);
}export default App;
使用事件对象参数
语法:在事件回调函数中设置形参e
javascript">function App() {const handleClick1 = (e) => {console.log('button被点击了',e)}return (<div className="App">{/* 使用事件对象参数e */}<button onClick={handleClick1}>click事件</button></div>);
}export default App;
传递自定义参数
语法:事件绑定的位置改造成箭头函数的写法,在执行实际处理业务函数的时候传递实参
javascript">function App() {const handleClick2 = (name) => {console.log('button被点击了',name)}return (<div className="App">{/* 传递自定义参数 */}<button onClick={() => handleClick2('bob')}>click事件</button></div>);
}export default App;
同时传递事件对象和自定义参数
语法:在事件绑定的位置传递事件实参e和自定义参数,clickHandler中声明形参,注意顺序对应
javascript">function App() {const handleClick3 = (name,e) => {console.log('button被点击了',name,e)}return (<div className="App">{/* 同时传递自定义参数和事件对象 */}<button onClick={(e) => handleClick3('bob',e)}>click事件</button></div>);
}export default App;
组件基础使用
概念:一个组件就是用户界面的一部分,它可以有自己的逻辑和外观,组件之间可以互相嵌套,也可以复用多次
语法:在React中,一个组件就是首字母大写的函数,内部存放了组件的逻辑和视图UI,渲染组件只需要把组件当成标签书写即可
javascript">// 定义组件
function Button(){// 组件业务逻辑return <button>click me</button>
}function App() {return (<div className="App">{/* 使用组件方式: */}{/* 1.自闭和 */}<Button/>{/* 2.成对标签 */}<Button></Button></div>);
}export default App;
useState基础使用
:::info
useState是一个React Hook(函数),它允许我们向组件添加一个状态变量,从而控制影响组建的渲染结果
本质:和普通的JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)
:::
- useState是一个函数,返回值是一个数组
- 数组中的第一个参数是状态变量,第二个参数时set函数用来修改状态变量
- useState的参数将作为count的初始值
set函数的作用:
- 使用新值更新状态变量
- 使用更新后的状态变量重新渲染UI
javascript">import { useState } from "react";function App() {// 1. 调用useState函数创建一个状态变量// count:状态变量// setCount:修改状态变量的方法const [count,setCount] = useState(0)// 2. 点击事件回调const handleClick = () => {// 作用:// 1. 用传入的新值修改count// 2. 使用新的count重新渲染UIsetCount(count+1)}return (<div className="App"><button onClick={handleClick}>{count}</button></div>);
}export default App;
修改状态的规则
- 状态不可变:在React中,状态被认为是只读的,我们应该始终替换它而不是修改它,直接修改状态不能引发视图更新
- 修改对象状态:对于对象类型的状态变量,应该始终传给set方法一个全新的对象来进行修改
javascript">import { useState } from "react";function App() {let [count,setCount] = useState(0)const handleClick = () => {// 错误写法:直接修改// 1. 值发生了变化// 2. 但无法引发视图更新// count++// console.log(count) // 自增// 正确写法:set函数// 1. 值发生了变化// 2. 视图重新渲染setCount(count + 1)}// 修改对象状态const [form,setForm] = useState({name:'bob'})const changeForm = () => {// 错误写法:直接修改// 1. 值发生了变化// 2. 但无法引发视图更新// form.name = 'tom'// console.log(form) // tom// 正确写法:set函数// 1. 值发生了变化// 2. 视图重新渲染setForm({...form,name:'Tom'})}return (<div className="App"><button onClick={handleClick}>{count}</button><button onClick={changeForm}>修改form-{form.name}</button></div>);
}export default App;
基础样式控制
React组件基础的样式控制有两种方式:
- 行内样式 — 不推荐
- class类名控制,注意不能使用class,而是使用className — 推荐
.foo{color: blue;
}
javascript">// 导入样式文件
import './index.css'const myStyle = {color:'yellow',fontSize: '50px'
}
function App() {return (<div className="App">{/* 行内样式 */}<span style={{color:'red',fontSize:'40px'}}>this is span</span><span style={myStyle}>this is span</span>{/* class类名控制 */}<span className='foo'>this is class</span></div>);
}export default App;
classnames优化类名控制
classnames是一个简单的JS库,可以非常方便的通过条件动态控制class类名显示
安装:npm install classnames
不使用classnames时,动态控制class类名:采用字符串拼接的方式
javascript"><span className={`tab ${item.type === type && 'active'}`}>bob</span>
使用classnames时,动态控制class类名:
javascript"><span className={classNames('tab',{active: item.type === type})}>bob</span>
表单受控绑定
概念:使用React组件的状态(useState)控制表单的状态
javascript">import { useState } from 'react';function App() {const [value,setValue] = useState('')return (<div className="App">{/* 通过value属性绑定状态,通过onChange属性绑定状态同步的函数 */}<input value={value} type='text' onChange={(e) => setValue(e.target.value)}/><div>{value}</div></div>);
}export default App;
获取DOM
API:useRef(null)
使用方法:
- 使用useRef生成ref对象,绑定到dom标签上
const inputRef = useRef(null)
<input type='text' ref={inputRef}/>
- dom可用时,ref.current获取dom,注意:dom在渲染完毕(dom生成)之后才可用
inputRef.current
javascript">// 导入样式文件
import { useRef } from 'react';function App() {// 1. 使用useRef生成ref对象,绑定到dom标签上const inputRef = useRef(null)const showDom = () => {// 2. dom可用时,ref.current获取dom// dom在渲染完毕(dom生成)之后才可用console.dir(inputRef.current)}return (<div className="App"><input type='text' ref={inputRef}/><button onClick={showDom}>获取Dom</button></div>);
}export default App;
组件通信
props说明
- props可传递数据类型:
数字、字符串、布尔值、数组、对象、函数、JSX
- props是只读对象
子组件只能读取props中的数据,不能直接进行修改,父组件的数据只能由父组件修改
javascript">function Son(props){console.log(props)// props.age = 32 错误代码,TypeError: Cannot assign to read only property 'age' of object '#<Object>'return (<div>this is Son App,{props.name}</div>)
}function App() {const name = 'this is App'return (<div className="App"><Son name={name} age={18} isTrue={true} list={['vue','react']} obj={{name:'bob'}} cb={() => console.log(123)} child={<span>this is span child</span>}/></div>);
}export default App;
特殊的prop - children
场景:当我们把内容嵌套在子组件标签中时,父组件会自动在名为children的prop属性中接收该内容
语法:
// 子组件中:
function Son(props){return (<div>{props.children}</div>)
}// 父组件中:
<Son><span>this is span</span>
</Son>
比如:
javascript">function Son(props){// 2. 子组件通过props参数接收数据console.log(props)return (<div>this is Son App,{props.name},{props.children}</div>)
}function App() {const name = 'this is App'return (<div className="App">{/* 1. 在子组件标签上绑定属性 */}<Son name={name}><span>this is span</span></Son></div>);
}export default App;
父传子
实现步骤:
- 父组件传递数据 - 在子组件标签上绑定属性
- 子组件接收数据 - 子组件通过props参数接收数据
javascript">function Son(props){// 2. 子组件通过props参数接收数据return (<div>this is Son App,{props.name}</div>)
}function App() {const name = 'this is App'return (<div className="App">{/* 1. 在子组件标签上绑定属性 */}<Son name={name}/></div>);
}export default App;
子传父
实现核心:在子组件中调用父组件中的函数并传递参数
语法:
// 子组件:
function Son(props){const {onGetSonMsg,onGetSonName} = propsreturn <button onClick={() => onGetSonMsg(sonMsg) }>点击</button>
}// 父组件:
const getSonMsg = (msg) => {// 接收子组件传递的参数console.log(msg) // this is son msg
}
<Son name={name} onGetSonMsg={getSonMsg}/>
比如:
javascript">import { useState } from "react"function Son(props){// 2. 子组件通过props参数接收数据const {onGetSonMsg,onGetSonName} = propsconsole.log(props)const name = 'this is Son'const sonMsg = 'this is son msg'return (<div>this is Son<div>{props.name}</div>{/* 3. 触发父组件传递过来的函数,并传递参数 */}<button onClick={() => onGetSonMsg(sonMsg) }>点击</button><button onClick={() => onGetSonName(name)}>点击2</button></div>)
}function App() {const [sonName,setSonName] = useState('')const name = 'this is App'const getSonMsg = (msg) => {// 4. 接收子组件传递的参数console.log(msg) // this is son msg}const getSonName = (sname) => {// 4. 接收子组件传递的参数setSonName(sname)}return (<div className="App">{/* 1. 在子组件标签上绑定属性 */}<Son name={name} onGetSonMsg={getSonMsg} onGetSonName={getSonName}/><div>{sonName}</div></div>);
}export default App;
使用状态提升实现兄弟组件通信
实现思路:A(子组件) —> App(父组件) —> B(子组件)
实现步骤:
- A组件先通过子传父的方式把数据传给父组件App
- App拿到数据后通过父传子的方式再传递给B组件
javascript">import { useState } from "react"function A({onGetName}) {const aName = 'this is A'return (<div>this is A{/* 1.将数据传递父组件App */}<button onClick={() => onGetName(aName)}>send</button></div>)
}function B(props) {const {name} = propsreturn (<div>this is B<div>{name}</div></div>)
}function App() {const [name,setName] = useState('')const getName = (aName) => {setName(aName)}return (<div className="App"><A onGetName={getName}/>{/* 2.将数据传递给子组件B */}<B name={name}/></div>);
}export default App;
使用Context机制跨层级组件通信
语法:createContext
、useContext
实现步骤:
- 使用createContext方法创建一个上下文对象Ctx
- 在顶层组件(App)中通过Ctx.Provider组件提供数据
- 在底层组件(B)中通过useContext钩子函数获取数据
import { createContext,useContext } from "react"// 1. 使用createContext创建一个上下文对象
const MsgContext = createContext()function B() {// 3. 在底层组件中通过useContext钩子函数使用数据const msg = useContext(MsgContext)return (<div>this is B<div>{msg}</div></div>)
}function A() {return (<div>this is A<B/></div>)
}function App() {const msg = 'this is App msg'return (<div className="App">{/* 2. 在顶层组件中通过Provider组件提供数据 */}<MsgContext.Provider value={msg}>this is App<A/></MsgContext.Provider></div>);
}export default App;
传递状态(useState):
javascript">import { createContext,useContext,useState } from "react"// 1. 使用createContext创建一个上下文对象
const NameContext = createContext()function B() {// 3. 在底层组件中通过useContext钩子函数使用数据const name = useContext(NameContext)return (<div>this is B<div>{name}</div></div>)
}function A() {return (<div>this is A<B/></div>)
}function App() {const [name,setName] = useState('bob')return (<div className="App">{/* 2. 在顶层组件中通过Provider组件提供数据 */}<NameContext.Provider value={name}>this is App<button onClick={() => setName('tom')}>改变name</button><A/></NameContext.Provider></div>);
}export default App;
useEffect
概念理解和基础使用
概念:useEffect是一个React Hook函数,用于在React组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送Ajax请求,更改Dom等等
语法: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()console.log(jsonRes)setList(jsonRes.data.channels)}getList()},[])return (<div className="App">this is App<ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul></div>);
}export default App;
依赖项参数说明
useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现
依赖项 | 副作用函数执行时机 |
---|---|
没有依赖项 | 组件初始渲染 + 组件更新时执行(如状态变化等) |
空数组依赖 | 只在初始渲染时执行一次 |
添加特定依赖项 | 组件初始渲染 + 特性依赖项变化时执行 |
import { useEffect, useState } from "react";function App() {// 1. 没有依赖项:初始 + 组件更新const [count, setCount] = useState(0)// 改变count会触发,组件发生变化都会触发useEffect(() => {console.log('没有依赖项的副作用函数')})// 2. 传入空数组依赖:初始执行一次// 只在初始执行一次useEffect(() => {console.log('空数组的副作用函数')},[])// 3. 传入特定依赖项:初始 + 依赖项变化时执行const [name,setName] = useState('Tom')// 改变name会触发,同时也会触发第一个useEffectuseEffect(() => {console.log('传入特定依赖项的副作用函数')},[name])return (<div className="App">this is App<button onClick={() => setCount(count + 1)}>+{count}</button><button onClick={() => setName(name + '+')}>update:{name}</button></div>);
}export default App;
清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行
import { useEffect, useState } from "react";function Son(){// 在初始执行一次// 实现副作用操作逻辑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>this is App{show && <Son/>}<button onClick={() => setShow(false)}>销毁Son组件</button></div>);
}export default App;
自定义Hook实现
概念:zidingyiHook是以use打头的函数,通过自定义Hook函数可以用来实现逻辑的封装和复用
实现步骤:
- 声明一个以use打头的函数
- 在函数体内封装可复用的逻辑
- 把组件中要用的状态或函数return出去,以对象或者数组的形式
import {useState } from "react";// 自定义的Hook
function useToggle(){const [show,setShow] = useState(true)function toggle (){setShow(!show)}return {show,toggle}
}function App() {const {show,toggle} = useToggle()return (<div>this is App{show && <h2>显示与隐藏</h2>}<button onClick={toggle}>切换</button></div>);
}export default App;
ReactHooks使用规则
使用规则
- 只能在组件中或者其他自定义Hook函数中调用
- 只能在组件的顶层调用,不能嵌套在if、for、其他函数中
import {useState } from "react";// 错误的用法1:
// useState() 报错function App() {if(Math.random > 5){// 错误的用法2:// useState() 报错}return (<div>this is App</div>);
}export default App;
Redux
什么是Redux?
Redux是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
Redux管理数据流程梳理
Redux把整个数据修改的流程分成了三个核心概念,分别是:state、action、reducer
- state - 一个对象 存放着我们管理的数据状态
- action - 一个对象 用来描述你想怎么修改数据
- reducer - 一个函数 根据action的描述生成一个新的state
配置工具
在React中使用redux,官方要求安装两个其他插件 - Redux Toolkit 和 react-redux
- Redux ToolKit (RTK) - 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式
- 简化store的配置方式
- 内置immer支持可变式状态修改
- 内置thunk更好的异步创建
- react-redux - 用来链接Redux和React组件的中间件
配置基础环境
- 使用CRA快速创建React项目
npx create-react-app react-redux
- 安装配置工具
npm i @reduxjs/toolkit react-redux
- 启动项目
npm run start
store目录结构设计
- 通常集中状态管理的部分都会单独创建一个单独的‘store’目录
- 应用通常会有很多个子store模块,所以创建一个‘modules’目录,在内部编写业务分类的子store
- store中的入口文件index.js的作用是组合modules中所有的子模块,并导出store
Redux使用步骤
使用React Toolkit创建counterStore
- initialState:仓库,store存放位置
- reducres:修改状态唯一的方法,同步方法
javascript">import { createSlice } from "@reduxjs/toolkit"const countStore = createSlice({name:'couter',// 初始化stateinitialState:{count:0},// 修改状态的方法 同步方法 支持直接修改reducers:{inscrement(state){state.count++},decrement(state){state.count--},addToNum(state,action){// 传递的参数都在action的payload属性中state.count = action.payload}}
})// 解构出actionCreater函数
const {inscrement,decrement,addToNum} = countStore.actions
// 获取reducer
const reducer = countStore.reducer// 以按需导出的方式导出actionCreater
export {inscrement,decrement,addToNum}
// 已默认导出的方式导出reducer
export default reducer
为React注入store
通过react-redux
内置Provider
组件将store实例注入应用中
主要代码:
<Provider store={store}><App />
</Provider>
完整代码:
javascript">import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';// 导入store
import store from './store';
import { Provider } from 'react-redux';const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(// 注入store<Provider store={store}><App /></Provider>
);// 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();
React组件使用store中的数据
通过钩子函数useSelector
把store中的数据映射到组件中
主要代码:
const {count} = useSelector(state => state.counter)
完整代码:
import { useSelector } from "react-redux";function App() {// 使用store中的数据const {count} = useSelector(state => state.counter)return (<div className="App"><span>{count}</span></div>);
}export default App;
React组件修改store中的数据
通过hook函数useDispatch
生成提交action对象的dispatch函数
主要代码:
// 1.定义reducers:
reducers:{decrement(state){state.count--}
}// 2:
const dispatch = useDispatch()// 3:调用reducers
<button onClick={() => dispatch(decrement())}>-</button>
完整代码:
import { useDispatch, useSelector } from "react-redux";
// 导入创建action对象的方法
import { decrement, inscrement } from "./store/modules/counterStore";function App() {// 使用store中的数据const {count} = useSelector(state => state.counter)// 使用dispatch函数修改storeconst dispatch = useDispatch()return (<div className="App">{/* 掉用dispatch提交action对象:修改store */}<button onClick={() => dispatch(decrement())}>-</button><span>{count}</span><button onClick={() => dispatch(inscrement())}>+</button></div>);
}export default App;
提交action传参
在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性上
主要语法:
// 1.定义reducers:
reducers:{addToNum(state,action){// 传递的参数都在action的payload属性中state.count = action.payload}
}// 2.使用reducers:
const dispatch = useDispatch()// 3.传递参数:
<button onClick={() => dispatch(addToNum(10))}>add to 10</button>
完整代码:
import { useDispatch, useSelector } from "react-redux";// 导入创建action对象的方法
import { addToNum } from "./store/modules/counterStore";function App() {// 使用store中的数据const {count} = useSelector(state => state.counter) // 使用dispatch函数修改storeconst dispatch = useDispatch()return (<div className="App"><span>{count}</span>{/* 掉用dispatch提交action对象:修改store */}{/* 传递参数 */}<button onClick={() => dispatch(addToNum(10))}>add to 10</button><button onClick={() => dispatch(addToNum(20))}>add to 20</button></div>);
}export default App;
异步操作Store
- 创建store的写法保持不变,配置好同步修改状态的方法
- 单独封装一个函数,在函数内部return一个函数,在新函数中
- 封装异步请求获取数据
- 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
- 组件中dispatch的写法保持不变
主要代码:
javascript">// 1.定义reducers:
reducers:{setChannels(state,action){state.channelList = action.payload}
}
// 2.在store文件中封装异步请求(channelStore.js):
const fetchChannelList = () => {// dispatch是一个固定参数return async dispatch => {const res = await axios.get('http://geek.itheima.net/v1_0/channels')dispatch(setChannels(res.data.data.channels))}
}// 修改状态:
// 3.使用dispatch函数修改store
const dispatch = useDispatch()// 4.使用useEffect触发异步请求执行:
useEffect(() => {dispatch(fetchChannelList())
}, [dispatch])
完整代码:
javascript">import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";const channelStore = createSlice({name:'channel',initialState:{channelList:[]},reducers:{setChannels(state,action){state.channelList = action.payload}}
})// 异步请求 - 主要代码
const {setChannels} = channelStore.actionsconst fetchChannelList = () => {// dispatch是一个固定参数return async dispatch => {const res = await axios.get('http://geek.itheima.net/v1_0/channels')dispatch(setChannels(res.data.data.channels))}
}
// 同步代码导出setChannels,异步代码导出fetchChannelList
export {fetchChannelList}const reducer = channelStore.reducerexport default reducer
javascript">import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";// 导入创建action对象的方法
import { fetchChannelList } from "./store/modules/channelStore";function App() {// 使用store中的数据const {channelList} = useSelector(state => state.channel) // 使用dispatch函数修改storeconst 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;
Redux调试工具
浏览器插件:Redux DevTools
ReactRouter
什么是前端路由
一个路径path对应一个组件component当我们在浏览器中访问一个path的时候,path对应的组件会在页面中进行渲染
安装react-router-dom
npm i react-router-dom
简单使用
- 创建路由
javascript">import Login from "../page/Login"
import Article from "../page/Article"import {createBrowserRouter} from 'react-router-dom'// 创建路由
const router = createBrowserRouter([{path: '/login',element: <Login />},{path: '/article',element: <Article />}
])export default router
- 绑定路由
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {RouterProvider} from 'react-router-dom'// 1. 导入路由
import router from './router';const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(// 2. 绑定路由<RouterProvider router={router}></RouterProvider>
);// 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();
路由懒加载
- 使用React内置的lazy函数进行导入
const Home = lazy(() => import("@/pages/Home"))
- 使用React内置的Suspense组件包裹路由中的element选项对应的组件
<Suspense><Home /></Suspense>
javascript">import { createBrowserRouter } from "react-router-dom";
import Layout from "@/pages/Layout";
import AuthRoute from "@/components/AuthRoute";
import { lazy, Suspense } from "react";// 路由懒加载
// 1. lazy函数对组件进行导入
const Home = lazy(() => import("@/pages/Home"));const router = createBrowserRouter([{path: "/",element: (<AuthRoute><Layout /></AuthRoute>),children: [{index: true,element: (<Suspense><Home /></Suspense> // 2. 使用suspense组件包裹),},],},
]);export default router;
路由导航跳转
什么是路由导航
路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信
声明式导航
声明式导航是指通过在模块中通过<Link/>
组件描述出要跳转到哪里去,<Link/>
组件会被渲染为浏览器支持的a链接
语法:<Link to="/article">文章</Link>
- to属性:指定跳转的路由path
- 传参:字符串拼接参数,如/login?id=1001&name=bob
import {Link} from 'react-router-dom'
function Login(){return (<div>这是登录页{/* 声明式导航 */}<Link to="/article">去文章页</Link></div>)
}export default Login
编程式导航
编程式导航是指通过useNavigate
钩子得到导航方法,然后通过调用方式以命令式的形式进行路由跳转
语法:
const navigate = useNavigate()
navigate('/article')
import {useNavigate} from 'react-router-dom'
function Login(){const navigate = useNavigate()return (<div>这是登录页{/* 编程式导航 */}<button onClick={() => navigate('/article')}>去文章页</button></div>)
}export default Login
路由导航传参
searchParams传参
主要代码:
// 传递:
navigate('/article?id=1001&name=jack')// 接收:
const [params] = useSearchParams()
let id = params.get('id')
let name = params.get('name')
例如:
import {useNavigate} from 'react-router-dom'
function Login(){const navigate = useNavigate()return (<div>这是登录页{/* searchParams传参 */}<button onClick={() => navigate('/article?id=1001&name=jack')}>去文章页</button></div>)
}export default Login
import { useSearchParams } from "react-router-dom"
function Article(){// 接收searchParams传参const [params] = useSearchParams()let id = params.get('id')let name = params.get('name')return (<div>这是文章页{id}-{name}</div>)
}export default Article
params传参
注意:params传参需要在路由定义时,添加占位参数
主要代码:
// 路由中定义参数:
const router = createBrowserRouter([{path: '/article/:id',element: <Article />}
])// 传递:
navigate('/article/1001')// 接收:
const params = useParams()
let id = params.id
例如:
javascript">import Login from "../page/Login"
import Article from "../page/Article"import {createBrowserRouter} from 'react-router-dom'const router = createBrowserRouter([{path: '/login',element: <Login />},{path: '/article/:id',element: <Article />}
])export default router
javascript">import {useNavigate} from 'react-router-dom'
function Login(){const navigate = useNavigate()return (<div>这是登录页{/* params传参 */}<button onClick={() => navigate('/article/1001')}>去文章页</button></div>)
}export default Login
javascript">import { useParams } from "react-router-dom"
function Article(){// 接收params传参const params = useParams()let id = params.idreturn (<div>这是文章页{id}</div>)
}export default Article
嵌套路由配置
什么是嵌套路由
在一级路由中又内嵌了其他路由,这种关系就叫做嵌套路由,嵌套至一级路由内的路由又称作二级路由
实现步骤:
- 使用children属性配置路由嵌套关系
- 使用
<Outlet>
组件配置二级路由渲染位置
const router = createBrowserRouter([{path:'/',element: <Layout/>,children:[{path:'/board',element: <Board/>},{path: '/about',element: <About/>}]}
])
javascript">import {Outlet,Link} from 'react-router-dom'function Layout(){return (<div>这是一级路由组件Layout<Link to="/about">去about页</Link><Link to="/board">去board页</Link>{/* 配置二级路由组件渲染位置 */}<Outlet/></div>)
}export default Layout
默认二级路由配置
场景和配置方式:当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置index属性为true
下例代码解析:当访问localhost:3000时,会渲染Layout组件,同时也会渲染二级组件Board;访问localhost:3000/board时,会报404错误
javascript">const router = createBrowserRouter([{path:'/',element: <Layout/>,children:[// 设置默认二级路由:一级路由访问时,默认二级路由组件也会渲染{index: true,element: <Board/>},{path: '/about',element: <About/>}]}
])
404路由配置
场景:当浏览器输入url的路径在整个路由配置中都找不到对应的path,为了用户体验,可以使用404兜底组件进行渲染
实现步骤:
- 准备一个NotFound组件
- 在路由表数组的末尾,以*号作为路由path配置路由
javascript">const router = createBrowserRouter([...code// 配置404页面{path:'*',element: <NotFound/>}
])
Hash和History路由模式
常用的路由模式有两种,history模式和hash模式,ReactRouter分别由createBrowerRouter(history路由)
和createHashRouter(hash路由)
函数负责创建
路由模式 | url表现 | 底层原理 | 是否需要后端支持 |
---|---|---|---|
history | url/login | history对象 + pushState事件 | 需要 |
hash | url/#/login | 监听hashChange事件 | 不需要 |
javascript">const router = createHashRouter([{path: '/login',element: <Login />},{path: '/article/:id',element: <Article />},
])
javascript">const router = createBrowserRouter([{path: '/login',element: <Login />},{path: '/article/:id',element: <Article />}
])
useReducer
- 定义一个reducer函数(根据不同的action返回不同的新状态)
- 在组件中调用useReducer,并传入reducer函数和状态的初始值
- 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
javascript">import { useReducer } from "react";// 1. 定义一个reducer函数(根据不同的action返回不同的新状态)
function reducer(state, action) {// 根据不同的action type 返回新的stateswitch (action.type) {case "INC":return state + 1;case "DEC":return state - 1;case "SET":return action.payload;default:return state;}
}function App() {// 2. 在组件中调用useReducer,并传入reducer函数和状态的初始值const [state, dispatch] = useReducer(reducer, 0);return (<div>this is App{/* 3. 通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI) */}<button onClick={() => dispatch({ type: "DEC" })}>-</button>{state}<button onClick={() => dispatch({ type: "INC" })}>+</button><button onClick={() => dispatch({ type: "SET", payload: 100 })}>update</button></div>);
}export default App;
useMemo - 不常用
作用:在组件每次重新渲染的时候缓存计算的结果
用途:一般用于消耗非常大的计算
说明:使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算
语法:
javascript">useMemo(() => {// 根据count1返回计算的结果
}, [count1])
解析:当我们点击按钮改变count1和count2时,组件App会重新渲染,故fib(count1)
会重新执行
javascript">import { useMemo, useState } from "react";function fib(n) {console.log("计算函数执行了");if (n < 3) {return 1;}return fib(n - 1) + fib(n - 2);
}function App() {const [count1, setCount1] = useState(0);const result = fib(count1);const [count2, setCount2] = useState(0);console.log("组件重新渲染");return (<div>this is App<button onClick={() => setCount1(count1 + 1)}>change count1: {count1}</button><button onClick={() => setCount2(count2 + 1)}>change count2: {count2}</button>{result}</div>);
}export default App;
解析:当我们点击按钮改变count1和count2时,组件App会重新渲染,但由于使用useMemo
API,且依赖项为count1,所以仅在count1发生变化时执行fib函数
javascript">import { useMemo, useState } from "react";function fib(n) {console.log("计算函数执行了");if (n < 3) {return 1;}return fib(n - 1) + fib(n - 2);
}function App() {const [count1, setCount1] = useState(0);const result = useMemo(() => {return fib(count1);}, [count1]);const [count2, setCount2] = useState(0);console.log("组件重新渲染");return (<div>this is App<button onClick={() => setCount1(count1 + 1)}>change count1: {count1}</button><button onClick={() => setCount2(count2 + 1)}>change count2: {count2}</button>{result}</div>);
}export default App;
React.memo
作用:运行组件在Props没有改变的情况下跳过渲染
React组件默认的渲染机制:只要父组件重新渲染子组件就重新渲染
说明:经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染
语法:
javascript">const MemoComponent = memo(function SomeComponent(props) {// ...
})
解析:点击按钮改变count时,App组件会重新渲染,Son组件也会重新被渲染;但Son组件没有必要重新渲染,因为没有变化
javascript">import { useState } from "react";function Son() {console.log("son子组件重新渲染了");return <div>this is son</div>;
}function App() {const [count, setCount] = useState(0);return (<div>this is App<button onClick={() => setCount(count + 1)}>+</button>{count}<Son /></div>);
}export default App;
解析:点击按钮改变count时,App组件会重新渲染,MemoSon组件不会重新渲染,因为通过memo包裹的组件在props没有发生变化时,是不会重新渲染的
javascript">import { memo, useState } from "react";const MemoSon = memo(function Son() {console.log("son子组件重新渲染了");return <div>this is son</div>;
});function App() {const [count, setCount] = useState(0);return (<div>this is App<button onClick={() => setCount(count + 1)}>+</button>{count}<MemoSon /></div>);
}export default App;
React.memo - props的比较机制
机制:在使用memo缓存组件之后,React会对每一个prop使用Object.is比较新值和老值,返回true,表示没有变化
prop是简单类型:Object.is(3,3) => true 没有变化
prop引用类型(对象/数组):Object([],[])=>false 有变化,React只关心引用是否变化
注意:引用类型中,变量其实是该引用类型值在内存中的地址,所以如果地址没有变化,也就代表prop没有变化
解析:传递一个简单类型prop,点击按钮App组件会重新渲染,子组件MemoSon不会重新渲染,因为num没有发生变化
javascript">import { memo, useState } from "react";const MemoSon = memo(function Son({ count }) {console.log("son子组件重新渲染了");return <div>this is son -- {count}</div>;
});function App() {const [count, setCount] = useState(0);console.log("App重新渲染");const num = 10;return (<div>{count}<button onClick={() => setCount(count + 1)}>+</button><MemoSon count={num} /></div>);
}export default App;
解析:传递一个引用类型prop,点击按钮App组件会重新渲染,子组件MemoSon也会重新渲染,看似list变量没有发生变化,实际上在App组件重新渲染时,会在内存中新建一个[0,1,2]
值的引用地址,即list的值的引用地址发生了改变,所以导致MemoSon组件重新渲染
javascript">import { memo, useState } from "react";const MemoSon = memo(function Son({ list }) {console.log("son子组件重新渲染了");return <div>this is son -- {list}</div>;
});function App() {const [count, setCount] = useState(0);console.log("App重新渲染");const list = [0, 1, 2];return (<div>{count}<button onClick={() => setCount(count + 1)}>+</button><MemoSon list={list} /></div>);
}export default App;
useCallback
作用:使用useCallback包裹函数之后,函数可以保证在App组件重新渲染的时候保持引用稳定
语法:
javascript">const func = useCallback(() => {// ...
}, []);
解析:点击button后App会重新渲染,当App重新渲染时,由于changeHandler是函数(引用类型),引用地址会发生变化,且作为prop传递给了Input组件,故Input组件也会重新渲染
javascript">import { memo, useState } from "react";const Input = memo(({ onChange }) => {console.log("子组件重新渲染了");return <input onChange={(e) => onChange(e.target.value)} />;
});function App() {console.log("App重新渲染");const [count, setCount] = useState(0);// 当App重新渲染时,由于changeHandler函数(引用类型)引用地址会发生变化,// 且作为prop传递给了Input组件,故Input组件也会重新渲染const changeHandler = (value) => {console.log(value);};return (<div><Input onChange={changeHandler} /><button onClick={() => setCount(count + 1)}>{count}</button></div>);
}export default App;
解决:通过useCallback包裹函数,保持引用稳定
javascript">import { memo, useCallback, useState } from "react";const Input = memo(({ onChange }) => {console.log("子组件重新渲染了");return <input onChange={(e) => onChange(e.target.value)} />;
});function App() {console.log("App重新渲染");const [count, setCount] = useState(0);// 通过useCallback包裹函数保持引用稳定,在App重新渲染之后,// changehandler引用地址保持稳定,不会发生变化const changeHandler = useCallback((value) => {console.log(value);}, []);return (<div><Input onChange={changeHandler} /><button onClick={() => setCount(count + 1)}>{count}</button></div>);
}export default App;
React - forwardRef
作用:在父组件中通过ref获取子组件内部的元素
语法:forwardRef
api中的ref是固定写法、位置
const Input = forwardRef((props, ref) => {return <input type="text" ref={ref} />;
});const inputRef = useRef(null);
<Input ref={inputRef} />
inputRef.current.focus()
比如:
import { forwardRef, useRef } from "react";const Input = forwardRef((props, ref) => {return <input type="text" ref={ref} />;
});function App() {const inputRef = useRef(null);const getInput = () => {console.log(inputRef.current.focus());};return (<div><Input ref={inputRef} /><button onClick={getInput}>获取input</button></div>);
}export default App;
React - useImperativeHandle
作用:通过ref调用子组件内部的方法
语法:
const Component = forwardRef((props, ref) => {const func = () => {// ...};useImperativeHandle(ref, () => {return {func,};});return <div></div>
});const sonRef = useRef(null);
<Component ref={sonRef} />
sonRef.current.func();
比如:
import { forwardRef, useImperativeHandle, useRef } 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 focusHandler = () => {sonRef.current.focusHandler();};return (<div><Son ref={sonRef} /><button onClick={focusHandler}>聚焦</button></div>);
}export default App;
Class类组件(不推荐,官方停止更新,推荐使用Hooks写法)
基础结构
- 通过类属性state定义状态数据
- 通过setState方法来修改状态数据
- 通过render来写UI模板(JSX语法一致)
- 在类组件中十分依赖this关键字,如访问state、调用事件等
javascript">class Counter extends Component {// 定义状态变量 类似于 useState()state = {count: 0,};// 事件回调clickHandler = () => {// 修改状态数据this.setState({count: this.state.count + 1,});};// UI模板(JSX)render() {return <button onClick={this.clickHandler}>{this.state.count}</button>;}
}export default Counter
生命周期函数
概念:组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数
- componentDidMount:组件挂载完毕自动执行,一般用于异步数据获取等
- componentWillUnmount:组件卸载时自动执行,一般用于清除副作用等,如清除定时器
javascript">import { Component, useState } from "react";class Son extends Component {// 挂载componentDidMount() {console.log("son componentDidMount");// 设置定时器this.timer = setInterval(() => {console.log("son timer");}, 1000);}// 卸载componentWillUnmount() {console.log("son componentWillUnmount");// 卸载定时器clearInterval(this.timer);}render() {return <div>this is Son</div>;}
}function App() {const [show, setShow] = useState(true);return (<div>{show && <Son />}<button onClick={() => setShow(false)}>unmount</button></div>);
}export default App;
组件通信
概念:类组件和Hooks编写的组件在组件通信的思想完全一致
- 父传子:通过prop绑定数据
- 子传父:通过prop绑定父组件中的函数,子组件调用
- 兄弟通信:状态提升,通过父组件做桥接
javascript">import { Component } from "react";class Son extends Component {render() {return (<div>this is Son - {this.props.name}<button onClick={() => this.props.onGetSonName("wangwu")}>wangwu</button></div>);}
}class Parent extends Component {state = {name: "zhangsan",};getSonName = (sonName) => {this.setState({name: sonName,});};render() {return (<div>this is parent<Son name={this.state.name} onGetSonName={this.getSonName} /></div>);}
}export default Parent;