前言
- 接上篇,这篇实现onReducer钩子。
利用onReducer实现redux-undo
- 记得前面实现了个钩子叫extraReducers,extra顾名思义是额外的reducer,自己添加的新的处理逻辑。而这个onReducer是增强reducer,类似于reducer中间件的感觉。并不是增加额外的reducer。
- 先看一个插件叫redux-undo。这个插件可以让状态有时间旅行的功能。听起来很酷对吧,实际上就是你派发的action改变了状态,然后它就把之前的状态记录下,目前的状态记录下。当你回退操作的时候,目前的状态就拿之前的状态,未来的状态就变为前面目前的状态。实际工作中,这玩意能用在啥地方真不太容易能想到,不过对于经常要重做或者撤销操作倒是很友好。但是需要注意,如果结合别的一些使用可能效果不是太好,比如dva-loading,因为dvaloading在状态改变前后都派发动作,所以使用撤销或者重做点一次只能撤销一个dispatch,就是dvaloading的start或者hide,点第二次才能真正撤销异步的那次操作,点第三次还得再次撤销start或者hide。
- 首先,看官方用法,安装这玩意:
cnpm i redux-undo -S
- 然后引入它:
import undoable, { ActionCreators } from 'redux-undo'
let app = dva({
onReducer: reducer => {
let undoReducer = undoable(reducer)
return function (state, action) {
let newState = undoReducer(state, action)
let router = newState.present.router ? newState.present.router : newState.present.routing
return { ...newState, router: router }
}
}
})
- 这个undoable类似于中间件,所以对传来的reducer进行包裹,返回一个增强过的reducer。为啥不直接返回undoReducer?因为前面使用路由时,获取路由信息是在router上,现在没有router了,于是获取不到。由于这个增强器把state改成past,present,future了,所以真正的router会在present里,于是我们可以改写时把这个router拎出来,这样就能取到了。以前版本叫routing,现在叫router。
- 然后增加2按钮,重做和撤销。
<button onClick={() => { props.dispatch(ActionCreators.undo()) }}>撤销</button>
<button onClick={() => { props.dispatch(ActionCreators.redo()) }}>重做</button>
- 另外connect也需要修改,因为connect的给组件提供的状态是present的。
let ConnectedCounter1 = connect(state => state.present.counter1)(Counter1)
- 这样就可以使用了。
- 下面试着自己写个redux-undo。原理就是上面说的保存状态做成现在过去未来,除了现在不是数组,过去和未来都是个数组,就像有个指针一样,在这些里面切换。从刚才配置里看见这个是使用了onReducer的钩子,然后传入的是原版reducer,另外还需要做个action。
const UNDO = 'UNDO'
const REDO = 'REDO'
export default function (reducer) {
let initialState = {
past: [],
present: reducer(undefined, {}),
future: []
}
return function (state = initialState, action) {
let { past, present, future } = state
switch (action.type) {
case UNDO: //撤销,把当前放未来,过去里拿出来放现在
if (past.length !== 0) {
if (!future) future = []
return {
present: past.pop(),
past,
future: [present, ...future]
}
} else {
return {
past, present, future
}
}
case REDO: //重做 把未来放到当前,把现在放过去
if (future.length !== 0) {
if (!past) past = []
return {
past: [...past, present],
present: future.shift(),
future
}
} else {
return {
past, present, future
}
}
default://默认就是正常操作
const newState = reducer(present, action)
if (!past) past = []
return {
past: [...past, present],
present: newState,
future
}
}
}
}
export const ActionCreators = {
undo() {
return { type: UNDO }
},
redo() {
return { type: REDO }
}
}
- 注意这个initialState里present的值reducer的返回值,如果用原版的话是不能不传的,使用dva2.6以下也会跟dva2.6不太一样,所以用自己写的就没问题。它内部对reducer传入参数进行了判断和报错。
- 然后就是内部加个钩子吧:
plugin.js
const hooks = [
"onEffect",//增强effect
"extraReducers",//添加reducer
"onAction",
"onStateChange",
"onReducer"
]
export function filterHooks(options) {
return Object.keys(options).reduce((prev, next) => {
if (hooks.indexOf(next) > -1) {
prev[next] = options[next]
}
return prev
}, {})
}
export default class Plugin {
constructor() {
this.hooks = hooks.reduce((prev, next) => {
prev[next] = []
return prev
}, {})//{hook:[],hook:[]}
}
use(plugin) {
const { hooks } = this
for (let key in plugin) {
hooks[key].push(plugin[key])
}//{hook:[fn|obj]}
}
get(key) {//合并值
if (key === 'extraReducers') {
return Object.assign({}, ...this.hooks[key])
} if (key === 'onReducer') {
return getOnReducer(this.hooks[key])
} else {
return this.hooks[key]
}
}
}
function getOnReducer(hook){
return function(reducer){
for (const reducerEnhancer of hook ){
reducer=reducerEnhancer(reducer)
}
return reducer
}
}
- 对获取到的reducer增强器遍历,然后把原本reducer传进去,最后生成一个新的reducer。
- 另外dva的index.js就把reducer套一层:
function createReducer() {
let extraReducers = plugin.get('extraReducers')
let reducerEnhancer=plugin.get('onReducer')
return reducerEnhancer( combineReducers({
...initialReducers,
...extraReducers//这里是传来的中间件对象
}))//reducer结构{reducer1:fn,reducer2:fn}
}
- 为什么套到createReducer里去?别忘了app的start执行前会走一遍,后面如果有替换reducer,还会走一遍这个,所以在方法里改一遍就可以复用了。
- 这样就实现了。剩下的下次写。