目录
1. Redux 是什么?
2. Redux 有什么用?
3. Redux 什么时候该用?
4. Redux 准则?
4.1. 状态管理
4.2. 不可变性
5. Redux 术语?
5.1. Actions
5.2. Action Creators
5.3. Reducers
5.4. Store
5.5. Dispatch
5.6. Selectors
6. Redux 数据流向?
7. Redux 工具包?
8. Redux 源码分析(上)
8.1. 总体目录结构
8.2. index.ts(入口)
8.3. 辅助——isPlainObject.ts
8.4. 辅助——warning.ts
8.5. 辅助——kindOf.ts
8.6. 内核——最基本的一个redux用例
8.7. 内核——createStore.ts
8.7.1. redux 的重要防御技能
8.7.2. redux 的 Observable 技能
8.7.3. 详解
8.8. 内核——bindActionCreators.ts
8.9. 内核——actionTypes.ts
8.10. 内核——combineReducers.ts
8.10.1. combineReducers 的重要防御技能
8.10.2. 详解
9. Redux 如何处理异步
9.1. reducer 中不能直接写异步逻辑
9.2. 有哪些异步处理中间件
9.3. redux-thunk
9.4. redux-promise
9.5. redux-promise-middleware
9.6. 总结一下
10. Redux 源码分析(下)
10.1. 内核——compose.ts
10.2. 内核——applyMiddleware.ts
11. Redux Toolkit
11.1. 是什么?
11.2. 包含什么?
11.3. 一个例子
1. Redux 是什么?
Redux 是JavaScript 应用的状态管理容器,提供集中式、可预测的状态管理。
Redux is a predictable state container for JavaScript apps. https://redux.js.org/introduction/getting-started
Redux is a pattern and library for managing and updating application state, using events called "actions". It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
2. Redux 有什么用?
Redux helps you manage "global" state - state that is needed across many parts of your application.
The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.
As the requirements for JavaScript single-page applications have become increasingly complicated, our code must manage more state than ever before. This state can include server responses and cached data, as well as locally created data that has not yet been persisted to the server. UI state is also increasing in complexity, as we need to manage active routes, selected tabs, spinners, pagination controls, and so on.
Managing this ever-changing state is hard. If a model can update another model, then a view can update a model, which updates another model, and this, in turn, might cause another view to update. At some point, you no longer understand what happens in your app as you have lost control over the when, why, and how of its state. When a system is opaque and non-deterministic, it's hard to reproduce bugs or add new features.
As if this weren't bad enough, consider the new requirements becoming common in front-end product development. As developers, we are expected to handle optimistic updates, server-side rendering, fetching data before performing route transitions, and so on. We find ourselves trying to manage a complexity that we have never had to deal with before, and we inevitably ask the question: is it time to give up? The answer is no.
This complexity is difficult to handle as we're mixing two concepts that are very hard for the human mind to reason about: mutation and asynchronicity. I call them Mentos and Coke. Both can be great in separation, but together they create a mess. Libraries like React attempt to solve this problem in the view layer by removing both asynchrony and direct DOM manipulation. However, managing the state of your data is left up to you. This is where Redux enters.
Following in the steps of Flux, CQRS, and Event Sourcing, Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen. These restrictions are reflected in the three principles of Redux.
3. Redux 什么时候该用?
Redux helps you deal with shared state management, but like any tool, it has tradeoffs. There are more concepts to learn, and more code to write. It also adds some indirection to your code, and asks you to follow certain restrictions. It's a trade-off between short term and long term productivity.
Redux is more useful when:
Not all apps need Redux. Take some time to think about the kind of app you're building, and decide what tools would be best to help solve the problems you're working on.
个人体会,当你的应用:
使用 Redux 是个好的选择
但是需要注意,如果用了 Redux,需要把应用的所有状态都存进去么?
NO. Global state that is needed across the app should go in the Redux store. State that's only needed in one place should be kept in component state.
In a React + Redux app, your global state should go in the Redux store, and your local state should stay in React components.
If you're not sure where to put something, here are some common rules of thumb for determining what kind of data should be put into Redux:
4. Redux 准则?
4.1. 状态管理
Let's start by looking at a small React counter component. It tracks a number in component state, and increments the number when a button is clicked:
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
It is a self-contained app with the following parts:
This is a small example of "one-way data flow":
However, the simplicity can break down when we have multiple components that need to share and use the same state, especially if those components are located in different parts of the application. Sometimes this can be solved by "lifting state up" to parent components, but that doesn't always help.
One way to solve this is to extract the shared state from the components, and put it into a centralized location outside the component tree. With this, our component tree becomes a big "view", and any component can access the state or trigger actions, no matter where they are in the tree!
By defining and separating the concepts involved in state management and enforcing rules that maintain independence between views and states, we give our code more structure and maintainability.
This is the basic idea behind Redux: a single centralized place to contain the global state in your application, and specific patterns to follow when updating that state to make the code predictable.
4.2. 不可变性
"Mutable" means "changeable".If something is "immutable", it can never be changed.
JavaScript objects and arrays are all mutable by default. If I create an object, I can change the contents of its fields. If I create an array, I can change the contents as well:
const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3
const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'
This is called mutating the object or array. It's the same object or array reference in memory, but now the contents inside the object have changed.
In order to update values immutably, your code must make copies of existing objects/arrays, and then modify the copies.
We can do this by hand using JavaScript's array / object spread operators, as well as array methods that return new copies of the array instead of mutating the original array:
const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}
const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}
const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')
// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')
Redux expects that all state updates are done immutably.
5. Redux 术语?
5.1. Actions
An action is a plain JavaScript object that has a type field. You can think of an action as an event that describes something that happened in the application.
The type field should be a string that gives this action a descriptive name, like "todos/todoAdded". We usually write that type string like "domain/eventName", where the first part is the feature or category that this action belongs to, and the second part is the specific thing that happened.
An action object can have other fields with additional information about what happened. By convention, we put that information in a field called payload.
A typical action object might look like this:
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
5.2. Action Creators
An action creator is a function that creates and returns an action object. We typically use these so we don't have to write the action object by hand every time:
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}
5.3. Reducers
A reducer is a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state: (state, action) => newState.
Reducers must always follow some specific rules:
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/increment') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
5.4. Store
The current Redux application state lives in an object called the store.
The store is created by passing in a reducer, and has a method called getState that returns the current state value:
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
5.5. Dispatch
The Redux store has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object. The store will run its reducer function and save the new state value inside, and we can call getState() to retrieve the updated value:
store.dispatch({ type: 'counter/increment' })
console.log(store.getState())
// {value: 1}
We typically call action creators to dispatch the right action:
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
5.6. Selectors
Selectors are functions that know how to extract specific pieces of information from a store state value. As an application grows bigger, this can help avoid repeating logic as different parts of the app need to read the same data:
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
6. Redux 数据流向?
Earlier, we talked about "one-way data flow", which describes this sequence of steps to update the app:
For Redux specifically, we can break these steps into more detail:
Here's what that data flow looks like visually:
7. Redux 工具包?
8. Redux 源码分析(上)
本文分析的源码是
目前Redux的最新发布版本
4.0.4
8.1. 总体目录结构
8.2. 入口——index.ts
8.3. 辅助——isPlainObject.ts
8.4. 辅助——warning.ts
8.5. 辅助——kindOf.ts
8.6. 内核——最基本的一个redux用例
import { createStore, combineReducers, bindActionCreators } from 'redux';
/**
* Initial State & Reducer & Selector & Action Creators
*/
// Initial State
const counterInitialState = {
value: 0,
};
// Reducer
function counterReducer(state = counterInitialState, action: any) {
const { type, payload } = action;
switch (type) {
case 'counter/INC': {
return {
...state,
value: state.value + payload,
};
}
case 'counter/DEC': {
return {
...state,
value: state.value - payload,
};
}
default:
return state;
}
}
// Selector
export const selectCount = (state: any) => state.count.value;
// Action Creators
function inc() {
return {
type: 'count/INC',
payload: 1,
};
}
function dec() {
return {
type: 'count/DEC',
payload: 1,
};
}
/**
* Another one
* Initial State & Reducer & Selector & Action Creators
*/
// Initial State
const timeInitialState = {
value: 0,
};
// Reducer
function timeReducer(state = timeInitialState, action: any) {
const { type, payload } = action;
switch (type) {
case 'time/UPDATE': {
return {
...state,
value: payload,
};
}
default:
return state;
}
}
export const selectTime = (state: any) => state.time.value;
function updateTime() {
return {
type: 'time/UPDATE',
payload: new Date().toLocaleTimeString(),
};
}
// combine reducer
const rootReducer = combineReducers({
count: counterReducer,
time: timeReducer,
});
// Create a store
const store = createStore(rootReducer);
// subscribe store change event
store.subscribe(() => {
const state = store.getState();
const count = selectCount(state);
const time = selectTime(state);
console.log(`count: ${count}, time: ${time}.`);
});
// bind Dispatch & Action Creator
const dispatchCounterInc = bindActionCreators(inc, store.dispatch);
const dispatchCounterDec = bindActionCreators(dec, store.dispatch);
const dispatchTimeUpdate = bindActionCreators(updateTime, store.dispatch);
// Dispatch actions
(async () => {
// 可以这样:
dispatchCounterInc();dispatchTimeUpdate();await delay(1000);
dispatchCounterInc();dispatchTimeUpdate();await delay(1000);
dispatchCounterDec();dispatchTimeUpdate();await delay(1000);
// 也可以这样:
store.dispatch(inc());store.dispatch(updateTime());await delay(1000);
store.dispatch(inc());store.dispatch(updateTime());await delay(1000);
store.dispatch(dec());store.dispatch(updateTime());await delay(1000);
})();
// some utils for test purpose
function delay(count: number) {
return new Promise((resolve) => setTimeout(resolve, count));
}
8.7. 内核——createStore.ts
8.7.1. redux 的重要防御技能
createStore.ts 中有一些逻辑,是为了能在不恰当使用 redux 时给出异常提示。
在 Redux 的设计中,reducer 应该是个纯函数、能重入、不应该有副作用。所以 redux 会当你在 reducer 中调用 getState、dispatch、subscribe、unsubscribe 时给出异常提示。
上述这些功能是 isDispatching变量负责实现
createStore.ts 中还有有一些逻辑,是为了即使是不恰当地使用 redux,也能规避掉错误。
上述功能是 ensureCanMutateNextListeners 负责实现
8.7.2. redux 的 Observable 技能
redux 的 store 也实现了 tc39/proposal-observable 接口,方便与 Rxjs 等库对接。
8.7.3. 详解
8.8. 内核——bindActionCreators.ts
用于把 store.dispatch 和 action creator 整合起来,方便使用。
8.9. 内核——actionTypes.ts
8.10. 内核——combineReducers.ts
当业务应用变得复杂,我们就需要对 reducer 函数进行拆分,拆分后的每一块独立负责管理 state 的一部分。combineReducers() 的作用就是把由多个不同 reducer 函数作为 value 的 object 合并成为一个总的 root reducer 函数。然后可以对这个 root reducer 调用 createStore()。合并后的 root reducer 可以调用各个子 reducer,并把他们的结果合并成一个 state 对象。
8.10.1. combineReducers 的重要防御技能
上述功能由
getUnexpectedStateShapeWarningMessage 负责实现
上述功能由
assertReducerShape
负责实现
8.10.2. 详解
9. Redux 如何处理异步
9.1. 异步逻辑应该写在哪?
异步逻辑不能直接写在 reducer 中
异步逻辑应放置在
Redux中间件中处理
!! Middleware !!
Redux中间件实质是
store.dispatch函数的增强器
它们拦截特定的Action
并在其中把带有副作用的工作完成
(例如:异步处理、日志记录...)
9.2. 有哪些异步处理中间件
redux-thunk:不到10行代码,精简到炸! redux-promise:基于Promise的异步处理; redux-promise-middleware:还是Promise; redux-saga:最优雅!最复杂!
9.3. redux-thunk
redux-thunk 中间件扩展了 redux 的 dispatch 功能,它允许你 dispatch 一个函数(即:thunk),异步逻辑就放在这个函数中处理;
源码:
示例:注意现在 dispatch 的是一个函数
import { createStore, combineReducers, bindActionCreators, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
/**
* Initial State & Reducer & Selector & Action Creators
*/
// Initial State
const counterInitialState = {
value: 0,
};
// Reducer
function counterReducer(state = counterInitialState, action: any) {
const { type, payload } = action;
switch (type) {
case 'count/INC': {
return {
...state,
value: state.value + payload,
};
}
default:
return state;
}
}
// Selector
export const selectCount = (state: any) => state.count.value;
// Action Creators
function inc() {
return {
type: 'count/INC',
payload: 1,
};
}
function asyncInc(){
return (dispatch: any, getState: any) => {
setTimeout(() => {
dispatch(inc());
}, 5 * 1000);
};
}
// combine reducer
const rootReducer = combineReducers({
count: counterReducer,
});
// apply Middleware
const enhancer = applyMiddleware(thunkMiddleware);
// Create a store
const store = createStore(rootReducer, enhancer);
store[Symbol.observable]().subscribe({
next(state: any) {
const count = selectCount(state);
console.log(count);
},
});
// Dispatch actions
(async () => {
store.dispatch(asyncInc());
})();
9.4. redux-promise
源码:
9.5. redux-promise-middleware
源码:
示例:
import { createStore, combineReducers, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
/**
* Initial State & Reducer & Selector & Action Creators
*/
// Initial State
const counterInitialState = {
value: 0,
loading: false,
};
// Reducer
function counterReducer(state = counterInitialState, action: any) {
const { type, payload } = action;
switch (type) {
case 'count/INC_PENDING': {
return {
...state,
loading: true
};
}
case 'count/INC_FULFILLED': {
return {
...state,
value: state.value + payload,
loading: false
};
}
default:
return state;
}
}
// Action Creators
function asyncInc() {
return {
type: 'count/INC',
payload: new Promise((resolve) => setTimeout(()=>{
resolve(10)
}, 5000)),
};
}
// combine reducer
const rootReducer = combineReducers({
count: counterReducer,
});
// apply Middleware
const enhancer = applyMiddleware(promiseMiddleware);
// Create a store
const store = createStore(rootReducer, enhancer);
store[Symbol.observable]().subscribe({
next(state: any) {
console.log(state.count);
},
});
// Dispatch actions
(async () => {
store.dispatch(asyncInc());
})();
9.6. 总结一下
redux:
store.dispatch({type: "INC", payload:....});
redux-thunk:
store.dispatch((dispatch, getState) => { ... })
redux-promise:
store.dispatch({type:"INC", payload: Promise});
redux-promise-middleware:
store.dispatch({type:"INC", payload: Promise});
redux-saga:
store.dispatch({type:"INC", payload:...});
10. Redux 源码分析(下)
10.1. 内核——compose.ts
compose 是函数式编程中的组合,compose 将 chain 中的所有匿名函数,[f1, f2, ... , fx, ..., fn],组装成一个新的函数,即新的 dispatch,当新 dispatch 执行时,[f1, f2, ... , fx, ..., fn],从右到左依次执行( 所以顺序很重要)。
以当 compose 执行完后,我们得到的 dispatch 是这样的,假设 n = 3。
dispatch = f1(f2(f3(store.dispatch))))
10.2. 内核——applyMiddleware.ts
applyMiddleware 是 Redux 官方附带的一个 Store 增强器。
回顾一下 createStore 中的 enhancer 增强技能
使用示例
applyMiddleware 机制的核心在于组合 compose,将不同的 middlewares 一层一层包裹到原生的 dispatch 之上,而为了方便进行 compose,需对 middleware 的设计采用柯里化 curry 的方式,达到动态产生 next 方法以及保持 store 的一致性。
middleware 中 next 与 dispatch 间的关系图:
11. Redux Toolkit
11.1. 是什么?
Redux Toolkit 是官方提供的为简化 Redux 开发而退出的工具箱。
Redux Toolkit was originally created to help address three common concerns about Redux: "Configuring a Redux store is too complicated" "I have to add a lot of packages to get Redux to do anything useful" "Redux requires too much boilerplate code" https://redux.js.org/redux-toolkit/overview
11.2. 包含什么?
Redux Toolkit includes:
11.3. 一个例子
import { createSlice, configureStore } from '@reduxjs/toolkit';
const delay = (count: number) => new Promise((resolve) => setTimeout(resolve, count));
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
},
reducers: {
INC: (state, action) => {
state.value += action.payload;
},
DEC: (state, action) => {
state.value -= action.payload;
},
},
});
const { INC, DEC } = counterSlice.actions;
const timeSlice = createSlice({
name: 'time',
initialState: {
value: 0,
},
reducers: {
UPDATE: (state, action) => {
state.value = action.payload;
},
},
});
const { UPDATE } = timeSlice.actions;
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
time: timeSlice.reducer,
},
});
store.subscribe(() => {
const state = store.getState();
console.log(state);
});
// Dispatch actions
(async () => {
store.dispatch(INC(1));
store.dispatch(UPDATE(new Date().toLocaleTimeString()));
await delay(1000);
store.dispatch(INC(1));
store.dispatch(UPDATE(new Date().toLocaleTimeString()));
await delay(1000);
store.dispatch(DEC(1));
store.dispatch(UPDATE(new Date().toLocaleTimeString()));
await delay(1000);
})();
参考:
《深入React技术栈》 https://github.com/reduxjs/redux https://github.com/reduxjs/redux-thunk https://github.com/evgenyrodionov/redux-logger