阅读 597

浅谈redux、applyMiddleware、redux-thunk

redux是一个可预测的状态管理工具,唯一可以改变state的方式就是dispatch一个action,action描述了以何种方式改变state,交由reducer去改变state。

创建redux应用

npm i redux
复制代码

src/index.js:

import {createStore} from 'redux'

const initState = {
    list: []
}

//  reducer,createStore的第一个参数,当store初始化的时候redux会调用reducer,传入state为undefined,action.type为一个@@redux/INIT开头的随机字符串
//  所以在这里可以设置state的默认值,防止下次reducer改变数据的时候报错
//  reducer应当返回一个state,来作为新的state。
function todo(state = initState, action){
    switch(action.type){
        case 'todoAdd':
            return {
                list: state.list.concat(action.text)
            }
        case 'todoRemove':
            return {
                list: state.list.filter((v) => v !== action.text)
            }
        default: 
            return state
    }
}

let store = createStore(todo)

//订阅store更新
store.subscribe(() => {
    console.log(store.getState())
})

//派发action,这个action会被传入到reducer的第二个参数
store.dispatch({
    type: 'todoAdd',
    text: '吃饭',
})
store.dispatch({
    type: 'todoAdd',
    text: '睡觉',
})
store.dispatch({
    type: 'todoAdd',
    text: '打豆豆',
})
store.dispatch({
    type: 'todoRemove',
    text: '睡觉',
})
复制代码

控制台打印结果为: 打印结果

合并reducer

假如说有多个reducer,一个是todo,另外一个是用户数据,我们可以使用redux提供的combineReducers来合并reducer,src下新建一个文件夹为store,src/store/index.js为创建的store,src/store/todo.js和src/store/user.js分别为todo的reducer和user的reducer。 src/store/index.js代码为:

import {createStore, combineReducers} from 'redux'
import todo from './todo'
import user from './user'

const reducer = combineReducers({
    todo,
    user,
})

const store = createStore(reducer)

export default store
复制代码

src/store/user.js代码为

const initState = {
    name: 'xiaobai',
    age: 18,
}

function user(state = initState, action){
    switch(action.type){
        case 'userAgeAdd':
            return {
                ...state,
                age: state.age + 1,
            }
        case 'userNameChange':
            return {
                ...state,
                name: action.name,
            }
        default: 
            return state
    }
}

export default user
复制代码

现在在src/index.js里面增加一段代码

store.dispatch({
    type: 'userNameChange',
    name: 'xiaohei',
})
复制代码

打印结果 打印结果

创建action生成函数

上面的写法dispatch每次都要写一个action,可是试想一下如果我们封装成一个函数来返回一个action的话会更方便一点,就以user这个reducer开始封装生成action的函数。 src/store/user.js增加两个函数

export function userNameChange(name){
    return {
        type: 'userNameChange',
        name,
    }
}
export function userAgeAdd(){
    return {
        type: 'userAgeAdd',
    }
}
复制代码

src/index.js修改为:

import store from './store'
import {userNameChange} from './store/user'

//订阅store更新
store.subscribe(() => {
    console.log(store.getState())
})

store.dispatch(userNameChange('xiaohei'))
复制代码

打印结果看到user.name已经被修改为'xiaohei'

创建异步action生成函数

reducer是一个纯函数,不应该修改传入的参数,不应该有执行有副作用的API 请求和路由跳转,不能调用非纯函数。只要传入参数相同,返回计算得到的下一个 state 就一定相同,单纯执行计算。 那么怎么执行异步操作呢,这时候就要用到一个插件redux-thunk。通过使用redux提供的applyMiddleware,action创建函数除了返回 action 对象外还可以返回函数,当返回函数时,这个函数会被执行,接收一个参数为dispatch。这个函数并不需要保持纯净。

npm i redux-thunk
复制代码

继续修改src/store/user.js导出的userNameChange

export function userNameChange(name){
    return (dispatch) => {  //返回的函数会被执行,并被传入dispatch
        setTimeout(() => {  //模拟api请求
            console.log('一秒后dispatch一个action')
            dispatch({
                type: 'userNameChange',
                name,
            })
        },2000)
    }
}
复制代码

/src/store/index.js

import {createStore, combineReducers, applyMiddleware} from 'redux'
import reduxThunk from 'redux-thunk'
import todo from './todo'
import user from './user'

const reducer = combineReducers({
    todo,
    user,
})

