这篇Redux,Redux-Toolkit教程真的很详细

Redux概念

什么是 Redux ?

Redux 是一个使用叫作 "actions" 的事件去管理和更新应用状态的模式和工具库。 它以集中式 Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

我为什么要用 Redux ?

Redux 帮助你管理"全局"状态 - 那些应用程序的许多部分都需要的状态。

**Redux 提供的模式和工具使你更容易理解应用程序中的状态何时、何地、为什么以及如何更新,以及当这些更改发生时你的应用程序逻辑将如何表现。 **Redux 指导你编写可预测和可测试的代码,这有助于让你确信你的应用程序将按预期工作。

我什么时候应该用 Redux ?

Redux 可帮助你处理共享状态的管理,但与任何工具一样,它也有权衡。有更多的概念需要学习,还有更多的代码需要编写。它还为你的代码添加了一些额外代码,并要求你遵循某些限制。这是短期和长期生产力之间的权衡。

Redux 在以下情况下更有用:

  1. 在应用的大量地方,都存在大量的状态
  2. 应用状态会随着时间的推移而频繁更新
  3. 更新该状态的逻辑可能很复杂
  4. 中型和大型代码量的应用,很多人协同开发

并非所有应用程序都需要 Redux。 花一些时间思考你正在构建的应用程序类型,并决定哪些工具最能帮助解决你正在处理的问题。

我理解的Redux

redux实际上就是一个全局状态管理器,能够做到当某个全局状态变化的时候,能通知到所有用这个状态的地方响应式修改数据。redux的实现是基于前端很重要的一个开发模式:"发布订阅模式",如果对发布订阅模式还不是很了解的,可以查阅我的另一篇文章:详解"观察者"模式和"发布订阅"模式

redux需要注意的是,跟react没有任何关系,只是单词长得像,redux可以用于任何框架项目,甚至于原生小demo中,与vuex不同,vuex官网介绍就是为vue定制的全局状态管理器。
我们下面就用最原生的代码html+js介绍redux和redux-toolkit原理和用法

Redux数据流

简单的单向数据流

在我们前端开发中,很多情况下数据流都是单项数据流即最简单的通过下面过程进行数据页面交互

  1. State 描述了应用程序在特定时间点的状况
  2. 基于 state 来渲染 UI
  3. 发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新
  4. 基于新的 state 重新渲染 UI

Redux的单项数据流

那么我们的redux数据流其实也是单向数据流,只是稍微复杂了一些,如下图所示

react修改状态需要如下几个步骤:

  1. 创建一个reducer:reducer函数用于计算更新状态,向reducer函数中传入当前状态值和需要处理时动作,也就是action,reducer函数通过当前值和动作action计算出最新值并返回
  2. 创建store:我们通过之前创建的计算更新状态的reducer函数,使用Redux.createStore方法并传入reducer函数,创建出一个store。创建出的store包含三个和新方法:dispatch,subscribe,getState
  3. 使用subscribe订阅修改ui函数:首先我们需要创建修改ui的函数,并使用store中的subscribe方法订阅该函数,当store中的state发生改变时,就会调用订阅的函数从而修改ui
  4. 使用dispatch触发reducer:当有某个事件触发时(例如按钮点击事件),调用store中的dispatch函数,并传入需要触发的动作action和计算所需要用到的数据payload,去触发reducer计算函数,需要将调用dispatch传入的参数再传给reducer函数
  5. 使用getState函数获取最新状态:当reducer函数执行完之后,最新的状态已经更新,此时redux会调用subscribe订阅的函数,并在订阅的函数中调用getState获取最新状态,然后更新到ui上

Redux,Redux-Toolkit详解

准备工作

我们下面的案例是直接使用原生的js实现,并且js直接在html中引用,不使用任何框架。因此我们需要下载两个文件:redux.jsredux-toolkit.umd.js,这两个是Redux,Redux-Toolkit的打包后的代码,不使用min压缩代码是为了方便看源码。

下载文件

下载这两个文件方式有很多,我这里介绍两个最简单的,一个就是使用npm下载这两个包,然后到dist文件夹中找到这两个文件复制出来就好了

第二个方法就是去我github下载,Github下载地址,这里面的redux文件夹中有处理好的代码和下面的案例源码

处理文件

如果是在npm包里获取到这两个文件,需要稍作处理(如果是我的github下载的就不用了,我处理好了)

  1. 首先去掉redux-toolkit.umd.js文件中最后一行的//# sourceMappingURL=redux-toolkit.umd.js.map注释,这个注释意思是需要请求redux-toolkit.umd.js.map文件,我们这里用不到,去掉之后可以防止报警告
  2. 这两个文件中并没有使用es6模块化方式导出我们需要的api,他们的默认导出方式是commonJS,但是他们对外暴露了两个全局对象,redux.js文件中暴露的是Reduxredux-toolkit.umd.js暴露出的是RTK,我们在各自文件底部将其使用export导出,后面我们就可以在自己的文件中使用import导入了
javascript 复制代码
// redux.js文件底部
export default Redux

// redux-toolkit.umd.js文件底部
export default RTK
  1. 创建自己的文件reduxDemo.jsreduxToolkitDemo.js分别用来存放Redux,Redux-Toolkit的案例代码,并且在这两个文件中分别导入Redux,RTK
javascript 复制代码
// reduxDemo.js文件顶部
import Redux from "./redux.js"

