我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
redux 是什么
- redux 是一个用来帮助我们管理应用状态的工具库
- 它以集中式 store 的方式对整个应用的状态进行集中管理
- 它制定了一套数据更新的规则,使得应用中的状态只能以可预测的方式进行更新
redux 的数据流向
- redux 初始化一个 store 来集中管理视图页面的状态
- store 中的 reducer 定义了更新状态的规则
- 页面渲染时,可以直接在 store 中获取状态
- 当用户在页面视图中进行一些操作,想要更新数据时,就需要 dispatch 一个 action 来更新数据,action 是一个纯对象,其中包含 type 与 payload
- reducer 函数会接收 action 和更新之前的 state,并根据 action 来更新 state,最后返回更新之后的 state
- 最后 redux 在 state 更新之后,触发页面更新渲染
redux 的使用
- 下面是 store 的代码
// redux
// import { applyMiddleware, combineReducers, createStore } from "redux";
// import logger from "redux-logger";
// import thunk from "redux-thunk";
// import promise from "redux-promise";
import {
createStore,
applyMiddleware,
combineReducers,
logger,
thunk,
promise,
promiseFull,
} from "../my-redux";
const countReducer = (state = 100, action) => {
const { type, payload = 1 } = action;
switch (type) {
case "ADD":
return state + payload;
default:
return state;
}
};
const countReducer2 = (state = { num: 1 }, action) => {
const { type, payload = 1 } = action;
switch (type) {
case "ADD2":
return { ...state, num: state.num + payload };
default:
return state;
}
};
export const store = createStore(
countReducer,
// 合并多个 reducer
// combineReducers({
// count: countReducer,
// count2: countReducer2,
// }),
applyMiddleware(promise, thunk, logger)
);
复制代码
- 下面是 redux 的使用 demo 的代码
import React, { Component } from "react";
import { store } from "./store";
export class MyReduxPage extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
this.unsubscribe();
}
handleAdd = () => store.dispatch({ type: "ADD", payload: 100 });
handleAsyncAdd = () => {
// 用 setTimeout 模拟 ajax 请求
//
// 1、直接在组件中写 ajax 请求
// 非常不优雅
// setTimeout(() => {
// store.dispatch({ type: "ADD", payload: 100 });
// }, 1000);
// 2、将数据请求函数抽出去
// 让 store.dispatch 直接调用,这样就可以将数据请求与视图渲染分离开
store.dispatch((dispatch) => {
setTimeout(() => {
dispatch({ type: "ADD", payload: 100 });
}, 1000);
});
};
handlePromiseAdd = () => {
store.dispatch({
type: "ADD",
payload: Promise.resolve(100),
});
};
setNum = () => {
store.dispatch({ type: "ADD2", payload: 10 });
};
render() {
return (
<div>
<h2>my-redux-demo</h2>
<button onClick={this.handleAdd}>ADD:{store.getState()}</button>
<br />
<button onClick={this.handleAsyncAdd}>
async-ADD:{store.getState()}
</button>
<br />
<button onClick={this.handlePromiseAdd}>
promise-ADD:{store.getState()}
</button>
<br />
{/* 下面是 combineReducers 的使用 */}
{/* <button onClick={this.handleAdd}>ADD:{store.getState().count}</button>
<button onClick={this.setNum}>
ADD2:{store.getState().count2.num}
</button> */}
</div>
);
}
}
复制代码
实现redux
实现 createStore
export function createStore(reducer) {
let currentState; // 用于存放 store 的数据
let listeners = []; // 用于存放监听的回掉
const getState = () => currentState;
const dispatch = (action) => {
// 执行 reducer
currentState = reducer(currentState, action);
// 执行监听的回掉
listeners.forEach((listener) => listener());
};
// 注册监听回掉
const subscribe = (newListener) => {
listeners.push(newListener);
// 返回一个移除监听回掉的方法
return () => {
listeners = listeners.filter((listener) => listener !== newListener);
};
};
// 解决 currentState 没有初始值的问题
dispatch({ type: `${Date.now()}` });
return {
getState,
dispatch,
subscribe,
};
}
复制代码
-
createStore 是一个函数
- 它接收一个,定义了 store 中数据更新规则的 reducer 函数
- 然后返回一个对象,其中包括
getState
、dispatch
、subscribe
等与 store 进行交互的方法
-
getState 是用来获取 store 数据的方法
-
dispatch 是用来提交数据更新到 store 的方法,其接受一个 action 纯对象
- action 对象包含了数据更新的类型 type,以及数据更新的参数 payload
- 执行 reducer 函数,并将当前的 state 与 action 作为参数传入,即可完成 store 数据的更新
-
数据更新之后如何让视图进行更新?
- 视图如何更新只有视图知道
- 所以可以将视图的更新函数作为回掉函数注册到 store 中
- 当数据更新时,调用视图更新的回掉函数即可完成视图的更新
- redux 的解决方案就是
- 在 store 中定义一个存放监听函数的数组 listeners
- 提供一个 subscribe 函数用于注册视图更新的回掉函数
- 该 subscribe 函数接收一个监听回掉
- 返回一个注销该监听回掉的函数
- 只要页面通过 dispatch 提交数据更新,就可以遍历 listeners,并执行每一个监听函数
- 视图如何更新只有视图知道
const dispatch = (action) => {
// 执行 reducer
currentState = reducer(currentState, action);
// 执行监听的回掉
listeners.forEach((listener) => listener());
};
// 注册监听回掉
const subscribe = (newListener) => {
listeners.push(newListener);
// 返回一个移除监听回掉的方法
return () => {
listeners = listeners.filter((listener) => listener !== newListener);
};
};
复制代码
- 上面 redux demo 的代码中
- 在组件挂载时,完成试图更新回掉的注册,然后将注销视图更新的函数保存在组件实例上的 unsubscribe 属性中
- 在组件卸载前,执行 unsubscribe 以注销视图更新回掉
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
this.unsubscribe();
}
复制代码
- 组件中通过
store.getState
方法即可获取 store 中存储的数据 - 每次 button 被点击时就会调用 handleAdd 方法,该方法中调用
store.dispatch
提交数据更新
...
handleAdd = () => store.dispatch({ type: "ADD", payload: 100 });
<button onClick={this.handleAdd}>ADD:{store.getState()}</button>
...
复制代码
实现 applyMiddleware
- 上面的例子实现了 redux 最基本的使用,dispatch 提交数据更新时,提交一个为普通对象的 action,但这只能满足一般的同步数据更新
- 如何提交数据的异步更新?
- 如下面代码所示
- 方案一:直接将数据请求写在组件内的某个方法中,在请求拿到结果之后调用
store.dispatch
提交数据更新,这样可以实现需求,但是数据请求与视图混在一起写,极其不优雅 - 方案二:将数据请求封装成一个函数,将该函数作为参数传给 store.dispatch 进行调用,这样就将数据请求与视图渲染分离开了
- 方案一:直接将数据请求写在组件内的某个方法中,在请求拿到结果之后调用
- 如下面代码所示
handleAsyncAdd = () => {
// 用 setTimeout 模拟 ajax 请求
//
// 1、直接在组件中写 ajax 请求
// 非常不优雅
// setTimeout(() => {
// store.dispatch({ type: "ADD", payload: 100 });
// }, 1000);
// 2、将数据请求函数抽出去
// 让 store.dispatch 直接调用,这样就可以将数据请求与视图渲染分离开
const request = (dispatch) => {
setTimeout(() => {
dispatch({ type: "ADD", payload: 100 });
}, 1000);
}
store.dispatch(request);
};
复制代码
- 但是上面也说到了,普通的 dispatch 只支持接收普通对象作为参数,那么如何让 store.dispatch 支持函数作为参数呢?
- redux 的解决方案是:
- 应用中间件的概念,给 dispatch 的执行过程加上中间件
- 添加特定作用的中间件之后,就相当于增强 dispatch 的特定能力
- redux 的解决方案是:
- 下面是 redux-thunk 中间件的代码
// 实现 redux-thunk 中间件
//
// 接收 storeApi,返回一个函数
export const thunk = ({ dispatch }) => {
return function thunkDispatchWarp(next) {
console.log("thunkDispatchWarp exec, next: ", next.name);
return function thunkDispatch(action) {
console.log("thunkDispatch exec", action);
if (typeof action === "function") {
return action(dispatch);
}
next(action);
};
};
};
复制代码
- 如何添加中间件呢?
- 下面则是仿照 redux 实现了用于应用中间件的 applyMiddleware 函数
export const applyMiddleware = (...middlewares) => {
return (createStore) => (reducer) => {
const store = createStore(reducer);
// 未增强的 dispatch
let dispatch = store.dispatch;
// 增强 dispatch 后
// 调用一次 dispatch, 所有中间件函数会被依次调用,最后执行 store.dispatch
// 每个中间件函数都是可以进行读写 store 的操作,因此这里定义一个用于操作 store 的对象
const storeApi = {
getState: store.getState,
// 用匿名箭头函数包了一层,它里面的 dispatch 就被固定下来,即为上面声明的 dispatch 变量
// 下面 27 行代码, dispatch 变量被赋值为 增强之后的 dispatch 函数
// 因此不管哪个中间件中使用 storeApi.dispatch 都相当于调用增强的 dispatch
dispatch: (action) => dispatch(action),
// 下面两种方式是等价的,都是直接引用的 store 里面的原始 dispatch,不符合要求
// dispatch: store.dispatch,
// dispatch,
};
// chain 里面是:每个中间件接收 storeApi 执行后的结果,也是一个函数
// middleware 是一个三层嵌套函数形成的闭包,每层函数依次接收的参数是 storeApi, next, action
const chain = middlewares.map((middleware) => middleware(storeApi));
// 这里就是增强的 dispatch,供外部使用
dispatch = compose(...chain)(dispatch);
return {
...store,
dispatch,
};
};
};
const compose = (...func) => {
if (!func.length) return (arg) => arg;
if (func.length === 1) return func[0];
return func.reduce(
(a, b) =>
(...arg) =>
a(b(...arg))
);
};
复制代码
- 修改 createStore 函数,使其第二个参数为 applyMiddleware 的执行结果
export function createStore(reducer, enhancer) {
// 不使用中间件时,dispatch 只能接受一个普通对象
// 使用中间件时,则可以增加 dispatch 的能力
// 那么可以将一个接收中间件的 applyMiddleware 函数的执行结果
// 当作 createStore 的第二个参数传入,也就是 enhancer,它也是一个函数
if (enhancer) {
// 这里就是如果传入 enhancer,就返回增强了 dispatch 的 store
// 否则就还是按照原来的逻辑走
return enhancer(createStore)(reducer);
}
...
}
复制代码
- 修改 store 的创建
- 在 createStore 的第二个参数上,传入 applyMiddleware 接收了三个中间件之后的执行结果
...
export const store = createStore(
countReducer,
applyMiddleware(promise, thunk, logger)
);
...
复制代码
- applyMiddleware 接收了中间件执行之后,返回了一个函数 enhancer
- enhancer 也是一个函数,其应用了函数式编程中柯里化的思想
- 接收一个,参数为 createStore 的函数
- 返回一个,接收一个参数为 reducer 函数的函数
- enhancer 最后执行完成之后,返回的对象中的 dispatch 就是增强之后的 dispatch
实现 combineReducers
- 上面实现的功能都是基于一个 reducer 的,那么多个 reducer 的情况该如何处理呢?
- redux 提供的 combineReducers 函数就是用来将多个 reducer 进行聚合
/**
*
* combineReducers 接收 reducer 的映射关系
*
* 返回合并后的 总 reducer
*
*/
export const combineReducers = (reducerDict) => {
// 返回是合并之后的总 reducer,所以也会接收 prevState 和 action
return (prevState = {}, action) => {
const nextState = {};
let hasChanged = false;
for (const key in reducerDict) {
const reducer = reducerDict[key];
nextState[key] = reducer(prevState[key], action);
hasChanged = hasChanged || nextState[key] !== prevState[key];
}
hasChanged =
hasChanged ||
Object.keys(nextState).length !== Object.keys(prevState).length;
return hasChanged ? nextState : prevState;
};
};
复制代码
- combineReducers 接收 reducer 的映射关系,返回一个函数,这个函数就是聚合之后的
总 reducer
- 实现也很简单
- 按照 reducer 的映射关系,取出每个 reducer 函数并都进行执行
- 然后将执行后的结果按照 reducer 的映射关系生成一个数据对象
- 最后判断是否有 reducer 改变了 store 中的数据
- 是则返回这个新的数据对象
- 否则返回之前的数据对象