本文写于2018/7/12
在开发一个管理系统的时候,遇到了这样的一个需求,用户可以通过操作建立一颗组织机构树,在建立树的过程中可以对树进行新增节点、修改属性、删除节点以及撤销操作等功能。
对于其他功能的实现,就不再多做讲述了,这里主要介绍一下撤销功能,这个功能伪代码实现如下。
let tree = {} // 组织机构树的数据结构
let history = []
function render (tree) {
// render tree
}
function pushHistory () {
history.push(JSON.parse(JSON.stringify(tree)))
}
/**
* action的装饰器
*/
function actionDecorator (actionFn) {
return function () {
let result = actionFn.call(this, ...arguments)
render(tree)
pushHistory()
}
}
function addAction (node) {
// ...some action
}
function deleteAction (node) {
// ...some action
}
function editAction (oldNode, newNode) {
// ...some action
}
function moveAction (node, from, to) {
// ...some action
}
const add = actionDecorator(addAction)
const delete = actionDecorator(deleteAction)
const edit = actionDecorator(editAction)
const move = actionDecorator(moveAction)
/**
* 撤销
*/
function actionUndo () {
if (history.length > 0) } {
render(history.pop())
}
}
可以从上面的代码看到,最初版的撤销功能的实现十分简单,就是通过一个装饰器,在每个操作(添加、删除、编辑)执行完成以后将当前树的快照(深拷贝)保存到history数组中。而撤销功能就是将history中最新的快照取出,通过快照重新渲染界面即可实现。
虽然上面的方法可以实现撤销功能,但是在使用的过程中,随着树节点的逐渐增加以及操作的频繁程度,也带来了一些性能的问题。下面看一下引起问题的原因:
history.push(JSON.parse(JSON.stringify(tree)))
第一个原因是深拷贝。在我们复制树的数据的时候,会使用JSON.parse(JSON.stringify(tree))获取当前树的快照,当树中的节点比较多时,这个操作就会带来一个不小的开销。
第二个原因就是对每次操作之后树的数据的全量存储。每次用户进行一次操作,我们就将快照放入history数组中,当用户操作了成千上万次时,history数组中的数据会非常庞大,这对内存也造成了很大的消耗。虽然我们可以通过控制history数组的长度来解决这个问题,但是还有没有更好的办法呢?
在分析了造成性能问题的原因之后,我们发现第一个问题深拷贝也是由全量存储所带来的并发问题,所以我们只要解决全量存储的问题即可。
对全量存储最简单的优化方法就是使用增量存储。增量存储在这个场景中可以是存储每次用户所进行的操作,在确定了这一点后,可以提出以下的优化思路。
伪代码实现如下:
let tree = {} // 组织机构树的数据结构
let history = []
// 逆向操作的方法
let reverse = {
add (node) {
delete(node)
},
delete (node) {
add(node)
},
edit (oldNode, newNode) {
edit(newNode, oldNode)
},
move (node, from, to) {
move(node, to, from)
}
}
function render (tree) {
// render tree
}
function pushHistory (options) {
// options内包含操作的名称,以及逆向操作时所需的参数
history.push(options)
}
function add (node) {
// ...some action
// 在完成操作后,将操作的名称及参数push到history中
pushHistory({
name: 'add',
params: {
node
}
})
}
function delete (node) {
// ...some action
// 在完成操作后,将操作的名称及参数push到history中
pushHistory({
name: 'delete',
params: {
node
}
})
}
function edit (oldNode, newNode) {
// ...some action
// 在完成操作后,将操作的名称及参数push到history中
pushHistory({
name: 'edit',
params: {
oldNode,
newNode
}
})
}
function move (node, from, to) {
// ...some action
// 在完成操作后,将操作的名称及参数push到history中
pushHistory({
name: 'move',
params: {
node,
from,
to
}
})
}
/**
* 撤销
*/
function undo () {
if (history.length > 0) } {
let { name, params } = history.pop()
reverse[name](...params)
render(tree)
}
}
可以看到,在新的代码中,在执行完操作之后,将用户的每次操作的名称以及参数保存到了history中,当使用撤销的时候,我们只要从history取出上次的操作,并将操作放到我们维护的逆向操作集合reverse中执行一次逆向操作,最后在渲染逆向操作之后的tree就可以了。
使用了上面的优化方法后,一方面减少了history中存储的内容大小,另一方面避免了深拷贝所带来的性能消耗。但是,这种优化方式也有较大的局限性,仅适合操作较为简单的场景中。