概述
redux是一个状态管理工具,以react为例,每个组件可以保有自己的状态,也可以有从props或context传入的状态,当react组件中的状态发生改变时,以当前组件为根节点的组件树就会重新渲染,当然可以通过一定的方式(比如 React.memo)避免不必要的渲染。
以上三种来源的状态和ui的同步都是react维护的,那么redux这种第三方状态管理工具是怎么工作的呢?
除了redux,本文还会在此基础上讨论redux-thunk,redux-saga,redux tookit。
redux本身
redux本身是个状态容器,数据以树的结构保存在store中,正如react一直强调的immutable,redux中的数据也是不可变的。
状态改变的方法只有dispatch action,其中dispatch是唯一可以改变state的方法,action是包含type和payload属性的普通对象,用来描述对状态进行的操作。
action会被发送给reducer,reducer是一个操作state的纯函数,会根据action的指令返回一个新的state。其中一个store可以有一个reducer来全权负责,也可以模块化分到若干个reducer中处理,注意尽管这里用模块化来描述,接收action时并不区分模块,只要匹配到对应type就会对应处理。
当state变化后,订阅到这个数据源的回调就会被执行,即,发布订阅模式,就是面试中经常会考到的手写的eventEmitter。
redux仅提供了几个api,虽然有一些推荐的最佳实践,但具体的使用自由度还是很大,就跟react本身一样。
import { createStore } from 'redux'
//实际修改state的reducer,包含默认state
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
let store = createStore(counterReducer)
//注册
store.subscribe(() => console.log(store.getState()))
//修改,每次修改后会触发前面注册的回调
store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}
redux源码
对于源码我们关注createStore和applyMiddleware两处实现
createStore
store实现了观察者模式,每次dispatch时会调用对应回调函数,为了让源码更明了,那部分隐去了。
这里关注其中的enhancer、dispatch、getState、replaceReducer
const randomString = () =>
Math.random().toString(36).substring(7).split('').join('.')
//这两个action其实在reducer中没处理,因此只用来初始数据currentState
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`,
}
export default function createStore(reducer, preloadedState, enhancer) {
//增强器,是个高阶函数,用来修改createStore,比如applyMiddleware
//type StoreEnhancer = (next: StoreCreator) => StoreCreator
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState)
}
//当前的reducer
let currentReducer = reducer
//当前的state
let currentState = preloadedState
//获取state
function getState() {
return currentState
}
function dispatch(action) {
currentState = currentReducer(currentState, action)
//每次dispatch时都会执行观察者模式的回调,代码略
return action
}
function replaceReducer(nextReducer) {
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
getState,
replaceReducer,
}
}
applyMiddleware
用法如下,参数是middleware参数列表,其中middle签名为({ getState, dispatch }) => next => action,返回一个enhancer,即createStore的第三个参数。
applyMiddleware(...middleware)
具体使用如
import { createStore, applyMiddleware } from 'redux'
import todos from './reducers'
function logger({ getState }) {
return next => action => {
console.log('will dispatch', action)
// 调下个中间件的dispatch方法
const returnValue = next(action)
console.log('state after dispatch', getState())
//一般会返回action本身,除非下个中间件修改了它
return returnValue
}
}
const store = createStore(todos, ['Use Redux'], applyMiddleware(logger))
store.dispatch({
type: 'ADD_TODO',
text: 'Understand the middleware'
})
// (These lines will be logged by the middleware:)
// will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' }
// state after dispatch: [ 'Use Redux', 'Understand the middleware' ]
具体源码解读为
//用于依次调用中间件函数,并将前者的结果作为后者执行的参数,如
//compose(f1, f2, f3)(1);等价于f3(f2(f1(1)))
function compose(...funcs) {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
//返回一个enhancer
export default function applyMiddleware(...middlewares) {
//这里的...args指的是createStore的前两个参数
return (createStore) => (...args) => {
//创建store
const store = createStore(...args)
//避免chain的生成不要用dispatch
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
}
//依次调用中间件,并传参middlewareAPI,主语中间件执行的结果是参数是next的函数
//第一个dispatch是为了在这个过程不要使用dispatch
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
//将真正的next,即dispatch传入
dispatch = compose(...chain)(store.dispatch)
//增强后的store,其中dispatch是被中间件修改过的
return {
...store,
dispatch,
}
}
}
redux异步中间件
前面说修改store中的state要使用dispatch,这就是个简单的函数调用,然后被纯函数reducer接收处理,整个过程不存在任何发生异步(以及执行副作用)的时机。
其实想要执行异步也很简单,就是先把异步和副作用执行完了再dispatch不就好了,这也是异步中间件的工作过程。
这里说的中间件,主要有redux-chunk和redux-saga,就是把上述过程进行了封装。
中间件
这里的中间件在谁和谁中间呢?
当我们dispatch一个action后,并不会直接到达reducer,而是会路过一串中间件,中间件可以访问store和当前action,然后任意操作后将action传给下一个中间件或reducer,完成当前action表示的指令。
redux-chunk
redux-chunk 很简单,就是判断如果action是一个函数,就调用当前函数,并将dispath和getState作为参数传入,函数体内可以执行完任意操作后,将真正的action dispatch掉。
function createThunkMiddleware(extraArgument) {
return function (_ref) {
var dispatch = _ref.dispatch,
getState = _ref.getState;
return function (next) {
return function (action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
};
}
var thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
chunk的含义就像这里表示的,将一个工作推迟执行,即将dispatch推迟到异步执行以后
redux-saga
redux-saga认为自己是redux-chunk的升级版(本质还是dispatch的延迟执行),用generator函数避免了回调,并且更容易测试。在带来这些好处的同时也引入了很多新的概念和api,越来越不react了。
saga是一个专业很强的术语,这里可以把saga看成一个工作线程,负责执行一段副作用。
每个saga都是一个generator函数,可以访问(select)和修改(put)redux state,yield后面是一个表示effect,即副作用的对象,可以直接是一个promise,但因为不方便测试,因此不推荐,推荐的方法是用call或fork等调用另一个saga或普通函数执行一个同步和异步的任务,这些任务的编排方式除了顺序执行还可以像promise.all等其他组织方式。
从整体来看,saga可以分为watcher和worker两种,即前一种是用来监听对应类型的action,然后再调用后一种真正的执行副作用,然后再dispatch另一个action。
为了表示异步loading,可能还需要定义并dispatch其他的action,写起来较为繁琐。
总结一下,redux-saga将redux-chunk中自由度很高的一个执行副作用的函数,又拆分成两个阶段,同时每个阶段又可以以不同的组织形式,最后再通过put,将action发送到reducer完成整个过程
react-redux
前面讲了redux本身的含义,那么为什么要用到react中呢?
react应用中,单个组件内的状态可以用useState;多个组件共享的状态可以提升到共同的父组件中,并用props传入;更加复杂的状态共享,比如用户信息等应用全局的状态可以用context保存在根组件,避免了一层层以props的形式传入。
那么问题来了,redux用来干什么,它的作用和第三种状态很像,被用来专门存储状态,当然不用也是可以的。
react-redux是连接react和redux的一层封装,即将个通用的数据容器转化为react应用的状态,而状态的使用可以分读和写两部分。
基本用法
像之前定义完store后在应用根组件外面添加一个组件,并将store传入
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
//在具体组件中使用对应hook对store进行访问和dispatch action。
import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'
import styles from './Counter.module.css'
export function Counter() {
const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
</div>
)
}
完整例子参考React Redux Quick Start
读
在组件Provider源码中,store被作为context的值传入,因此所有的子组件都可以对其进行访问。
写
到目前为止,我们的react app获得了store的访问和修改权限,但是我们应该还知道,现在的redux和一个全局变量没什么区别的,并不会像react state一样修改时引起页面rerender。
这里的具体实现是当store的state发生变化时,会对前后的state进行前对比后,执行forceRender强制重渲染。
const [, forceRender] = useReducer((s) => s + 1, 0)
redux-toolkit
因为redux核心只包含少量的功能,处理复杂功能时需要引入大量中间件,以及为了最佳实践等要写大量 Boilerplate / Verbosity 的代码,因为redux官方在此基础上推出了toolkit,来简化redux在复杂场景的使用。
注意,redux-toolkit的本质是对现有功能的简化,没太多需要理解的,唯手熟尔,这里大概介绍一下。
- configureStore封装了createStore,并自带redux-thunk和devtool
- createSlice 进一步封装了createReducer()和createAction,会在创建reducer的同时创建action,导出后直接dispatch,而且内置了immer,在创建reducer时省去了switch case。
- createAsyncThunk 首先创建一个chunk,然后添加到reducer中,在最终dispatch时会根据promise的状态自动dispatch
pending,fulfilled, andrejected三种类型的action。
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
- createEntityAdapter 用来像操作数据库一样操作store中保存的数据,what and why
RTK query
RTK query也是rtk toolkit的一部分,用来处理网络请求和缓存相关的功能。
具体可以参考这篇