一、 Redux
1. Flux框架
Flux 不是一个具体的框架,而是一套由 FackBook 团队提出的应用架构。这套架构约束的是应用处理数据的模式。
Flux 将每个应用都划分为四部分:View、Store、Action、Dispatcher
view 视图层:表示用户界面,可以是任何形式的产物Action 动作:视图层发出的消息,会触发应用状态改变Dispatcher 派发器:负责对 action 进行分发Store 数据层:存储应用状态的仓库,同时具备修改状态的逻辑
Flux 最核心的原理是严格的单向数据流,Redux 是 Flux 思想的产物,虽然没有完全实现 Flux,但是却保留了单向数据流的特点。
2. Redux
1. 功能
-
组件获取状态:任何组件都可以以约定的方式从 Store 读取全局状态
-
组件修改状态:任何组件都可以通过合理的派发 Action 来修改全局状态
2. 核心元素
Store:存储应用程序的全局状态。在 Redux 中,整个应用程序的状态存储在一个单一的对象树中,并且这个状态树只存在于唯一的 Store 中。Actions:当应用程序的状态需要更新时,会发送一个 Action。Action 是一个描述“发生了什么”的普通对象。Reducers:它们是处理状态更新的纯函数。Reducer 接收 state 和一个 action 作为参数,并返回一个新的state。
3. reducer 管理
reducer 本质上是一个函数,负责响应 action 并修改数据,根据 previousState 参数和 action 行为计算出新的 newState。
reducer(previousState,action)=>{
if(previousState){
... ...
return newState
}else{
... ...
}
}
- 拆分:根据独立的模块拆分出单独的 reducer
- 合并:创建 Store 时合并 reducer
- 统一管理 actionType
4. Action Type管理
整个 store 中的 action type 值不能重复,需要达到全局唯一性
- 命名空间(Namespacing)
为每个模块或功能区分配一个独立的命名空间,以确保它们的 action type 常量不会发生冲突。例如将模块名作为前缀,USER_FETCH_REQUESTED
- 统一文件管理
将所有模块的 action type 常量定义放在一个统一的文件中,以避免不同文件之间的命名冲突
- 使用工具库
使用工具库来自动化处理 action type 常量的生成。一些常用的工具库有 redux-actions 和 redux-toolkit,它们简化 Redux 开发的功能,可以自动生成唯一且不会重复的 action type 常量
- 唯一性检查
可以编写自定义的工具函数或脚本,在构建或开发过程中对 action type 常量进行唯一性检查
5. 三大原则
-
单一数据源:在 Redux 中,整个应用的状态被存储在一个对象树中,并且这个对象树只存在于唯一的一个存储中。这样的设计不仅使得状态的管理变得更加可预测,而且也便于开发者进行状态追踪和调试。
- 保证数据一致性
- 简化数据管理
- 便于调试
-
状态是只读的:唯一改变状态的方式是触发一个动作(action),动作是一个用于描述已发生事件的普通对象。这种方式确保了视图或网络回调不能直接修改状态,而是必须通过分发动作的方式,保证了数据流的清晰和一致性。
-
使用纯函数来执行修改:为了描述动作如何改变状态树,你需要编写 reducers。Reducer 是一种特殊的函数,根据旧的状态和一个动作,返回一个新的状态。关键在于,reducers 必须是纯函数,这意味着它们应该只计算下一个状态,而不改变原始状态。
6. 项目中使用
import { Provider } from 'react-redux'
import store from './store'
const App = (
<Provider store={store}>
<Home />
</Provider>
利用 Provider 从最外层封装整个应用,向 connect 传递 store,使 Provider 内部的所有组件都可以使用 store。
connect 连接器,连接 React 和 Redux。本质上是一个高阶组件,接收一个组件作为参数。
function connect(mapStateToProps, mapDispatchToProps, mergeProps, options={})(App)
参数说明:
-
mapStateToProps:从 Redux 中获取部分状态作为 props 传递给当前组件 -
mapDispatchToProps:将 actionCreator 与 dispatch 绑定在一起,并将其作为 props 传递给当前组件 -
mergeProps:对接收到的所有 props 进行分类,命名和重组 -
options:配置项,一般包含两方面- pure:true 表示 connect 在 shouldComponentUpdate 中使用浅比较
- withRef:true 表示 connect 保存对装饰组件的 refs 引用,可以通过 getWrappedInstance 获取最终的 DOM 节点
import { connect } from 'react-redux';
function Myapp(props) {
const {num,add} = props
return (
<div>
<div>我的值:{num}</div>
<button onClick={()=>{add(1)}}>+1</button>
</div>
)
}
// 接收props
const mapStateToProps= (state) => {
return {
num: state.getNum.num,
}
}
// 接收props中的action
const mapDispatchToProps= (dispath) =>{
return {
add:(id)=>dispath({type:'ADD'})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Myapp)
页面通过点击按钮,改变 store 中的一个 state,其完整的数据流动如下所示:
3. 工作原理
1. createStore
createStore 方法是在使用 Redux 时最先调用的方法,是整个流程的入口。同时也是 Redux 中最核心的 API。
通过 createStore 方法创建 store 对象
const store = createStore(reducers);
createStore 本身包含四个方法:
getState:获取当前 store 中的状态dispatch(action):分发一个 action,并返回这个 action,这是唯一能改变 store 中数据的方式subscribe(listener):注册一个监听者,在 store 发生变化时调用replaceReducer(nextReducer):更新当前 store 里的 reducer,一般只会在开发模式中调用此方法
export function createStore(reducers, initialState, enhancer){}
createStore 利用前两个参数进行 createStore 的调用。
createStore 利用 enhancer 对 createStore 能力做增强,并返回增强后的 createStore(利用高阶函数的原理)
export function createStore(reducers,initialState,enhancer){
// 认为没有传默认state值
if(typeof initialState === 'function' && typeof enhancer === 'undefined'){
enhancer = initialState;
initialState = undefined;
}
if(typeof enhancer !== 'undefined'){
if(typeof enhancer !== 'function'){
throw new Error('错误')
}
// 高阶函数原理
return enhancer(createStore)(reducers,initialState)
}
}
接收 createStore 作为参数传入 ,对 createStore 的能力做增强,并返回 createStore,然后再将 reducers,initialState 作为参数传递给增强后的 createStore,最终得到 store。
2. dispatch
dispatch action,主要工作即“将 Redux 核心三要素串联起来”。
通过上锁,避免套娃式的 dispatch
try{
isDispatching = true;
currentState = currentReducer(currentState,action);
}finally{
isDispatching = false;
}
Redux 完整流程如下:
3. subscribe
在 store 对象创建成功后,通过调用 store.subscribe 注册监听函数。
当 dispatch action 发生时,Redux 会在 reducer 执行完毕后,将 listeners 数组中的监听函数逐个执行。
- nextListener:订阅、触发、解除订阅操作的均是 nextListener
- currentListener:记录当前正在工作的 listeners 数组的引用,将它与可能发生改变的 nextListeners 区分开来,以确保监听函数在执行过程中的稳定性
4. 中间件
const store = createStore(
reducer,
initial_state,
applyMiddleWare(middleWare1,middleWare2,...)
)
applyMiddleWare 的作用就是向 store 中注入中间件(enhancer 包装 createStore)。
中间件是指可以增强 createStore 的工具,在 Redux 中所有的更新都是同步执行的,如果想要异步处理更新流程,则需要借助中间件。
中间件的工作流程图:
中间件的执行时机:action 分发之后、reducer 执行之前。
中间件的执行前提:利用 applyMiddleWare 对 dispatch 函数进行改写,使其在触发 reducer 之前,会先执行对 redux 中间件的链式调用。
1. Redux Thunk
Redux Thunk 是一个异步 Action 的中间件,Redux Thunk 允许在 Redux action 中返回函数而不仅仅是纯对象,从而可以在 action 中进行异步操作,并在操作完成后分发一个新的 action。
第一步:下载插件npm install redux-thunk。
第二步:使用插件
const store = createStore(
reducer,
initial_state,
applyMiddleWare(redux-thunk)
)
自定义一个异步 action,如下所示:
// 定义异步 action
const fetchData = () => {
return (dispatch) => {
dispatch({ type: 'FETCH_DATA_START' });
// 发起异步操作
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => {
// 异步操作成功后,分发新的 action
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data });
})
.catch((error) => {
// 异步操作失败后,分发新的 action
dispatch({ type: 'FETCH_DATA_ERROR', payload: error });
});
};
};
// 在组件中调用异步 action
dispatch(fetchData());
fetchData 是一个异步 action,它返回一个函数,函数中包含异步操作,当异步操作完成后,新的 action(成功或失败)将被分发到 Redux store。
适用场景:Redux Thunk 适用于简单的异步操作,比如发送 Ajax 请求或者执行定时器等
4.2 Redux Saga
Redux Saga 是另一个常用的 Redux 中间件,它使用生成器函数(Generators)的方式来处理异步操作,提供了一种声明式的方法来管理和处理副作用。
function* fetchData() {
try {
yield put({ type: 'FETCH_DATA_START' });
const response = yield call(fetch, 'https://api.example.com/data');
const data = yield response.json();
yield put({ type: 'FETCH_DATA_SUCCESS', payload: data });
} catch (error) {
yield put({ type: 'FETCH_DATA_ERROR', payload: error });
}
}
适用场景:Redux Saga 适用于复杂的异步操作场景,如多个连续的异步操作、长轮询、并发请求等
二、 React-router
1. 路由模式
React Router 与 React 的很多特性保持一致,在 React 中,组件就是一个方法,props 作为参数传入方法,当 props 更新时会触发方法的执行,从而重新绘制 View。在React Router 中,同样可以把 Router 看作是一个方法,location 作为参数传入方法,返回的结果同样是一个 View。
当用户在任何路由下刷新页面,浏览器都可以根据当前URL进行资源定位,不会出现白屏问题。
- hash 模式:改变 URL 中 # 后面的部分,实现组件的切换
// 感知hash变化
window.addEventListener('hashChange',functionn(event){
...
},false)
- history 模式:改变整个 URL,实现组件切换
// 追加记录
history.pushState(data[,title][,url]);
// 修改记录
history.replaceState(data[,title][,url])
// 感知state变化
window.addEventListener('popState',functionn(event){
...
},false)
1. 声明式路由
ReactRouter 继承了声明式编程特点,允许使用 JSX 标签书写路由
// 当前页面url为/login时,React会渲染Login这个组件
import { Router,Route,browserHistory} from 'react-router';
const routes = (
<Router history={browserHistory}>
<Route path="/login" component={<Login />} />
</Router>
)
2. 嵌套路由及路径匹配
在许多单页应用中,嵌套路由是最常见的路由模式。
例如页面有的顶栏、侧边栏、列表,点击具体列表卡片跳转时顶栏和侧边栏需要复用
import { Router,Route,IndexRoute,browserHistory} from 'react-router';
const routes = (
<Router history={browserHistory}>
<Route path="/" component={<App />}>
<IndexRoute component={< List/>} />
<Route path="/list/:listID" component={<Case />} />
</Route>
</Router>
)
App 组件具有顶栏和侧边栏的功能,React Router 自动根据当前 url 决定匹配列表页还是详情页
url = /:则匹配 List 组件url = /list/1:则匹配 Case 组件
3. 支持多种路由切换模式
- hashHistory:利用 hashChange 改变 # 后面的 url,浏览器兼容性较好,但是 url 中会增加 #
import { Router,Route,hashHistory} from 'react-router';
- browserHistory:利用 history.pushState 更新整个 url,需要服务端配置,解决任意路径刷新的问题
import { Router,Route,browserHistory} from 'react-router';
4. 路由跳转
- BrowserRouter:路由器,根据映射关系匹配新的组件。分为 BrowserRouter 和 HashRouter
- Route:路由,定义组件与路径的映射关系。包括 Route、Switch 等
- Link:导航,改变路径。如 Link、NavLink、Redirect
2. react-router-dom
在浏览器宿主下进一步封装 react-router,集成了 history 与 react-router,初始化了<BrowserRouter>,<HashRouter>,<Link>等可以直接使用的组件
3. react-router-redux
绑定 Router 与 Redux store
Redux 作为单一的状态管理工具,管理全局状态,其中路由也是全局状态的一种,所以也应该由 Redux 管理,通过 Redux 的方式改变路由。
React Router Redux 提供了 syncHistoryWithStore 实现 Redux store 与 Router 的绑定,它接收两个参数:history 和 store,返回一个增强的 history 对象。
将增强的 history 对象作为 props 传给 React Router 中的<Router>组件,从而实现观察路由变化改变 store 的能力。
import { browserHistory} from 'react-router';
import { syncHistoryWithStore} from 'react-router-redux';
import { reducers} from 'react-redux';
const store = createStore(reducers);
const history = syncHistoryWithStore(browserHistory,store);
用 Redux 切换路由
首先对 store 进行增强
import { browserHistory} from 'react-router';
import {routerMiddleware} from 'react-router-redux';
const middleware = routerMiddleware(browserHistory);
const store = createStore(
reducers,
applyMiddleware(middleware)
);
然后通过 action 切换路由
import {push} from 'react-router-redux';
store.dispatch(push('/home'));
三、 React 知识点延伸
1. 不能在循环或条件语句中使用 Hook
函数组件本身是没有状态的,所以需要引入 Hook 为其增加内部状态。
React 中每一个 Hook 方法都对应一个hook对象,用于存储和相关信息。在当前运行组件中有一个_hooks链表用于保存所有的 hook 对象。
export type Hook = {
memoizedState: any, // 上次渲染时所用的 state
baseState: any, // 已处理的 update 计算出的 state
baseQueue: Update<any, any> | null, // 未处理的 update 队列(一般是上一轮渲染未完成的 update)
queue: UpdateQueue<any, any> | null, // 当前出发的 update 队列
next: Hook | null, // 指向下一个 hook,形成链表结构
};
以 useState 为例,初次挂载的时候执行 mountState,更新的时候执行 updateState。
mountState:构建链表updateState:按照顺序遍历链表,获取 数据进行页面渲染
hook 的渲染其实是根据“依次遍历”来定位每个 hook 内容,如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。
假设在if循环中使用 useState。
// eslint-disabled-next-line
if (Math.random() > 0.5) {
useState('first');
}
useState(100);
如上所示例子中,假设第一次调用时 Math.random()>0.5,则底层的 _hooks 数组结构如下:
_hooks: [
{
memoizedState: 'first',
baseState: 'first',
baseQueue: null,
queue: null,
next: {
memoizedState: 100,
baseState: 100,
baseQueue: null,
queue: null,
},
}
]
假设第二次调用时 Math.random()<0.5,则会将链表中的 'first' 赋值给100,从而造成显示错误。
React Hooks 是为了简化组件逻辑和提高代码可读性而设计的。将 Hook 放在 if/循环/嵌套函数中会破坏它们的封装性和可预测性,使得代码更难维护和理解。同时,这样做也增加了代码的复杂度,可能会导致性能下降和潜在的错误。
因此,在编写 React 函数组件时,一定要遵循 Hook 规则,只在顶层使用 Hooks,并且不要在循环、条件或嵌套函数中调用。
-
只能在函数最外层调用 Hook 。不要在循环、条件语句或子函数中调用useState、useEffect等。
-
只能在React函数组件或者自定义 Hook 调用 Hook ,不能在其他JavaScript函数中调用。
2. 定时器hook实现
入门案例:利用 react hook 实现一个计时器
1)简单实现
import React, { useEffect, useState } from "react";
const List = () => {
const [count,setCount] = useState(0);
useEffect(()=>{
const timeRef = setInterval(()=>{
setCount((count) => count + 1);
},1000)
return ()=>{
clearInterval(timeRef);
}
},[]);
return (
<div>{count}</div>
);
};
export default List;
2)封装 hook(支持自定义初始值和时间间隔)
// 使用
const {count} = useInterval(0,1000);
// 封装hook
const useInterval = (initValue: number,delay:number) => {
const [count, setCount] = useState(initValue);
useEffect(() => {
const timer = setInterval(()=>{
setCount(count => count + 1)
}, delay);
return () => {
clearInterval(timer);
};
}, []);
return {count}
};
3)封装 hook(改变当前组件内状态)
// 使用setCount(count => count + 1)
useInterval(()=> setCount(count => count + 1),1000);
// 封装hook
const useInterval = (callback: ()=> void,delay:number) => {
useEffect(()=>{
let timer = setInterval(callback,delay);
return ()=>{
clearInterval(timer);
}
},[]);
};
// 使用setCount(count + 1)
useInterval(()=> setCount(count + 1),1000);
// 封装 hook
const useInterval = (callback: () => void, delay: number) => {
useEffect(() => {
const time = setInterval(callback, delay);
return () => {
clearInterval(time);
};
});
};
React Hook使用时必须显示指明依赖,不能在条件语句中声明Hook
3. 封装 Button 组件
Button 调用方式如下所示:
<Button
classNames='btn1 btn2'
onClick={()=>alret(1)}
size='middle'
>按钮文案</Button>
封装一个 Button 组件如下:
const Button: React.FC = memo((props:any) => {
const { classNames, onClick, size } = props;
const sizeList = ['small','large','middle'];
const getClassName = ()=>{
if(size && sizeList.includes(size)){
return `btn-${size} ${classNames}`;
}
return classNames;
}
const handleClick = ()=>{
if(onClick){
onClick();
}
}
return (
<div
className={getClassName()}
onClick={handleClick}
>
{props.children}
</div>
);
});
.btn-small{
width: 32px;
height:24px;
}
.btn-middle{
width: 50px;
height:32px;
}
.btn-large{
width: 88px;
height:42px;
}
4. 封装 usePrevious Hook
const usePreValue = (value:any)=>{
const preValue = useRef(null);
useEffect(()=>{
preValue.current = value;
console.log(preValue.current, 'preValue.current');
});
return preValue;
}
组件中使用:
const Case: React.FC = () => {
const [count, setCount] = useState(0);
const increment = () => setCount((c) => c + 1);
const preCount = usePreValue(count);
console.log(count, 'count', preCount, 'preCount');
return (
<div className="container">
<div>Count: {count}</div>
<div onClick={increment} >+1</div>
</div>
);
};
5. 封装 useDebounce Hook
点击按钮3秒后打印,只执行1次,不会重复执行。
const dbouncedFunction = useDbounce((str:string) => {
console.log('数据保存到数据库:', str);
}, 3000);
return (
<div onClick={() => dbouncedFunction('hello!')}>save</div>
);