// reduxToolkitDemo.js文件顶部
// 之前的默认导出可以自定义名字
import ReduxToolkit from "./redux-toolkit.umd.js"
  1. 创建index.html文件并引入我们自己的js文件,在引入js时需要加上type="module",这样浏览器才会自动解析import,现在的主流浏览器都支持es6模块化了,所以我们这里直接用就好不用打包也能在浏览器运行import。
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Redux Study Page</title>
    <script type="module" src="./reduxDemo.js"></script>
    <script type="module" src="./reduxToolkitDemo.js"></script>
</head>
<body>
    <P>This is Redux Study Page</P>
</body>
</html>
  1. 至此我们就做好了所有准备工作,下面是文件夹结构

Redux使用详解

Redux核心API简介

createStore:创建一个包含程序完整 state 树的 Redux store 。 应用中应有且仅有一个 store。
store:由createStore所创建,包含getState,dispatch,subscribe三个核心方法
combineReducers:作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。
applyMiddleware:Middleware 最常见的使用场景是无需引用大量代码或依赖类似 Rx 的第三方库实现异步 actions。
bindActionCreators:把一个 value 为不同 action creator 的对象,转成拥有同名 key 的对象。同时使用 dispatch 对每个 action creator 进行包装,以便可以直接调用它们。
compose:这是函数式编程中的方法,为了方便,被放到了 Redux 里。

计数器案例,添加获取dom元素

index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Redux Study Page</title>
        <script type="module" src="./reduxDemo.js"></script>
        <script type="module" src="./reduxToolkitDemo.js"></script>
    </head>
    <body>
        <P>This is Redux Study Page</P>
        <span class="show-count"></span>
        <button class="add-count">AddCount</button>
        <button class="minus-count">minusCount</button>
    </body>
</html>

reduxDemo.js

javascript 复制代码
const showCount = document.getElementsByClassName('show-count')
const addCount = document.getElementsByClassName('add-count')
const addNumber = document.getElementsByClassName('add-number')

实现计数器的reducer

我们在reducer传入两个参数,state和action,state初始值是0,action中包含type和payload,type决定操作类型,payload是计算需要的数据

javascript 复制代码
const countReducer = (state = 0, action) => {
    const { type, payload } = action
    switch (type) {
        case 'incrementCount':
            return state + payload
        case 'decrementCount':
            return state - payload
        default:
            return state
    }
}

特别注意:在 Redux 中,我们的 reducer 永远不允许改变原始/当前状态值!例如当我们需要修改一个对象状态时,不可以使用state.value = 123这种形式,需要返回一个新的对象,写法为{ ...state, value: 123 },例如下面案例。修改用户信息的reducer

javascript 复制代码
const userInfoReducer = (state = {name: '张三', age: 18}, action) => {
    const { type, payload } = action
    switch (type) {
        case 'changeUserName':
            return {...state, name: payload}
        case 'changeUserAge':
            return {...state, age: payload}
        default:
            return state
    }
}

使用createStore创建store

我们直接调用Redux的createStore方法,并将countReducer传入,此时我们就创建了一个状态容器store

javascript 复制代码
const store = Redux.createStore(countReducer)

打印store将出现看到如下方法

其中核心方法就是dispatch,subscribe,getState,我们将其取出方便后续使用

javascript 复制代码
const { dispatch, getState, subscribe } = store

创建回调函数,并使用subscribe注册

我们创建一个回调函数assignCount在回调函数中使用getState()获取count的值,并且传入show-count元素中,定义后直接执行一次是为了初始化页面值。

我们使用subscribe函数注册回调函数,注册后每当store中的state发生改变,就会调用 该回调函数,从而触发更新

javascript 复制代码
const assignCount = () => {
    showCount[0].innerHTML = getState()
}
assignCount()
subscribe(assignCount)

使用dispatch触发状态更新

我们分别给addCount和minusCount两个按钮元素绑定click事件,当被点击时触发dispatch函数,dispatch函数传入一个对象参数,该参数必须是对象并且必须包含type字段,type字段就是指定的操作类型,之后这个对象参数就会当作reducer函数的第二个参数action传入reducer函数中

dispatch分发的action传入reducer后,reducer会返回一个新的状态,并且保存下来,新状态产生后,redux便会循环执行订阅列表中的所有回调函数,在回调函数中调用getState方法就能返回一个最新的状态值

javascript 复制代码
addCount[0].addEventListener('click', () => {
    dispatch({ type: 'incrementCount', payload: 3 })
})
minusCount[0].addEventListener('click', () => {
    dispatch({ type: 'decrementCount', payload: 2 })
})

至此我们就创建了一个redux简单使用的小案例,页面如图所示,当点击AddCount按钮时,数字会加3,当点击minusCount按钮时,数字会减2

使用combineReducers组合所有reducer

我们知道在我们项目中所需要的全局状态一定不只有一个,而我们的store一个项目中只能有一个,这是redux的单一数据源的核心思想,那我们要想一个store保存多个state,就需要将所有的state整合,也就是需要将所有的reducer联合成一个总的reducer

我们联合所有的reducer将用的redux核心api之一的combineReducers,这个函数接收一个对象参数,对象中的key就是将来store中保存状态的key,可以自定义名字。而value就是自定义名字的state对应的reducer。

例如我们再创建一个用户信息的全局状态,并创建它的reducer

javascript 复制代码
const userInfoReducer = (state = {name: '张三', age: 18}, action) => {
    const { type, payload } = action
    switch (type) {
        case 'changeUserName':
            return {...state, name: payload}
        case 'changeUserAge':
            return {...state, age: payload}
        default:
            return state
    }
}

我们使用combineReducers将countReducer和userInfoReducer联合成一个rootReducer,然后我们再将rootReducer传入creatStore,创建唯一数据源store

