项目优化之撤销功能的优化

项目优化之撤销功能的优化

本文写于2018/7/12

1、初始

在开发一个管理系统的时候,遇到了这样的一个需求,用户可以通过操作建立一颗组织机构树,在建立树的过程中可以对树进行新增节点、修改属性、删除节点以及撤销操作等功能。

对于其他功能的实现,就不再多做讲述了,这里主要介绍一下撤销功能,这个功能伪代码实现如下。

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中最新的快照取出,通过快照重新渲染界面即可实现。

2、问题

虽然上面的方法可以实现撤销功能,但是在使用的过程中,随着树节点的逐渐增加以及操作的频繁程度,也带来了一些性能的问题。下面看一下引起问题的原因:

深拷贝
history.push(JSON.parse(JSON.stringify(tree)))

第一个原因是深拷贝。在我们复制树的数据的时候,会使用JSON.parse(JSON.stringify(tree))获取当前树的快照,当树中的节点比较多时,这个操作就会带来一个不小的开销。

全量存储

第二个原因就是对每次操作之后树的数据的全量存储。每次用户进行一次操作,我们就将快照放入history数组中,当用户操作了成千上万次时,history数组中的数据会非常庞大,这对内存也造成了很大的消耗。虽然我们可以通过控制history数组的长度来解决这个问题,但是还有没有更好的办法呢?

在分析了造成性能问题的原因之后,我们发现第一个问题深拷贝也是由全量存储所带来的并发问题,所以我们只要解决全量存储的问题即可。

3、优化

对全量存储最简单的优化方法就是使用增量存储。增量存储在这个场景中可以是存储每次用户所进行的操作,在确定了这一点后,可以提出以下的优化思路。

  • 每次操作完成后将用户所进行的操作放入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中存储的内容大小,另一方面避免了深拷贝所带来的性能消耗。但是,这种优化方式也有较大的局限性,仅适合操作较为简单的场景中。

你可能感兴趣的:(前端)