const store = createStore(reducer, applyMiddleware(
    reduxThunk
))

export default store
复制代码

现在打开控制台刷新页面,一秒之后打印结果正常,说明我们已经做好了action的异步操作。 redux-thunk并不是redux处理异步操作唯一的解决方式,当你读完下一章节你也可以写一个自定义的middleware

Middleware分析

middleware 是指可以被嵌入在框架接收请求到产生响应过程之中的代码。例如,Express 或者 Koa 的 middleware 可以完成添加 CORS headers、记录日志、内容压缩等工作。middleware 最优秀的特性就是可以被链式组合。你可以在一个项目中使用多个独立的第三方 middleware。 Redux middleware 它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。

手动记录日志

假如我们没有redux提供的applyMiddleware,如果想记录redux日志的话,可能会需要这样写来手动记录。 src/store/user.js修改为原来的userNameChange,src/store/index.js删除middleware。 src/index.js

import store from './store' 
import {userNameChange} from './store/user'


let action = userNameChange('xiaohei')
console.log('dispatch', action.type)
store.dispatch(action)
console.log('newState', store.getState())
复制代码

重写dispatch

上面的方法虽然可以实现记录日志的功能,但是需要每次dispatch都需要记录。既然改变数据就一定会用dispatch,我们可以尝试重写dispatch,在保留原来dispatch完整功能的情况下,增加一些我们自己需要做的操作。 src/index.js

import store from './store'
import {userNameChange} from './store/user'

let next = store.dispatch   //保存原来的dispatch完整功能

store.dispatch = (action) => {      //重写dispatch,接收一个action
    console.log('dispatch', action.type)
    let result = next(action)       //执行原来的dispatch功能
    console.log('newState', store.getState())
    return result
}

let action = userNameChange('xiaohei')
store.dispatch(action)
复制代码

现在打开控制台,不管在哪里dispatch,都已经可以正常的记录日志了,

新增middleware

实际开发中捕获异常也是很重要的,现在如果需要新增功能的话,在原来重写的dispatch上面写新功能会让代码看起来很乱,我们完全可以写成两个独立的功能。 src/index.js

import store from './store'
import {userNameChange} from './store/user'

const logMiddleware = (store) => {
    let next = store.dispatch
    store.dispatch = (action) => {
        console.log('dispatch', action.type)
        let result = next(action)
        console.log('newState', store.getState())
        return result
    }
}

const errMiddlware = (store) => {
    let next = store.dispatch
    store.dispatch = (action) => {
        try {
            return next(action)
        }catch(err){
            console.log('redux抛出异常')
            throw err
        }
    }
}

logMiddleware(store)
errMiddlware(store)

let action = userNameChange('xiaohei')
store.dispatch(action)
//为了试验异常捕获,dispatch不传参数。
store.dispatch()
复制代码

现在打开控制台打印结果为打印结果 不出所料,两个中间件功能全部实现了,整个代码运行流程为:执行logMiddleware,传入createStore生成的store,logMiddleware对store的dispatch方法进行重写;执行errMiddlware,传入dispatch方法已经被logMiddleware方法重写过的store,errMiddlware在保留原来被处理过的完整的dispatch方法之上,继续添加新的功能。

applyMiddleware源码

通过研究redux提供的applyMiddleware源码

function applyMiddleware(...middlewares) {
	//createStore会判断如果执行applyMiddleware返回函数,创建store的工作就交由下面的代码来执行
	//返回一个处理过dispatch的store,现在的...args为我们传入的reducer。
	//createStore的代码为 enhancer(createStore)(reducer, preloadedState)
	return (createStore) => (...args) => {
		const store = createStore(...args)
		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)
		}
		const chain = middlewares.map(middleware => middleware(middlewareAPI)),	//接收next,返回dispatch的函数组成的数组。
		dispatch = compose(...chain)(store.dispatch)	//原始dispatch传入compose生成的函数被链式处理。
		return {
			...store,
			dispatch	//被处理过的dispatch
		}
	}
}
复制代码

下面为compose:

export default function compose(...funcs) {
	 if (funcs.length === 0) {
	   	return arg => arg
	 }
	if (funcs.length === 1) {
		return funcs[0]
	}
	return funcs.reduce((a, b) => {
		return (...args) => a(b(...args))
	})
}
//compose 的作用是:传入一组任意数量的函数,比如 funcA, funcB,funcC,
//可生成一个新的函数 (...args) => funcA(funcB(funcC(...args)))
//它的含义是每个函数均以上一个函数的返回值为参数传入,并将自己计算得到的返回值作为下一个函数的参数。
复制代码

