浅用高阶Redux原理实现日志流水--实现篇

浅用高阶 Redux 原理实现日志流水–实现篇

Demo B 要实现低代码操作流水记录,实现撤回重做的功能,一开始的想法比较简单,直接在 action 中完善撤回重做功能,但是后面参考官方的 redux ,其实有个高级的写法,也就是高阶 Redux,可以实现可插件式的可撤退重做日志。具体高阶 Redux 介绍,可以移步到,文章:Redux 官方实现撤销重做 – 解析

参考:Web 应用的撤销重做实现 - 掘金 (juejin.cn)

实现撤销重做 · Redux

01 实现撤回重做的方案

  • 数据快照式,也就是将每次操作完之后,数据状态进行保存,从而形成历史记录
    • 优点:通用性广,实现容易,可封装成组件成可插拔插件
    • 缺点:假若数据结构复杂庞大,随着操作历史记录变多,导致数据量冗余,不太好管理
  • 路径新旧值式,单一数据源,对每次操作后,将操作数据的路径、新值和旧值作为记录进行保存
    • 优点:只需记录操作路径和新旧值,减少大量的内存消耗,方便管理数据
    • 缺点:实现比较难, 需要对 action 的传入数据的结构有所要求(路径、新旧值)

下文将采用的是路劲新旧值式的方案

02 数据结构的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
histories: [],
historyIndex: -1,
current:{}
}

// 例:histories存放一个更新操作
histories = [
{
path:["state","Data",0,"name"],
newValue: "xiaoming",
oldValue: "KK"
}
]

其中,

  • histories: 用于存放记录数组
  • historyIndex: 游标,用于标识当前历史记录的指针
  • current:存放业务 reducer 返回最新的业务 state

03 撤销重做算法

撤销 undo

  • 判断 historyIndex 的值,从而判断撤退进行到什么状态。
    • 0 ,说明已经执行到栈底了
    • -1,未发生撤退操作
    • != -1 && != 0,正在进行撤退操作

image-20220514111615614

  • 执行撤回操作
    • newValue === null, 为删除操作,对应撤回操作 => 新增操作
    • oldValue === null, 为新增操作,对应撤回操作 => 删除操作
    • newValue !== null && oldValue !== null ,为更新操作,对应撤回操作 => 更新操作

重做 redo

  • 判断 historyIndex 的值,是否已经发生撤回操作

    image-20220514113531299

  • 执行重做操作

    • newValue === null, 为删除操作,对应重做操作 => 删除操作
    • oldValue === null, 为新增操作,对应撤重回操作 =>新增操作
    • newValue !== null && oldValue !== null ,为更新操作,对应重做操作 => 更新操作
  • 最后,判断 historyIndex 是否到达栈顶

    • 到达栈顶,重置游标historyIndex = -1
    • 未到达栈顶,historyIndex ++1

其他业务 Action

  • 特殊情况:当在发生撤退重做时,若发生其他业务 Action 操作,应该把当前游标后面的记录清空,并推入新的业务 Action 操作

  • 执行其他业务的 Action,委托给业务的 reducer 进行处理,并将业务 reducer 返回新的 业务 state 存储到 current

  • 高阶 reducer 接收 include 数组参数,用于判断是否将这个操作加入撤退重做记录中

    • 属于,将其封装成记录,并推入 histories
    • 不属于,无处理
  • 额外操作:当高阶 reducer 接收 limit 参数,用于限制记录的最大长度

    • 默认, limit = false,不进行记录长度的限制
    • limit 为数字,且大于 0。对记录进行限制

03 实现源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import _ from 'lodash'