javascript 复制代码
const rootReducer = Redux.combineReducers({
    count: countReducer,
    userInfo: userInfoReducer,
})
const store = Redux.createStore(rootReducer)

console.log('初始状态:', store.getState())

此时我们打印store的getState()方法,我们将获得如下打印结果

此时我们就获得了一个整合的store对象,里面包含了所有的状态,我们只需要用getState().属性名或者对象解构的方式取出我们需要用的state就可以了。

剩下的使用dispatch派发action就和单个state时完全一样,没什么区别了

使用Action Creators

Action Creators定义:Action creators是一个函数,用于创建一个action对象。在Redux中,action是一个包含type属性的普通JavaScript对象,用于描述某个事件的发生。它会告诉Redux中的reducer应该如何处理这个事件,进而更新应用程序的状态。

我们可以看一下我们之前派发action的写法是dispatch({ type: 'incrementCount', payload: 3 }),这种写法是直接在dispatch函数中写入action对象,而我们这里用到的Action creators就是创建一个函数,该函数会返回一个action对象,我们使用Action creators返回的action对象传入dispatch进行分发action。例如上述案例中的分发count的action就可以写成如下形式

javascript 复制代码
const incrementCount = (payload) => {
    return { type: 'incrementCount', payload }
}
const decrementCount = (payload) => {
    return { type: 'decrementCount', payload }
}

dispatch(incrementCount(3))
dispatch(decrementCount(2))

为什么要使用Action creators呢?Action creators有以下几点优势

  1. 抽象复杂性: 在复杂的应用中,可能有多个组件需要触发相同类型的action。通过使用action creators,我们可以将创建action的逻辑抽象出来,避免在多个地方重复定义action的结构。
  2. 可读性和维护性: 使用action creators可以提高代码的可读性和维护性。通过给action一个描述性的名字,并将其逻辑封装在单独的函数中,可以更清晰地了解应用程序中发生的事件。
  3. 测试性: 使用action creators可以方便地对action进行单元测试。你可以简单地调用action creator并检查返回的action是否符合预期。
  4. 中间件支持: Redux中的中间件可以在action被dispatch之前对其进行处理。通过使用action creators,我们可以方便地在中间件中对action进行额外的处理,比如添加异步支持或日志记录等功能。
  5. 代码组织: 使用action creators可以帮助更好地组织代码,将相关的action创建逻辑放在一起,提高代码的可维护性和可扩展性。

总结来说,使用action creators能够提供更好的抽象性、可读性、可测试性和代码组织性,使得应用程序的状态管理更加灵活和清晰。在实际开发中,推荐始终使用action creators来创建action,而不是直接在组件中创建action对象。这也是redux官方推荐的redux标准写法

使用bindActionCreators

上面我们说了,可以使用Action creators函数来返回一个action对象以封装action对象,然而当我们需要改变状态的时候,还是需要调用dispatch函数进行派发action。

那我们就想和既然已经通过Action creators获取到需要派发的action了,接下来只需要调用dispatch将其传入就可以。那我们是不是再进行封装,当我们调用Action creators时就直接调用dispatch进行派发呢

因此我们引入了bindActionCreators函数,它将action creators绑定到dispatch函数的实用函数。它的作用是简化在React应用中将action creators和dispatch函数关联起来的过程。

还以上面两个Action creators为例,bindActionCreators用法如下

javascript 复制代码
const incrementCount = (payload) => {
    return { type: 'incrementCount', payload }
}
const decrementCount = (payload) => {
    return { type: 'decrementCount', payload }
}

const boundCountActions = Redux.bindActionCreators({ incrementCount, decrementCount }, dispatch)

boundCountActions.incrementCount(3)
boundCountActions.decrementCount(2)

bindActionCreators函数传入两个参数,第一个参数是你需要绑定的Action creators函数集合的对象,其中对象的key可以自定义,不过我们一般就使用函数名本身,这样可以简写,value就是你定义的Action creators函数。第二个参数就是我们创建出的store中的dispatch函数

因此我们想要增加计数时就可以写成boundCountActions.incrementCount(3)

使用applyMiddleware

applyMiddleware是Redux提供的一个中间件函数,用于增强Redux store的dispatch函数。它的作用是在dispatch函数被调用时,对action进行预处理、日志记录、异步操作等等。

Redux中间件允许你在action被派发(dispatch)之后,到达reducer之前,对action进行一些额外的操作。这使得在Redux应用中使用异步代码、日志记录、错误处理等功能变得更加方便和可控。

使用第三方middleware

有很多第三方的middleware我们可以下载下来直接用,例如redux-thunk是一个用于处理异步操作的常用中间件。redux-logger用于记录每次dispatch的action和state,redux-promise用于处理返回Promise的action等等。我们以redux-thunk为例介绍一下第三方中间件的用法。首先我们一样需要下载打包后的redux-thunk.js,方法与开头讲的一样。让后将全局对象ReduxThunk默认导出,并在reduxDemo.js导入
redux-thunk.js作用是可以使action creator返回一个函数,返回的函数中接收两个参数,分别是dispatch, getState,这里的dispatch, getState就是store中的两个函数。在返回的函数中可以使用getState获取之前的state值,并且可以在返回的函数中使用异步操作,例如如下我们把修改用户姓名的action模拟请求延迟一秒,然后调用dispatch方法,最终可实现异步设置state

ReduxThunk的使用直接在creatStore中的第二个参数里,使用applyMiddleware返回,后续使用action creator就和之前一样,可以直接使用或者使用bindActionCreators简化使用

javascript 复制代码
// 引入redux和redux-thunk
import Redux from "./redux.js"
import ReduxThunk from "./redux-thunk.js"