通过以上源码不难发现,其实middleware就是一个接收middlewareAPI的函数,返回的函数接收一个参数next,返回一个函数作为下一个中间件的next。

使用applyMiddleware

先改写logMiddleware和errMiddlware src/store/index.js

import {createStore, combineReducers, applyMiddleware} from 'redux'
import reduxThunk from 'redux-thunk'
import todo from './todo'
import user from './user'

const reducer = combineReducers({
    todo,
    user,
})

const logMiddleware = (store) => {  //store为applyMiddleware传出的middlewareAPI
    return (next) => {              //返会函数接收next,执行返回dispatch作为下一个middleware的next参数
        return (action) => {        //dispatch
            console.log('dispatch', action.type)
            let result = next(action)
            console.log('newState', store.getState())
            return result
        }
    }
}

const errMiddlware = (store) => {   //middlewareAPI
    return (next) => {              //logMiddleware返回的dispatch
        return (action) => {        //返回dispatch,作为下一个middleware的next
            try {
                return next(action)
            }catch(err){
                console.log('redux抛出异常')
                throw err
            }
        }
    }
}

const store = createStore(reducer, applyMiddleware(
    //middlewares
    logMiddleware,
    errMiddlware,
))

export default store
复制代码

src/index.js删除对dispatch的处理。

import store from './store'
import {userNameChange} from './store/user'

store.dispatch(userNameChange('xiaohei'))
//为了试验异常捕获,dispatch不传参数。
store.dispatch()
复制代码

现在打开控制台,既打印了redux日志,也有异常捕获,说明我们middleware写法是正确的,现在对他们进行柯里化。

const logMiddleware = (store) => (next) => (action) => {
    console.log('dispatch', action.type)
    let result = next(action)
    console.log('newState', store.getState())
    return result
}

const errMiddlware = (store) => (next) => (action) => {
    try {
        return next(action)
    }catch(err){
        console.log('redux抛出异常')
        throw err
    }
}
复制代码

异步action

上面以优雅的写法增加了logMiddleware和errMiddleware,现在还不能支持异步action的写法,继续增加一个middleware让action可以返回一个函数,函数处理异步操作最终dispatch一个action来改变数据。 src/store/user.js增加一个异步action

export function userAgeAddSync(){
    return (dispatch) => {	//接收dispatch用来异步操作完成后的派发动作
        setTimeout(() => {
            dispatch(userAgeAdd())
        }, 1000)
    }
}
复制代码

刷新页面发现redux抛出异常打印结果 因为执行userAgeAddSync得到的是一个接收dispatch的函数,action.type为undefined的原因也是如此,因此需要增加一个中间件来处理这个异步action。 src/store/index.js增加一个middleware并应用到applyMiddleware中

const syncMiddlware = (store) => (next) => (action) => {
    if(typeof action === 'function'){       //如果action是一个函数,就直接执行这个函数,传入dispatch
        action(store.dispatch)
    }else{
        return next(action)
    }
}
复制代码

现在来看打印结果 打印结果 现在一秒之后派发userAgeAdd我们已经做到了,但是中间有一个步骤打印action.type为undefined,如果你对middleware链式调用理解的还不错的话,你已经知道什么原因了。因为在我们执行store.dispatch(userAgeAddSync())的时候,userAgeAddSync()返回的函数被logMiddleware处理,打印action.type一定会是undefined,现在只需要在logMiddleware里面座一层判断,如果action为函数的话,直接执行action,传入dispatch,如果不是的话执行原来的记录日志逻辑。 修改logMiddleware代码为

const logMiddleware = (store) => (next) => (action) => {
    let result
    if(typeof action === 'function'){
        action(store.dispatch)
    }else{
        console.log('dispatch', action.type)
        result = next(action)
        console.log('newState', store.getState())
        return result
    }
}
复制代码

现在打开控制台,页面加载完毕只有userNameChange的日志,一秒后打印userAgeAdd的日志,这正是我们想要的结果。后期如果有时间的话我会基于这套代码进行封装,应用到React上面。本篇博客代码和所有的章节提交记录我放在了Gitee上面,链接链接: Gitee,有兴趣的同学可以下载研究。

文章分类
前端
文章标签