前端闲聊系列(2):redux是怎么回事

854 阅读9分钟

概述

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,就是把上述过程进行了封装。

中间件

这里的中间件在谁和谁中间呢?

image.png

当我们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的状态自动dispatchpendingfulfilled, and rejected三种类型的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))

RTK query

RTK query也是rtk toolkit的一部分,用来处理网络请求和缓存相关的功能。

具体可以参考这篇