React 的 Hooks 与 Redux
useState
const [count,setCount] = useState(0);
return(
<div>
<h1>{count}</h1>
<button onClick = { () => setCount((count) => count * 2) }>+</button>
<div>
<button onClick = { () => setCount(count / 2)}>/</button>
</div>
</div>
);
这个示例中,逻辑代码都写在button
语句内部,+ - * / 逻辑都在计算 count 中,它具有多种操作方案,每一种方案都可能有很多地方需要使用。
但随着组件状态管理变得越来越复杂,直接使用useState
可能会让组件的可读性和可维护性降低。
这时,useReducer
就提供了一种更高效的状态管理方式,它允许你将多个状态更新逻辑集中到一个 reducer 函数中处理,使得状态变更逻辑更加清晰和易于维护
useState 的源码实现
通过维护两个数组states
和stateSetters
来分别存储各个状态的值及其对应的更新函数。
状态改变后,会重新重新 app 组件的,所有的代码都会重新执行,会重新生成一个状态,那如何使状态不会变成 0 呢
通过重置stateIndex
并在每次useState
调用时递增它,确保每个状态能够正确关联到其更新函数。这样,即使组件重新执行,每个状态也能保留之前的值,避免了状态被重置为初始值的问题。
同时这个更新一定不是在 app 内维护的,会存在一个对比机制。
看外面的状态有没有变化,如果初次加载找不到之前的状态,就设置为初始值,如果有,则使用。
// 引入ReactDOM的createRoot方法用于创建React的根容器
const { createRoot } = ReactDOM;
// 创建React应用的根容器,挂载到id为"app"的DOM元素上
export const root = createRoot(document.getElementById("app"));
// 定义两个数组分别用来存储状态值和状态设置函数
const states = [];
const stateSetters = [];
// 初始化状态索引,用于追踪当前useState调用的索引
let stateIndex = 0;
// createState函数,根据索引获取初始化状态值
function createState(initialState, stateIndex) {
// 如果states数组中已有对应索引的状态,则直接返回该状态
// 否则,使用初始值初始化该状态
return states[stateIndex] ? states[stateIndex] : initialState;
}
// createStateSetter函数,为特定状态索引创建设置状态的函数
function createStateSetter(stateIndex) {
// 返回一个新的setState函数
// 这个函数接收新状态作为参数,如果是函数,则执行状态更新的函数逻辑;
// 否则,直接赋值新状态
// 更新状态后调用render函数触发重渲染
return (newState) => {
if (typeof newState === 'function') {
states[stateIndex] = newState(states[stateIndex]);
} else {
states[stateIndex] = newState;
}
render();
};
}
// 自定义useState实现
export function useState(initialState) {
// 使用当前索引初始化或获取状态值
states[stateIndex] = createState(initialState, stateIndex);
// 若当前索引没有对应的setState函数,则创建并添加到stateSetters数组
if (!stateSetters[stateIndex]) {
stateSetters.push(createStateSetter(stateIndex));
}
// 获取当前状态值和对应的setState函数
const _state = states[stateIndex];
const _setState = stateSetters[stateIndex];
// 更新状态索引,为下一个useState调用做准备
stateIndex++;
// 返回状态值和setState函数
return [_state, _setState];
}
// 渲染函数,负责重新渲染App组件
async function render() {
// 动态导入App组件模块并获取默认导出的组件
const App = (await import('./App')).default;
// 在每次渲染前重置状态索引,确保每次渲染都是从头开始处理useState
stateIndex = 0;
// 使用最新的状态重新渲染App组件
root.render(<App />);
}
useReducer
useReducer 的逻辑
userReducer 收集所有操作某一个数据的方案。
每个按钮点击事件只需触发一个派发器(dispatch
)操作,并传递一个描述该操作的类型对象(包含type
属性,通过switch
),即可调用不同的逻辑
接下来使用useReducer
来重构上述示例的代码:
首先,定义一个 reducer 函数,它接收当前状态(count
)和一个描述该操作的类型对象(action {type,payload}
),根据不同的类型来决定如何更新状态,并返回新的状态值。
注:payload 是可以一起传进来的参数,通过一个逻辑,不同的type
和参数
实现不同的操作
function countReducer(count, {type,payload}) {
switch (type) {
case 'PLUS'
return count + payload;
case 'MINUS'
return count - payload;
case 'MUL':
return count * payload;
case 'DIV':
return count / payload;
default:
return count;
}
}
然后,在组件中使用useReducer
钩子,派发器(dispatch
)就接受type
和附带的payload
参数,按照type
的类型去countReducer
找对应的逻辑,然后带入payload
参数进行执行
import React, { useReducer } from 'react';
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch({ type: 'ADD' , payload:2 })}>+</button>
<div>
<button onClick={() => dispatch({ type: 'DIV' , payload:3 })}>/</button>
</div>
</div>
);
}
export default Counter;
useEffect
useEffect:副作用,在 react 中,和视图不相干的逻辑和量都是副作用
第二个参数对 useEffect 的影响
如果第二个参数undefined
=> 任何状态改变时,都会重新执行回调
useEffect(() => {
console.log('UseEffect');
}); //这里第二个参数不存在
如果第二个参数不是一个数组 => 报告警
useEffect(() => {
console.log('UseEffect');
}, {}); //这里第二个参数为对象
如果第二个参数是一个数组 且 是一个空数组 => 回调只会在函数组件调用时执行一次
useEffect(() => {
console.log('UseEffect');
}, []); //这里第二个参数为数组,且是一个空数组
如果第二个参数是一个数组 且 是不为空 => 回调只会在这个状态改变的时候执行一次
useEffect(() => {
console.log('UseEffect');
}, [count]); //这里第二个参数为数组,且不为空
清理函数:useEffect
的回调函数中返回一个清除函数,这个返回的函数会在组件卸载以及在下次渲染 effect 之前运行
useEffect(() => {
console.log('UseEffect');
t = setInterval(() => {
setSecond(second => second + 1);
},1000);
//清理函数(如清理订阅、定时器等)
return () => {
clearInterval(t);
t = null;
}
}, [count]);
在 useEffect 中执行异步操作的方法
useEffect(() => {
const fetchData = async () => {
try {
const fetchedData = await http.get('xxx');
setData(fetchedData);
} catch (error) {
// 处理错误,例如设置错误状态或日志记录
console.error('Error fetching data:', error);
}
};
// 直接调用async函数,无需立即执行函数
fetchData();
// 注意:在这个例子中,因为我们没有需要清理的副作用(如取消网络请求),所以不返回清理函数
}, []);
不建议在 useEffect 中使用立即执行函数
执行异步操作
useEffect(() => {
;(async () => {
try {
const data = await http.get('xxx');
setData(data);
} catch (error) {
console.error('Error fetching data:', error);
}
})();
return
}, []);
对于简单的异步操作,立即执行函数虽然可以减少代码量。但在复杂的结构中,可能会降低代码的可读性。
useEffect 的源码实现
它通过比较当前 effect 的依赖数组与上一次的依赖数组来决定是否执行副作用函数。只有当依赖项发生变化时副作用才会重新执行。
// 引入ReactDOM的createRoot方法,用于创建React的根容器
const { createRoot } = ReactDOM;
// 创建React应用的根容器,并将其挂载到页面上的id为'app'的元素
export const root = createRoot(document.getElementById('app'));
// 初始化状态数组,用于存储useState产生的状态值
const states = [];
// 初始化状态设置器数组,与useState配合使用
const stateSetters = [];
// 初始化effect依赖数组,用于存储每个useEffect的依赖项
const effectDepArr = [];
// 状态索引,跟踪当前useState调用
let stateIndex = 0;
// effect索引,跟踪当前useEffect调用
let effectIndex = 0;
// 创建或获取状态值的函数
function createState(initialState, stateIndex) {
// 如果states数组中已存在对应索引的状态,则返回该状态,否则使用初始值初始化
return states[stateIndex] ? states[stateIndex] : initialState;
}
// useEffect函数实现
export function useEffect(cb, depArr) {
// 确保传递的回调函数是函数类型
if (typeof cb !== 'function') {
throw new TypeError('Callback must be a function');
}
// 确保依赖项数组是数组类型(如果提供了的话)
if (depArr !== undefined && !Array.isArray(depArr)) {
throw new TypeError('Dependencies must be an array');
}
// 判断是否需要执行副作用函数
const isChanged = effectDepArr[effectIndex] ?
depArr.some((dep, index) => dep !== effectDepArr[effectIndex][index]) :
true; // 如果没有旧的依赖数组,则默认需要执行
// 如果依赖项改变或者首次执行(没有旧依赖数组),则执行副作用函数
if (depArr === undefined || isChanged) {
cb(); // 执行副作用函数
}
// 更新当前effect的依赖数组
effectDepArr[effectIndex] = depArr;
// 增加effect索引,为下一个useEffect调用准备
effectIndex++;
}
// 渲染函数,负责重新渲染App组件
async function render() {
// 动态导入App组件模块并获取默认导出的组件
const App = (await import('./App')).default;
// 在每次渲染前重置状态和effect的索引,确保每次渲染都能从头开始处理useState和useEffect
stateIndex = 0;
effectIndex = 0;
// 使用最新的状态和副作用配置重新渲染App组件
root.render(<App />);
}
Memo
问题阐述:下面的示例中有一个子组件,当一个组件的状态发生了改变的时候,相关的视图是必然要更新的。函数组件在视图更新的需求来临的时候,函数是必然要执行的
也就是说,当状态更新的时候,会返回整个组件,变动count1
状态,虽然并没有传给子组件,也没有子组件什么事,但是子组件还是参与了重新渲染
function App(){
const [ count1, setCount1 ]= useState(0);
const [ count2, setCount2 ]= useState(0);
return(
<div>
<h1>count1:{ count1 }</h1>
<button onclick={ () => setCount1(count1 + 1) }>+</button>
//使用 count2 的子组件
<Child count2={ count2 } />
<button onClick={ () => setCount2(count2 + 1) }>+</button>
</div>
);
root.render(<App />);
export default App;
//子组件
const Child = (props) => {
return (
<div>
<h1>count2:{ props.count2 }</h1>
</div>
);
}
这个时候,我们可以使用 memo,使子组件不参与重新渲染
//使用了memo的子组件
const Child = memo((props) => {
console.log('Child function is recalled.')
return (
<div>
<h1>count2:{ props.count2 }</h1>
</div>
);
}
memo 会检查这个内容是不是被改变了,如果没有改变就不需要被更新了
但是 memo 也存在问题,如果我们换一种写法,memo 就没办法解决了
function App(){
const [ count1, setCount1 ]= useState(0);
const [ count2, setCount2 ]= useState(0);
const childData = {
count2
}
return(
<div>
<h1>count1:{ count1 }</h1>
<button onclick={ () => setCount1(count1 + 1) }>+</button>
//使用 新的childData 的子组件
<Child childData={ childData } />
<button onClick={ () => setCount2(count2 + 1) }>+</button>
</div>
);
root.render(<App />);
export default App;
//使用了memo的子组件
const Child = memo((props) => {
console.log('Child function is recalled.')
return (
<div>
//这里的写法变了
<h1>count2:{ props.childData.count2 }</h1>
</div>
);
}
这个时候 memo 就失效了,为什么呢?
memo 核心会对引用进行比较
,但是这种比较是浅比较
childData 引用,如果更新了一个新的引用,那么 Child 就会被执行。如果引用没有变化,Child 就不会执行
如果count1
更新,APP 组件必然要重新执行,所以 childData 就必然要跟着重写赋值一个新的引用
那么这个时候,就出现了useMemo
useMemo
const childData = useMemo( () => { count2 },[count2] )
只有count2
改变的时候,返回一个新的引用,才会执行这个函数
useMemo 的两种作用:
-
让子组件可以更好的缓存当前的状态,防止做没必要的重新执行
-
帮助集成计算的特性
const doubleCount1 = useMemo( () => count2 *2, [count2]);
注意:如果函数要返回的是一个值的话,就不需要用了
ContextAPI
contextapi 消费方消费提供方的数据,可以实现跨组件传递数据
缺点:数据来源不明确,没有很好的解决兄弟组件的传递
只能通过事件的方式传给父组件,父组件在传递给兄弟组件
所以我们需要用到 Redux 的设计模式
Redux
1、使用 redux 思想完成案例
1.首先定义 store/actionType.js
export const PLUS ='PLUS';
export const MINUS ='MINUS';
2.定义 store/reducer.js
import { PLUS } from './actionType';
import initialState from './state';
export default function reducer ( state = initialState, action = {} )
switch (action.type){
case PLUS:
return {
count: state.count + 1
}
case MINUS:
return {
count: state.count - 1
}
default:
return state;
}
}
2、使用 React - Redux
将 redux 挂载到 react
1.必须用 react 的 state 绑定视图,也就是说用 redux 的 state 赋值到 react 的 state
2.要订阅 redux state 的更新来通知 react 更新自己的 state,从而更新视图
1、安装 React-Redux
它是连接 Redux 与 React 应用的桥梁
2、创建 Redux Store
1.创建 src/store/index.js
//创建 Redux Store 并引入 添加中间件
import{ applyMiddleware, createStore } from 'redux';
//引入 reducer
import reducer from './reducer';
//// 创建并导出 Redux Store
export default createStore(reducer);
2.创建多个状态:
src/store/state/counter.js
export default {
count: 1
}
src/store/product.js
export default {
list:[]
detail:{}
}
3.创建多个 Reducer:
src/store/reducer/couter.js
import counterState from'../state/counter';
export default function counter ( state={ ...counterState },action = {} )
const { type, payload }= action;
switch (type){
case 'PLUS':
return {
...state,
count: state.count + payload
}
case 'MINUS':
return {
...state,
count: state.count - payload
}
default:
return state;
}
}
src/store/reducer/product.js
import productState from'../state/product';
//它接受两个参数:当前的state(默认值是通过解构并浅拷贝productState得到的初始状态)和action(一个包含type和payload属性的对象,默认为空对象)
export default function product ( state={ ...productState },action = {} )
const { type, payload }= action;
//开始一个switch语句,根据action.type的值来决定执行哪一段代码块
switch (type){
//当type等于'LIST'时,返回一个新的状态对象,该对象是当前state的浅拷贝,并且其count属性被更新为payload的值。
case 'LIST':
return {
...state,
count: payload
}
case 'DETAIL':
return {
...state,
count: payload
}
default:
return state;
}
}
因为 reducer 会有很多,且 reducer 是可以合并的
创建 src/store/reducer/index.js
import { combineReducers } from "redux';
import counter from './counter';
import counter from './product';
export default combineReducers({
counter
product
});
4.创建 action
export function counterAction ( action ={} ){
const { type, payload }= action;
return {
type,
payload
}
}
虽然多此一举,但内在思想是:dispatch 调用 action,返回 action 需要的 type 和 payload,进入 reducer 的某个逻辑分支
在 action 中进行异步请求数据
function xxxAction(){
// 异步请求
axios(); =>异步 => data
return {
type: xxx,
payload: data
}
}
这个写法是不行的,因为 axios 请求是异步的,在请求的时候,函数就已经 return 出去了
async function xxxAction(){ // return promise
// 异步请求
await axios(); =>异步 => data
return {
type: xxx,
payload: data
}
}
async/await 也不行,因为 async 返回的是一个 promise,会将返回的 type 和 payload 包装成一个 promise
在下面这个路线图中,我们可以看到
在 action 这一步是可以进行异步请求的,然后将请求到的数据,当作 payload
这件事本身 redux 做不到,需要用到中间件去支撑
使用一个叫redux-promise
的中间件,写法简便
首先在 src/store/index.js 中引入这个中间件
// 创建 Redux Store 并引入 添加中间件
import{ applyMiddleware, createStore } from 'redux';
// 引入 reducer
import reducer from './reducer';
// 导入 redux-promise 中间件
import reduxPromise from 'redux-promise'
// 创建并导出 Redux Store
// 使用中间件
export default createStore(reducer,applyMiddleware(
redux-promise
));
在 action 中
export function detailAction (id){
return http('/detail' + id).then(res => {
return { type:'DETAIL', payload: res.data }
})
}
在 main.jsx 中使用 Provider
import App from './App';
import { createRoot } from "react-dom/client';
import { Provider } from 'react-redux';
import store from '@/store';
const root = createRoot(document.getElementById("root"));
root.render(
<Provider store={ store }>
<App />
</provider>
);
provide 是提供,connect 是连接 store
将状态注入 props,props 一变化,页面就会更新
这些就是注入 props 的内容
在 Home.jsx 中就可以使用 store
import React, { useEffect } from 'react';
import { useParams, connect } from 'react-router-dom';
import { detailAction } from './actions';
function Product( datail, datailAction ){
const {id} = useParams();
//检查productDetail。如果productDetail为空对象(即没有详情数据),则调用detailAction去获取产品详情
useEffect(() => {
if (!productDetail || Object.keys(productDetail).length === 0){
detailAction(id);
}
},[productDetail, detailAction, id]); // 添加依赖项以确保当这些值变化时重新运行effect
return(
<div>Home</div>
<div>
<img src = { productDetail.image }
</div>
);
//定义了一个Redux状态到props的映射函数mapStateToProps,从Redux的全局状态中提取product.detail作为productDetail prop传递给Product组件
const mapStateToProps = (state) => {
return {
productDetail: state.product.detail
}
}
//将detailAction函数绑定到Product组件的props上,使得组件可以调用该action来更新状态
const mapDispatchToProps = {
detailAction
}
export default connect(mapStateToProps, mapDispatchToProps)(Product);