const changeUserNameAction = (payload) => {
    return (dispatch, getState) => {
        setTimeout(() => {
            dispatch({ type: 'changeUserName', payload })
        }, 1000)
    }
}

const store = Redux.createStore(rootReducer, Redux.applyMiddleware(ReduxThunk))

// 派发changeUserNameAction

dispatch(changeUserNameAction('李四'))
// 或者
const boundActionsCreators = Redux.bindActionCreators({
    changeUserNameAction
}, dispatch)
boundActionsCreators.changeUserNameAction('李四')

使用自定义middleware

自定义middleware在写法上有固定写法,首先我们自定义的中间件接收一个参数store,该参数就是我们使用createStore创建出的store。然后中间件函数然会一个函数,返回的函数也接收一个参数next,该参数也是个函数。在中间件函数返回的函数中还需要再返回一个函数,也接收一个参数action,该action就是我们每次使用dispatch派发传入的action,示例如下

javascript 复制代码
const myMiddleware = (store) => {
    return (next) => {
        return (action) => {
            // 自定义逻辑
        }
    }
}

上述写法我们也可以简写成如下形式

javascript 复制代码
const myMiddleware = (store) => (next) => (action) => {
    // 自定义逻辑
}

下面我们实现一个自定义日志打印中间件,并在代码中使用。需要注意的是,在自定义中间件中一定要调用next函数并将action传入,这样是为了避免在其他中间件函数中修改了action,我们需要拿到最新的action再返回
applyMiddleware可以接收多个参数,每个参数都是一个中间件,不管是自定义还是第三方中间件都需要传入applyMiddleware获取返回值

javascript 复制代码
// 自定义中间件
const loggerMiddleware = (store) => (next) => (action) => {
    console.log('dispatching', action)
    const result = next(action)
    console.log('next state', store.getState())
    return result
}

// 使用中间件
const store = Redux.createStore(rootReducer, Redux.applyMiddleware(ReduxThunk, loggerMiddleware))

此时我们再派发任何action都会执行我们自定义的logger中间件,打印出下面内容

完整测试代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Redux Study Page</title>
    <script type="module" src="./reduxDemo.js"></script>
<!--     <script type="module" src="./reduxToolkitDemo.js"></script> -->
</head>
<body>
    <P>This is Redux Study Page</P>
    <span class="show-count"></span>
    <br>
    <span class="show-user-info"></span>
    <br>
    <button class="add-count">AddCount</button>
    <button class="minus-count">minusCount</button>
    <button class="change-user-name">ChangeUserName</button>
    <button class="change-user-age">ChangeUserAge</button>
</body>
</html>
javascript 复制代码
import Redux from "./redux.js"
import ReduxThunk from "./redux-thunk.js"
const showCount = document.getElementsByClassName('show-count')
const addCount = document.getElementsByClassName('add-count')
const minusCount = document.getElementsByClassName('minus-count')
const showUserInfo = document.getElementsByClassName('show-user-info')
const changeUserName = document.getElementsByClassName('change-user-name')
const changeUserAge = document.getElementsByClassName('change-user-age')

const incrementCount = (payload) => {
    return { type: 'incrementCount', payload }
}
const decrementCount = (payload) => {
    return { type: 'decrementCount', payload }
}

const countReducer = (state = 0, action) => {
    const { type, payload } = action
    switch (type) {
        case 'incrementCount':
            return state + payload
        case 'decrementCount':
            return state - payload
        default:
            return state
    }
}

const changeUserNameAction = (payload) => {
    return (dispatch, getState) => {
        setTimeout(() => {
            dispatch({ type: 'changeUserName', payload })
        }, 1000)
    }
}
const changeUserAgeAction = (payload) => {
    return { type: 'changeUserAge', payload }
}

const userInfoReducer = (state = {name: '张三', age: 18}, action) => {
    const { type, payload } = action
    switch (type) {
        case 'changeUserName':
            return {...state, name: payload}
        case 'changeUserAge':
            return {...state, age: payload}
        default:
            return state
    }
}

const rootReducer = Redux.combineReducers({
    count: countReducer,
    userInfo: userInfoReducer,
})

const loggerMiddleware = (store) => (next) => (action) => {
    console.log('dispatching', action)
    const result = next(action)
    console.log('next state', store.getState())
    return result
}

const store = Redux.createStore(rootReducer, Redux.applyMiddleware(ReduxThunk, loggerMiddleware))

const { dispatch, getState, subscribe} = store

const boundActionsCreators = Redux.bindActionCreators({ 
    incrementCount,
    decrementCount,
    changeUserNameAction,
    changeUserAgeAction
}, dispatch)

const assignCount = () => {
    showCount[0].innerHTML = getState().count
}
const assignUserInfo = () => {
    const userInfo = getState().userInfo
    showUserInfo[0].innerHTML = `姓名:${userInfo.name},年龄:${userInfo.age}`
}

assignCount()
assignUserInfo()

subscribe(assignCount)
subscribe(assignUserInfo)

addCount[0].addEventListener('click', () => {
    boundActionsCreators.incrementCount(3)
})

minusCount[0].addEventListener('click', () => {
    boundActionsCreators.decrementCount(2)
})

changeUserName[0].addEventListener('click', () => {
    boundActionsCreators.changeUserNameAction('李四')
})

changeUserAge[0].addEventListener('click', () => {
    boundActionsCreators.changeUserAgeAction(20)
})

Redux-Toolkit使用详解

什么是Redux-Toolkit?