export default function undoReducerEnhancer(
reducer,
{
limit = false, // 日志流水最大长度
undoType = 'UNDO', // 撤销操作类型
redoType = 'REDO', // 重做操作类型
include = [], // 需要加入日志流水的操作类型
}
) {
// 以一个空的 action 调用 reducer,来初始化 state
const initialState = {
histories: [], //日志流水数组
historyIndex: -1, // 游标, -1: 说明是第一次撤销
// eslint-disable-next-line no-undefined
current: reducer(undefined, {}), // 当前状态
}

const Add = (list = [], { path, newValue }) => {
let newList = _.cloneDeep(list)

// 需要存放的目标数组
const target = path.slice(0, path.length - 1)

// 需要存放的目标数组的游标
const targetIndex = path[path.length - 1]
const newTarget = _.get(newList, target)
newTarget.splice(targetIndex, 0, newValue)
newList = _.set(newList, target, newTarget)
return newList
}

const Update = (list = [], { path = [], newValue }) => {
// 需要存放的目标数组
let newList = _.cloneDeep(list)

// path 路径为空, 修改就是当前对象
if (path.length !== 0) {
newList = _.set(newList, path, newValue)
} else {
newList = newValue
}
return newList
}

const Delete = (list = [], { path }) => {
let newList = _.cloneDeep(list)

// 需要存放的目标数组
const target = path.slice(0, path.length - 1)

// 需要存放的目标数组的游标
const targetIndex = path[path.length - 1]
let newTarget = _.get(newList, target)
newTarget.splice(targetIndex, 1)
newList = _.set(newList, target, newTarget)
return newList
}

// 根据判断是否加入撤销重做流水操作
const isInclude = (actionType) =>
include.indexOf(actionType) === -1 ? false : true

// 返回一个可撤销和重做的新的 reducer
return (state = initialState, action) => {
const { histories, historyIndex } = state

// 撤销操作
if (action.type === undoType) {
if (historyIndex === -1) {
// 第一次撤退
state.historyIndex = histories.length - 1
} else if (historyIndex > 0) {
// 非第一次撤退
state.historyIndex--
} else {
// 撤退end,返回
return state
}

// 执行撤退操作
const { path, newValue, oldValue } = histories[state.historyIndex]
if (newValue === null) {
// 删除操作 => 新增操作
state.current = Add(state.current, { path, newValue: oldValue })
} else if (oldValue === null) {
// 新增操作 => 删除操作
state.current = Delete(state.current, { path })
} else {
// 更新操作 => 更新操作
state.current = Update(state.current, { path, newValue: oldValue })
}
return _.cloneDeep(state)
}

// 重做操作
if (action.type === redoType) {
if (historyIndex === -1) {
// 未发生撤销
return state
}

// 执行重做操作
const { path, newValue, oldValue } = histories[state.historyIndex]
if (newValue === null) {
// 删除操作 => 删除操作
state.current = Delete(state.current, { path })
} else if (oldValue === null) {
// 新增操作 => 新增操作
state.current = Add(state.current, { path, newValue })
} else {
// 更新操作 => 更新操作
state.current = Update(state.current, { path, newValue })
}

if (state.historyIndex < histories.length - 1) {
// 继续重做
state.historyIndex++
} else {
// 重做end,返回
state.historyIndex = -1
}
return _.cloneDeep(state)
}

// 其他业务操作, 委托给业务reducer处理
// 特殊情况:打断撤销重做操作,清空 historyIndex 后面所有操作流水,并置 historyIndex 为 -1
if (state.historyIndex !== -1) {
state.histories = state.histories.slice(0, state.historyIndex)
state.historyIndex = -1
}
const newState = reducer(state.current, action)
state.current = newState

// 如果是 actonType 是属于 Include ,则加入撤销重做流水
if (isInclude(action.type)) {
state.histories.push({
type: action.type,
...action.payload,
})
}

// 当流水长度超过了 limit 长度时
if (limit && limit > 0) {
state.histories = state.histories.slice(-limit)
}
return _.cloneDeep(state)
}
}

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import undoReducerEnhancer from './undoReducerEnhancer'

// 接受一个业务refucer,返回一个具有可撤退重做的新的reducer
const undoCustomComponent = undoReducerEnhancer(customComponent, {
limit: 10, // 限制记录长度
undoType: Types.UNDO_HISTORY, // 撤退操作别名
redoType: Types.REDO_HISTORY, // 重做操作别名
include: [
Types.ADD_COMPONENT,
Types.PASTE_COMPONENT,
Types.UPDATE_COMPONENT_CONFIG,
Types.UPDATE_FORM_CONFIG,
Types.UPDATE_DATA_SORT,
Types.DELETE_COMPONENT,
Types.DELETE_ALL_COMPONENTS,
],
// 加入需要记录流水的操作 action
})

export default undoCustomComponent

04 不足之处

  • 还未解决,封装一个方法,可以使得业务 action 无需按照高阶 reducer 的需要的数据格式进行数据包装,做到真正地可插拔式
  • 提供更多的参数,以便做到职责分明,减少耦合依赖