关于我 壹文 项目 三五好友
React的Hooks和Redux的使用 2024-05-09
文章摘要

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 的源码实现

通过维护两个数组statesstateSetters来分别存储各个状态的值及其对应的更新函数。

状态改变后,会重新重新 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 的两种作用:

  1. 让子组件可以更好的缓存当前的状态,防止做没必要的重新执行

  2. 帮助集成计算的特性

    const doubleCount1 = useMemo( () => count2 *2, [count2]);
    

注意:如果函数要返回的是一个值的话,就不需要用了

ContextAPI

contextapi 消费方消费提供方的数据,可以实现跨组件传递数据

缺点:数据来源不明确,没有很好的解决兄弟组件的传递

只能通过事件的方式传给父组件,父组件在传递给兄弟组件

所以我们需要用到 Redux 的设计模式

Redux

无标题-2024-04-26-1549

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

在下面这个路线图中,我们可以看到

无标题-2024-04-26-1549-2

在 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 一变化,页面就会更新

120111715167445_.pic

这些就是注入 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);

Not-By-AI