Redux Toolkit是一个用于简化Redux状态管理库的官方工具集。它旨在帮助开发者更快速、更简洁地编写和维护Redux应用程序。Redux是一个非常强大的状态管理库,但在处理一些常见的任务时,开发者可能需要编写大量的模板代码。Redux Toolkit的目标就是通过提供一组工具函数和约定,来减少这些冗余的代码,同时促进最佳实践。因此我们使用Redux-Toolkit可以让我们的redux代码变得更加清晰,简洁。

Redux-Toolkit核心API简介

configureStore()封装了createStore,简化配置项,提供一些现成的默认配置项。它可以自动组合 slice 的 reducer,可以添加任何 Redux 中间件,默认情况下包含 redux-thunk,并开启了 Redux DevTools 扩展。
createReducer() 帮你将 action type 映射到 reducer 函数,而不是编写 switch...case 语句。另外,它会自动使用immer库来让你使用普通的 mutable 代码编写更简单的 immutable 更新,例如 state.todos[3].completed = true。
createAction() 生成给定 action type 字符串的 action creator 函数。该函数本身已定义了 toString(),因此可以代替常量类型使用。
createSlice() 接收一组 reducer 函数的对象,一个 slice 切片名和初始状态 initial state,并自动生成具有相应 action creator 和 action type 的 slice reducer。
createAsyncThunk接收一个 action type 字符串和一个返回值为 promise 的函数, 并生成一个 thunk 函数,这个 thunk 函数可以基于之前那个 promise ,dispatch 一组 type 为 pending/fulfilled/rejected 的 action。

使用configureStore

configureStore封装了createStore,简化了配置项,提供一些现成的默认配置项。我们这里看一下configureStore最基本的用法,后续还会继续扩展。

我们这里还没有说到createSlice这个方法,因此先使用之前redux中创建reducer的方式测试,configureStore的基本使用如下
configureStore接受一个对象作为参数,对象中可以传入一个reducer属性,reducer属性就是所有reducer的集合,这里写法简化了使用combineReducers来整合所有reducer

我们打印出的store和使用createStore创建出的store结果一样,同样包含了getState,dispatch,subscribe三个核心方法

javascript 复制代码
import ReduxToolkit from "./redux-toolkit.umd.js"

const countReducer = (state = 0, action) => {
    const { type, payload } = action
    switch (type) {
        case 'incrementCount':
            return state + payload
        case 'decrementCount':
            return state - payload
        default:
            return state
    }
}

// 使用Redux Toolkit的configureStore方法创建store
const store = ReduxToolkit.configureStore({
    reducer: {
        counter: countReducer
    },
})

console.log(store)

使用createAction

createAction 的主要作用是帮助开发者更轻松地创建 Redux 中的 action 创建函数,从而简化代码编写和管理。它提供了一种声明性的方式来定义 action 类型以及创建与该类型相关的 action 对象。

在之前我们自定义action creator函数来封装action,在Redux Toolkit我们可以使用createAction更方便的创建action函数,例如我们创建计数器增减的action函数,我们执行该action函数并打印,我们将得到两个action对象,对象中默认包含type和payload属性,type的值就是我们传入的action type字符串

javascript 复制代码
const incrementCount = ReduxToolkit.createAction('incrementCount')
const decrementCount = ReduxToolkit.createAction('decrementCount')

console.log(incrementCount(), decrementCount())

若我们给action传参,action函数返回的action对象中的payload就是我们传的参数值

javascript 复制代码
const incrementCount = ReduxToolkit.createAction('incrementCount')
const decrementCount = ReduxToolkit.createAction('decrementCount')

console.log(incrementCount(3), decrementCount(2))

若我们直接将action函数字符串化,我们可以直接得到createAction传入的字符串,这个特性可以让我们很好的和下面的createReducer配合使用

javascript 复制代码
const incrementCount = ReduxToolkit.createAction('INCREMENT_COUNT')
const decrementCount = ReduxToolkit.createAction('DECREMENT_COUNT')

console.log(incrementCount.toString(), decrementCount.toString())

使用createReducer

createReducer 的主要作用是帮助开发者更轻松地创建 Redux 中的 reducer 函数,从而简化代码编写和管理。它提供了一种声明性的方式来定义 reducer,使其更易读、易理解,同时还可以自动处理更新状态的操作。

自定义reducer函数和使用createReducer对比

我们传统定义reducer的方式是自定义的写法,这种写法有以下几点不足:

  1. 不可变性处理: 传统自定义 reducer 需要手动处理状态的不可变性,可能导致编写更多繁琐的代码,增加了出错的机会。例如我们修改对象时,不能直接修改state的属性值,而是需要重新返回一个新的对象。
  2. 样板代码多: 传统 reducer 编写需要更多的样板代码,如 switch 语句、手动拷贝对象等,使代码变得冗长、难以维护。
  3. 类型安全问题: 在 TypeScript 环境中,传统 reducer 可能需要更多的类型断言或声明,不如 Redux Toolkit 那样直接支持类型推断。
  4. 难以组织: 传统 reducer 可能需要分散在多个文件中,代码的组织和管理相对困难。

使用createReducer定义reducer的优势:

  1. 减少样板代码: 使用 createReducer 可以大大减少编写 reducer 的样板代码量,使代码更加简洁、易读。
  2. 不可变性处理: createReducer 使用 Immer 库,可以在 reducer 中直接对状态进行修改,而不需要手动编写不可变性的逻辑,使状态更新更直观、便捷。
  3. 类型安全: Redux Toolkit 提供了 TypeScript 支持,使用 createReducer 创建的 reducer 可以更容易地与 TypeScript 集成,获得更好的类型检查。
  4. 易于组织: createReducer 结合 createAction 和 createSlice 可以更好地组织 action 创建函数、reducer 和状态初始值,使代码的逻辑更一致、清晰。
  5. 动态增加处理逻辑: 使用 createReducer 创建的 reducer 可以通过链式调用的方式动态地添加处理不同 action 类型的逻辑,增加了代码的可扩展性。

