入门
Redux 是 JavaScript应用的状态容器,提供可预测的状态管理。
可以让你开发出行为稳定可预测的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供超爽的开发体验,比如有一个Redux Devtools
可以查看实时预览。
Redux 除了和React一起用外,还支持其他 UI 框架。它体小精悍(只有 2kb,包括依赖),却有很强大的插件扩展生态。
安装
使用 React 脚手架安装 Redux
官方推荐的创建React Redux
新应用的方式是使用 官方 Redux+JS 模版,它基于 Create React App,它利用了 Redux Toolkit 和 Redux
与React
组件的集成.
npx create-react-app my-app --template redux
NPM 安装 Redux
Redux
核心库同样有NPM
软件包,安装方式如下:
# NPM
npm install redux
# Yarn
yarn add redux
安装 Redux Toolkit
Redux Toolkit
是官方推荐的编写 Redux
逻辑的方法。它包含了Redux
核心,并包含我们认为对于构建 Redux
应用必不可少的软件包和功能。Redux Toolkit
简化了大多数 Redux
任务,防止了常见错误,并使编写Redux
应用程序更加容易。
RTK
包含了有助于简化许多常见场景的工具,包括 配置 Store, 创建 reducer 并编写 immutable 更新逻辑, 甚至还包含 一次性创建整个 State 的 “slice”。
无论新手还是老手,都可以用RTK
开发出更好的代码
安装方法
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
三大核心
Redux有 3 大核心概念:
- Action
- Reducer
- Store
其中Action和Store都非常好理解,我们可以直接按照其字面意思,将他们理解为动作和储存。
Action
action表示应用中的各类动作或操作,不同的操作会改变应用相应的state状态,说白了就是一个带type属性的对象。
type
字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"
。我们通常把那个类型的字符串写成“域/事件名称”,其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情。
action 对象可以有其他字段,其中包含有关发生的事情的附加信息。按照惯例,我们将该信息放在名为 payload
的字段中。
一个典型的 action 对象可能如下所示:
const addTodoAction = {
type: "todos/todoAdded",
payload: "Buy milk",
};
Action Creator
action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:
const addTodo = (text) => {
return {
type: "todos/todoAdded",
payload: text,
};
};
Store
store 则是我们储存state的地方。
createStore
我们通过redux当中的createStore
方法来创建一个store,它提供 3 个主要的方法。
// 以下代码示例来自redux官方教程
const createStore = (reducer) => {
let state; //实际存放数据的地方,叫做对象树
let listeners = []; //监听器
// 用来返回当前的state
const getState = () => state;
// 根据action调用reducer返回新的state并触发listener
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach((listener) => listener());
};
/* 这里的subscribe有两个功能
* 调用 subscribe(listener) 会使用listeners.push(listener)注册一个listener
* 而调用 subscribe 的返回函数则会注销掉listener
*/
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter((l) => l !== listener);
};
};
return { getState, dispatch, subscribe };
};
Dispatch
Redux store 有一个方法叫 dispatch
。更新 state 的唯一方法是调用 store.dispatch()
并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState()
可以获取新 state。
store.dispatch({ type: "counter/increment" });
console.log(store.getState());
// {value: 1}
dispatch 一个 action 可以形象的理解为 “触发一个事件”。发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。
我们通常调用 action creator 来调用 action:
const increment = () => {
return {
type: "counter/increment",
};
};
store.dispatch(increment());
console.log(store.getState());
// {value: 2}
Selector
Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑
const selectCounterValue = (state) => state.value;
const currentValue = selectCounterValue(store.getState());
console.log(currentValue);
// 2
Reducer
中文可以叫做缩减器或者折叠器,reduce操作被称为Fold折叠,之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue)
的回调函数属于相同的类型。
我们来看一下array使用reduce方法的具体例子:
// 以下代码示例来自 MDN JavaScript 文档
/* 这里的callback是和reducer非常相似的函数
* arr.reduce(callback, [initialValue])
*/
var sum = [0, 1, 2, 3].reduce(function (acc, val) {
return acc + val;
}, 0);
// sum = 6
/* 注意这当中的回调函数 (prev, curr) => prev + curr
* 与我们redux当中的reducer模型 (previousState, action) => newState 看起来是不是非常相似呢
*/
[0, 1, 2, 3, 4].reduce((prev, curr) => prev + curr);
我们再来看一个简单的具体的reducer的例子:
// 以下代码示例来自redux官方教程
// reducer接受state和action并返回新的state
const todos = (state = [], action) => {
// 根据不同的action.type对state进行不同的操作,一般都是用switch语句来实现
switch (action.type) {
case "ADD_TODO":
return [
// 这里是ES7里的对象展开运算符语法 将下面3个值赋值给state对象
...state,
{
id: action.id,
text: action.text,
completed: false,
},
];
// 不知道是什么action类型的话则返回默认state
default:
return state;
}
};
reducer 是一个函数,接收当前的 state
和一个 action
对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState
。 你可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。
Reducer 必需符合以下规则:
- 仅使用
state
和action
参数计算新的状态值 - 禁止直接修改
state
。必须通过复制现有的state
并对复制的值进行更改的方式来做 不可变更新(immutable updates)。 - 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码
reducer 函数内部的逻辑通常遵循以下步骤:
- 检查 reducer 是否关心这个 action
- 如果是,则复制 state,使用新值更新 state 副本,然后返回新 state
- 否则,返回原来的 state 不变
下面是 reducer 的小例子,展示了每个 reducer 应该遵循的步骤:
const initialState = { value: 0 };
function counterReducer(state = initialState, action) {
// 检查 reducer 是否关心这个 action
if (action.type === "counter/increment") {
// 如果是,复制 `state`
return {
...state,
// 使用新值更新 state 副本
value: state.value + 1,
};
}
// 返回原来的 state 不变
return state;
}
Redux 数据流
早些时候,我们谈到了“单向数据流”,它描述了更新应用程序的以下步骤序列:
- State 描述了应用程序在特定时间点的状况
- 基于 state 来渲染 UI
- 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新
- 基于新的 state 重新渲染 UI
具体来说,对于 Redux,我们可以将这些步骤分解为更详细的内容:
- 初始启动:
- 使用最顶层的 root reducer 函数创建 Redux store
- store 调用一次 root reducer,并将返回值保存为它的初始
state
- 当 UI 首次渲染时,UI 组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。同时监听 store 的更新,以便他们可以知道 state 是否已更改。
- 更新环节:
- 应用程序中发生了某些事情,例如用户单击按钮
- dispatch 一个 action 到 Redux store,例如
dispatch({type: 'counter/increment'})
- store 用之前的
state
和当前的action
再次运行 reducer 函数,并将返回值保存为新的state
- store 通知所有订阅过的 UI,通知它们 store 发生更新
- 每个订阅过 store 数据的 UI 组件都会检查它们需要的 state 部分是否被更新。
- 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页
基础示例
应用的所有状态以一个对象的形式存储在唯一的 store 里面,我们可以把这个存储状态的对象叫状态树,而更改对象树的唯一方式就是 emit 一个 action(这个 action 是一个描述发生了什么的对象),为了把这个 action 转换为对象树里面的某个值,就需要提供一个 reducer, 所以简单来说,一个 redux 程序包括一个 store、一个 reducer 和多个 action
一个简单的例子
import { createStore } from 'redux'
//这是一个reducer,更具不同的类型变更状态
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 }
case 'counter/decremented':
return { value: state.value - 1 }
default:
return state
}
}
// 创建一个新的store对象
// 它拥有以下API { subscribe, dispatch, getState }.
let store = createStore(counterReducer)
// 你可以通过subscribe()更新UI库的响应状态
// 通常你是用视图绑定器,而不是直接使用subscribe()方法
// 他们帮助其他使用这个类型的订阅者
store.subscribe(() => console.log(store.getState()))
// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}
在典型的Redux应用程序中,只有一个store以及一个根reducer函数。随着应用程序的增长,您可以将根 reducer 拆分为较小的 reducer,分别在状态树的不同部分上进行操作。这就像在 React 应用程序中只有一个根组件一样,但是它是由许多小组件组成的。
可能对于我们简单的应用程序来说,这种设计方式过于复杂,不过可以扩展到大型或者超大型的应用程序
Redux Toolkit 示例
Redux Toolkit 简化了编写 Redux 逻辑和设置 store 的过程。 使用 Redux Toolkit,相同的逻辑如下所示:
import { createSlice, configureStore } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0,
},
reducers: {
incremented: (state) => {
// Redux Toolkit 允许我们在Reducer中编写"变化"逻辑
// 不过实际上是使用 RTK 的Immer 库,检测"草稿状态"的更改,变更state为新的状态
state.value += 1;
},
decremented: (state) => {
state.value -= 1;
},
},
});
export const { incremented, decremented } = counterSlice.actions;
const store = configureStore({
reducer: counterSlice.reducer,
});
// 通过订阅获取状态
store.subscribe(() => console.log(store.getState()));
// 仍然将动作对象传递给`dispatch`
store.dispatch(incremented());
// {value: 1}
store.dispatch(incremented());
// {value: 2}
store.dispatch(decremented());
// {value: 1}
Redux Toolkit使我们可以编写更精短的代码,更易于阅读,同时仍然遵循同样的 Redux 规范和数据流。
教程
使用的时候建议浏览器中安装了 React 和 Redux DevTools 扩展:
- React DevTools 扩展:
- Redux DevTools Extension:
Redux 库和工具
Redux 是一个小型的独立 JS 库。 但是,它通常与其他几个包一起使用
React-Redux
Redux可以与任何 UI 框架集成,并且最常与 React 一起使用。 React-Redux 是我们的官方包,它允许您的 React 组件通过读取状态片段和调度操作来更新存储来与 Redux 存储进行交互。
Redux Toolkit
Redux 的工具库,简化写法
Redux DevTools 扩展
可以显示 Redux 存储中状态随时间变化的历史记录。这允许您有效地调试应用程序,如time-travel debugging
概念
State 管理
让我们从一个小的 React 计数器组件开始。 它跟踪组件状态中的数字,并在单击按钮时增加数字:
function Counter() {
// State: 一个计数器应用,默认状态为0
const [counter, setCounter] = useState(0);
// Action: 当事件发生后,触发状态更新的代码
const increment = () => {
setCounter((prevCounter) => prevCounter + 1);
};
// View: UI 定义
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
);
}
这是一个包含以下部分的自包含应用程序:
- state:驱动应用的真实数据源头
- view:基于当前状态的 UI 声明性描述
- actions:根据用户输入在应用程序中发生的事件,并触发状态更新
接下来简要介绍 “单向数据流(one-way data flow)”:
- 用 state 来描述应用程序在特定时间点的状况
- 基于 state 来渲染出 View
- 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新,生成新的 state
- 基于新的 state 重新渲染 View
action -> state -> view -> action //单走向
然而,当我们有多个组件需要共享和使用相同 state时,可能会变得很复杂,尤其是当这些组件位于应用程序的不同部分时。有时这可以通过 “提升 state” 到父组件来解决,但不是那么好
解决这个问题的一种方法是从组件中提取共享 state,并将其放入组件树之外的一个集中位置。这样,我们的组件树就变成了一个大“view”,任何组件都可以访问 state 或触发 action,无论它们在树中的哪个位置!
通过定义和分离 state 管理中涉及的概念并强制执行维护 view 和 state 之间独立性的规则,代码变得更结构化和易于维护。
这就是 Redux 背后的基本思想:应用中使用集中式的全局状态来管理,并明确更新状态的模式,以便让代码具有可预测性。
不可变性 Immutability
“Mutable” 意为 “可改变的”,而 “immutable 意为永不可改变。
JavaScript 的对象(object)和数组(array)默认都是 mutable 的。如果我创建一个对象,我可以更改其字段的内容。如果我创建一个数组,我也可以更改内容:
const obj = { a: 1, b: 2 };
// 对外仍然还是那个对象,但它的内容已经变了
obj.b = 3;
const arr = ["a", "b"];
// 同样的,数组的内容改变了
arr.push("c");
arr[1] = "d";
这就是 改变 对象或数组的例子。内存中还是原来对象或数组的引用,但里面的内容变化了。
如果想要不可变的方式来更新,代码必需先复制原来的 object/array,然后更新它的复制体。
JavaScript array/object 的展开运算符(spread operator)可以实现这个目的:
const obj = {
a: {
// 为了安全的更新 obj.a.c,需要先复制一份
c: 3,
},
b: 2,
};
const obj2 = {
// obj 的备份
...obj,
// 覆盖 a
a: {
// obj.a 的备份
...obj.a,
// 覆盖 c
c: 42,
},
};
const arr = ["a", "b"];
// 创建 arr 的备份,并把 c 拼接到最后。
const arr2 = arr.concat("c");
// 或者,可以对原来的数组创建复制体
const arr3 = arr.slice();
// 修改复制体
arr3.push("c");