使用createReducer创建reducer函数

使用createReducer创建reducer函数时有两种方式,区别于第二个参数传递的值。createReducer函数接收两个参数,第一个参数为初始state的值,第二个参数可以是个对象或者一个回调函数。我们已修改用户信息为例,演示这两种用法。
注意:在createReducer中创建的reducer函数中,由于使用了Immer库,因此我们可以直接修改对象的属性值,而无需再返回一个新的对象。

  1. 使用对象形式

当我们第二个参数接收一个对象时,对象中的key为我们的action,value就是reducer函数,其中key有两种写法,直接使用字符串,或者使用createAction生成的action。

javascript 复制代码
const userInfoReducer = ReduxToolkit.createReducer({name: '张三', age: 18}, {
    "CHANGE_USER_NAME": (state, action) => state.name = action.payload,
    "CHANGE_USER_NAME": (state, action) => state.age = action.payload,
})
// 或者
const changeUserNameAction = ReduxToolkit.createAction('CHANGE_USER_NAME')
const changeUserAgeAction = ReduxToolkit.createAction('CHANGE_USER_AGE')

const userInfoReducer = ReduxToolkit.createReducer({name: '张三', age: 18}, {
    [changeUserNameAction]: (state, action) => state.name = action.payload,
    [changeUserAgeAction]: (state, action) => state.age = action.payload,
})
// 或者
const changeUserNameAction = ReduxToolkit.createAction('CHANGE_USER_NAME')
const changeUserAgeAction = ReduxToolkit.createAction('CHANGE_USER_AGE')

const userInfoReducer = ReduxToolkit.createReducer({name: '张三', age: 18}, {
    [changeUserNameAction().type]: (state, action) => state.name = action.payload,
    [changeUserAgeAction().type]: (state, action) => state.age = action.payload,
})

这三种方式设置key都可以,不过推荐使用第二种,最简单而且清晰。并且在这里我们可以看出,我们设置姓名或者年龄时没有像之前那样返回一个新的对象,而是直接给对象的属性赋值state.name = action.payload这就是createReducer带来的优势

  1. 使用回调函数形式

当我们使用上面的对象形式设置reducer函数时,控制台会有如下警告

这里的意思时,在RTK 2.0将移除对象形式,因此推荐我们使用回调函数形式。

回调函数中接收一个构建器builder,builder构建器中有三个属性addCase,addMatcher,addDefaultCase,他们分别的作用是
addCase:用于添加指定 action 类型的处理逻辑。它接受两个参数:action 类型和处理函数。处理函数会接收当前状态和 action 作为参数,并返回一个更新后的状态对象。
addMatcher:用于根据自定义的匹配函数来添加处理逻辑。它接受两个参数:匹配函数和处理函数。匹配函数会接收 action 作为参数,如果匹配成功,则调用对应的处理函数。
addDefaultCase:用于添加默认的处理逻辑,适用于没有匹配到任何已定义的 action 类型时的情况。它接受一个处理函数作为参数,处理函数会接收当前状态state和 action 作为参数,并返回一个更新后的状态对象。

javascript 复制代码
const changeUserNameAction = ReduxToolkit.createAction('CHANGE_USER_NAME')
const changeUserAgeAction = ReduxToolkit.createAction('CHANGE_USER_AGE')

const userInfoReducer = ReduxToolkit.createReducer({ name: '张三', age: 18 }, (builder) => {
    builder
        .addCase(changeUserNameAction, (state, action) => {
            state.name = action.payload
        })
        .addCase(changeUserAgeAction, (state, action) => {
            state.age = action.payload
        })
        .addMatcher((action) => action.payload === 'whg', (state, action) => {
            state.name = action.payload
        })
        .addDefaultCase((state, action) => {
            return state
        })
})

注意:在addCase中我们第一个参数可以传一个字符串也可以传一个createAction生成的action函数,若传的是action函数,则redux-toolkit内部会自动匹配该action函数返回的type属性。即如上面案例中changeUserNameAction等价于'CHANGE_USER_NAME'

使用createSlice

createSlice 是 Redux Toolkit 提供的一个函数,用于创建一个包含 reducer 和 action 创建函数的"切片"(slice)。切片是一种用于管理状态和处理状态更新的封装单元,它将相关的 reducer 和 action 创建函数组合在一起,使得状态管理变得更加模块化和易于管理。createSlice 包含下面四个属性
name:是一个string类型,用于指定该createSlice 创建出的state名字
initialState:state的初始值
reducers:reducer函数的集合,是个对象形式,key为reducer函数的函数名,value是reducer函数
extraReducers:可选参数,用于定义额外的reducer,通常搭配createAsyncThunk使用。

创建slice并取出action函数

javascript 复制代码
const userInfoSlice = ReduxToolkit.createSlice({
    name: 'userInfo',
    initialState: {name: '张三', age: 18},
    reducers: {
        changeUserNameAction(state, action) {
            state.name = action.payload
        },
        changeUserAgeAction(state, action) {
            state.age = action.payload
        }
    }
})

const { changeUserNameAction, changeUserAgeAction } = userInfoSlice.actions

console.log(userInfoSlice)

我们打印出createSlice返回的结果可以得到一个对象,对象中包含actions,caseReducers,getInitialState,name,reducer其中actions中包含的就是createSlice自动生成的action函数,action函数返回的type就是根据name和reducer函数名自动生成。reducer是我们后面需要在configureStore传入的reducer函数。

注意:在使用createSlice定义reducers时并没有定义默认执行的reducers,这是因为createSlice会自动生成一个identity reducer,用于处理未匹配的action,直接返回原state。

使用extraReducers定义reducer函数

在createSlice中定义reducer函数,除了上面在reducers里面定义之外,还可以使用extraReducers来定义,extraReducers属性的value值和createReducer函数所传的第二个参数一样,有两种形式,对象形式和回调函数形式,我们在createReducer中也介绍了,官方推荐使用回调函数形式,因此我们这里就直接使用回调函数的写法。注意:使用extraReducers定义reducer时,需要使用外部action对象或者直接用字符串

javascript 复制代码
const changeUserNameAction = ReduxToolkit.createAction('CHANGE_USER_NAME')
const changeUserAgeAction = ReduxToolkit.createAction('CHANGE_USER_AGE')

const userInfoSlice = ReduxToolkit.createSlice({
    name: 'userInfo',
    initialState: {name: '张三', age: 18},
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(changeUserNameAction, (state, action) => {
                state.name = action.payload
            })
            .addCase(changeUserAgeAction, (state, action) => {
                state.age = action.payload
            })
            .addDefaultCase((state, action) => {
                return state
            })
    }
})

使用prepare预处理action

我们定义reducer时,可以使用prepare预处理action,prepare函数接收一个参数就是action中的payload,我们可以修改payload,当调用reducer时,使用的是修改后的payload

javascript 复制代码
const userInfoSlice = ReduxToolkit.createSlice({
    name: 'userInfo',
    initialState: {name: '张三', age: 18},
    reducers: {
        changeUserNameAction(state, action) {
            state.name = action.payload
        },
        changeUserAgeAction(state, action) {
            state.age = action.payload
        },
        changeUserAgeAddTwo: {
            reducer(state, action) {
                state.age = action.payload
            },
            prepare(payload) {
                payload = payload + 2
                return { payload }
            }
        }
    }
})

const { 
    changeUserNameAction, 
    changeUserAgeAction,
    changeUserAgeAddTwo 
} = userInfoSlice.actions

dispatch(changeUserAgeAddTwo(20))
// age将被改成22

使用createAsyncThunk处理异步数据

在我们之前的redux中处理异步改变state用的是redux-thunk中间件,让我们可以返回一个异步执行的action。在redux-toolkit中封装了该方法,并生成一个新的函数createAsyncThunk用来生成异步action。
createAsyncThunk接收两个参数:

  1. 一个字符串,用作生成的 action types 的前缀
  2. 一个 payload creator 回调函数,应该返回一个 Promise。这通常使用 async/await 语法编写,因为 async 函数会自动返回一个 Promise。

我们使用setTimeout模拟一个异步请求

javascript 复制代码
// 模拟异步请求
const simulateApiRequest = (value, delay) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(value)
        }, delay)
    })
}

// 创建异步thunk action
const asyncChangeUserNameAction = ReduxToolkit.createAsyncThunk('asyncChangeUserName', async (value) => {
    const response = await simulateApiRequest(value, 1000)
    return response
})

const userInfoSlice = ReduxToolkit.createSlice({
    name: 'userInfo',
    initialState: { name: '张三', age: 18 },
    reducers: {
        changeUserNameAction(state, action) {
            state.name = action.payload
        },
        changeUserAgeAction(state, action) {
            state.age = action.payload
        },
        changeUserAgeAddTwo: {
            reducer(state, action) {
                state.age = action.payload
            },
            prepare(payload) {
                payload = payload + 2
                return { payload }
            }
        }
    },
    extraReducers: (builder) => {
        builder
            .addCase(asyncChangeUserNameAction.pending, (state, action) => {
                console.log('pending')
            })
            .addCase(asyncChangeUserNameAction.fulfilled, (state, action) => {
                console.log('fulfilled')
                state.name = action.payload
            })
            .addCase(asyncChangeUserNameAction.rejected, (state, action) => {
                console.log('rejected')
            })
    }
})

dispatch(asyncChangeUserNameAction(whg))

我们传递'asyncChangeUserName'作为字符串前缀,以及一个调用 API 并返回包含获取数据的 promise 的 payload creator 函数。在内部,createAsyncThunk 将生成三个 action creators 和 action types,以及一个在调用时自动 dispatch 这些 actions 的 thunk 函数。在这种情况下,action creators 和它们 types 是:

  1. asyncChangeUserNameAction.pending:userInfo/asyncChangeUserName/pending
  2. asyncChangeUserNameAction.fulfilled:userInfo/asyncChangeUserName/fulfilled
  3. asyncChangeUserNameAction.rejected:userInfo/asyncChangeUserName/rejected

但是,这些 action creators 和 types 是在 createSlice 调用 之外 定义的。我们无法在 createSlice.reducers 中处理它们,因为它们也会生成新的 action types。此时我们需要用上面所介绍的extraReducers来处理这些action。当我们执行下面的dispatch时,页面会延迟1秒更新,并且会打印出下面结果

configureStore中使用中间件

在configureStore中除了我们上面说的基本使用之外,还可以传入多个参数,其中使用中间件可以在configureStore中添加middleware属性,该属性值可以为数组或者一个回调函数,不管是数组还是回调函数形式,都需要结合redux-toolkit的默认中间件

javascript 复制代码
const loggerMiddleware = (store) => (next) => (action) => {
    console.log('dispatching', action)
    const result = next(action)
    console.log('next state', store.getState())
    return result
}

// 使用Redux Toolkit的configureStore方法创建store
const store = ReduxToolkit.configureStore({
    reducer: {
        count: countReducer,
        userInfo: userInfoSlice.reducer
    },
    middleware: [loggerMiddleware, ...ReduxToolkit.getDefaultMiddleware()]
})

完整测试代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Redux Study Page</title>
    <!-- <script type="module" src="./reduxDemo.js"></script> -->
    <script type="module" src="./reduxToolkitDemo.js"></script>
</head>
<body>
    <P>This is Redux Study Page</P>
    <span class="show-count"></span>
    <br>
    <span class="show-user-info"></span>
    <br>
    <button class="add-count">AddCount</button>
    <button class="minus-count">minusCount</button>
    <button class="change-user-name">ChangeUserName</button>
    <button class="change-user-age">ChangeUserAge</button>
    <br>
    <button class="async-change-user-name">AsyncChangeUserName</button>
</body>
</html>
javascript 复制代码
import ReduxToolkit from "./redux-toolkit.umd.js"

const showCount = document.getElementsByClassName('show-count')
const addCount = document.getElementsByClassName('add-count')
const minusCount = document.getElementsByClassName('minus-count')
const showUserInfo = document.getElementsByClassName('show-user-info')
const changeUserName = document.getElementsByClassName('change-user-name')
const changeUserAge = document.getElementsByClassName('change-user-age')
const asyncChangeUserName = document.getElementsByClassName('async-change-user-name')

const incrementCount = ReduxToolkit.createAction('INCREMENT_COUNT')
const decrementCount = ReduxToolkit.createAction('DECREMENT_COUNT')

const countReducer = ReduxToolkit.createReducer(0, (builder) => {
    builder
        .addCase(incrementCount, (state, action) => {
            return state + action.payload
        })
        .addCase(decrementCount, (state, action) => {
            return state - action.payload
        })
        .addMatcher((action) => action.payload === 100, (state, action) => {
            return state + action.payload
        })
        .addDefaultCase((state, action) => {
            return state
        })
})

// 模拟异步请求
const simulateApiRequest = (value, delay) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(value)
        }, delay)
    })
}

// 创建异步thunk action
const asyncChangeUserNameAction = ReduxToolkit.createAsyncThunk('asyncChangeUserName', async (value) => {
    const response = await simulateApiRequest(value, 1000)
    return response
})

const userInfoSlice = ReduxToolkit.createSlice({
    name: 'userInfo',
    initialState: { name: '张三', age: 18 },
    reducers: {
        changeUserNameAction(state, action) {
            state.name = action.payload
        },
        changeUserAgeAction(state, action) {
            state.age = action.payload
        },
        changeUserAgeAddTwo: {
            reducer(state, action) {
                state.age = action.payload
            },
            prepare(payload) {
                payload = payload + 2
                return { payload }
            }
        }
    },
    extraReducers: (builder) => {
        builder
            .addCase(asyncChangeUserNameAction.pending, (state, action) => {
                console.log('pending')
            })
            .addCase(asyncChangeUserNameAction.fulfilled, (state, action) => {
                console.log('fulfilled')
                state.name = action.payload
            })
            .addCase(asyncChangeUserNameAction.rejected, (state, action) => {
                console.log('rejected')
            })
    }
})

const { changeUserNameAction, changeUserAgeAction, changeUserAgeAddTwo } = userInfoSlice.actions

const loggerMiddleware = (store) => (next) => (action) => {
    console.log('dispatching', action)
    const result = next(action)
    console.log('next state', store.getState())
    return result
}

// 使用Redux Toolkit的configureStore方法创建store
const store = ReduxToolkit.configureStore({
    reducer: {
        count: countReducer,
        userInfo: userInfoSlice.reducer
    },
    middleware: [loggerMiddleware, ...ReduxToolkit.getDefaultMiddleware()]
})

const { dispatch, getState, subscribe } = store

const assignCount = () => {
    showCount[0].innerHTML = getState().count
}
const assignUserInfo = () => {
    const userInfo = getState().userInfo
    showUserInfo[0].innerHTML = `姓名:${userInfo.name},年龄:${userInfo.age}`
}

assignCount()
assignUserInfo()

subscribe(assignCount)
subscribe(assignUserInfo)

const boundActionsCreators = ReduxToolkit.bindActionCreators({
    incrementCount,
    decrementCount,
    changeUserNameAction,
    changeUserAgeAction,
    changeUserAgeAddTwo,
    asyncChangeUserNameAction
}, dispatch)

addCount[0].addEventListener('click', () => {
    boundActionsCreators.incrementCount(3)
})

minusCount[0].addEventListener('click', () => {
    boundActionsCreators.decrementCount(2)
})

changeUserName[0].addEventListener('click', () => {
    boundActionsCreators.changeUserNameAction('李四')
})

changeUserAge[0].addEventListener('click', () => {
    boundActionsCreators.changeUserAgeAddTwo(20)
})

asyncChangeUserName[0].addEventListener('click', () => {
    boundActionsCreators.asyncChangeUserNameAction('王五')
})

总结

这里是redux和redux-toolkit的原生用法,后续还有结合react项目的综合用法,点赞关注不迷路。

Github地址:Github

相关推荐
若川38 分钟前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
IT女孩儿1 小时前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
@解忧杂货铺5 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
真的很上进10 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
噢,我明白了14 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__14 小时前
APIs-day2
javascript·css·css3
关你西红柿子14 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根14 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.15 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析
Aphasia31115 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试