使用 RxJS 模拟命令队列和撤消堆栈
Simulating a command queue and undo stack with RxJS
我正在尝试使用 RxJS 复制 this demo。该演示是一个小型应用程序,用户可以在其中控制机器人。机器人可以向前或向后移动,向左或向右旋转,以及捡起或放下物品。用户可以对命令进行排队(如"Forward"、"rotate"),当用户点击"Execute"-按钮时,队列中的命令就会被执行。用户还可以撤消已经执行的命令。
传统上,这个应用程序可以很容易地使用队列来为尚未执行的命令实现。执行的命令被压入堆栈,每当按下撤消按钮时,顶部的命令就会弹出并撤消。
我能够 "collect" 命令并通过这样做来执行它们:
var id = 0;
var add = Rx.Observable.fromEvent($("#add"), 'click').map(function(){
var ret = "Command_"+id;
id++;
return ret
})
var invoke = Rx.Observable.fromEvent($("#invoke"), 'click')
var invokes = add.buffer(invoke)
buffer() 方法将流转换为数组流。我可以订阅调用流并获取命令数组:
invokes.subscribe(function(command_array){...})
或者我可以创建一个 Rx.Subject() ,我只需一个一个地推送命令:
var invoked_commands = new Rx.Subject()
invokes.subscribe(function(command_array){
for(var i=0; i < command_array.length; i++){
invoked_commands.onNext(command_array[i])
}
});
invoked_commands.subscribe(function(command){ ...});
老实说,我不知道哪种方法更好,但我又一次不知道这是否对我来说太相关了。我一直在想如何实现撤消功能,但我完全不知道该怎么做。
在我看来,它必须是这样的(抱歉格式化):
-c1---c2-c3-------->
----------------u---u->("u" = 单击撤消按钮)
----------------c3--c2>(从新到旧获取命令,调用undo()方法)
所以我的问题是双重的:
- 我收集命令的方法好吗?
- 如何实现撤消功能?
编辑:我正在比较转换式和反应式样式,并且我正在使用两者来实现此演示。因此,我想尽可能多地使用 Rx* 特性。
您必须继续维护撤消堆栈的状态。我认为您收集命令的方法是合理的。如果您保留 Subject
,您可以通过再次订阅该主题来将撤消功能与命令执行分离:
var undoQueue = [];
invoked_commands.subscribe(function (c) { undoQueue.unshift(c); });
Rx.Observable
.fromEvent($("#undo"), "click")
.map(function () { return undoQueue.pop(); })
.filter(function (command) { return command !== undefined; })
.subscribe(function (command) { /* undo command */ });
编辑:仅使用没有可变数组的 Rx。这似乎不必要地令人费解,但是哦,它是实用的。我们使用 scan
来维护撤消队列,并发出一个包含当前队列的元组以及是否应执行撤消命令。我们将执行的命令与撤消事件合并。执行命令添加到队列,撤消事件从队列中弹出。
var undo = Rx.Observable
.fromEvent($("#undo"), "click")
.map(function () { return "undo"; });
invoked_commands
.merge(undo)
.scan({ undoCommand: undefined, q: [] }, function (acc, value) {
if (value === "undo") {
return { undoCommand: acc.q[0], q: acc.q.slice(1) };
}
return { undoCommand: undefined, q: [value].concat(acc.q) };
})
.pluck("undoCommand")
.filter(function (c) { return c !== undefined })
.subscribe(function (undoCommand) { ... });
我刚刚创建了类似的东西,虽然有点复杂。
也许对某人有帮助。
// Observable for all keys
const keypresses = Rx.Observable
.fromEvent(document, 'keydown')
// Undo key combination was pressed
// mapped to function that undoes the last accumulation of pressed keys
const undoPressed = keypresses
.filter(event => event.metaKey && event.key === 'z')
.map(() => (acc) => acc.slice(0, isEmpty(last(acc)) && -2 || -1).concat([[]]))
// a 'simple' key was pressed
const inputChars = keypresses
.filter(event => !event.altKey && !event.metaKey && !event.ctrlKey)
.map(get('key'))
.filter(key => key.length === 1)
// the user input, respecting undo
const input = inputChars
.map((char) => (acc) =>
acc.slice(0, -1).concat(
acc.slice(-1).pop().concat(char)
)
) // map input keys to functions that append them to the current list
.merge(undoPressed)
.merge(
inputChars
.auditTime(1000)
.map(() => (acc) => isEmpty(last(acc)) && acc || acc.concat([[]]))
) // creates functions, that start a new accumulator 1 sec after the first key of a stroke was pressed
.scan(
(acc, f) => f(acc),
[[]],
) // applies the merged functions to a list of accumulator strings
.map(join('')) // join string
.distinctUntilChanged() // ignore audit event, because it doesn't affect the current string
我正在尝试使用 RxJS 复制 this demo。该演示是一个小型应用程序,用户可以在其中控制机器人。机器人可以向前或向后移动,向左或向右旋转,以及捡起或放下物品。用户可以对命令进行排队(如"Forward"、"rotate"),当用户点击"Execute"-按钮时,队列中的命令就会被执行。用户还可以撤消已经执行的命令。
传统上,这个应用程序可以很容易地使用队列来为尚未执行的命令实现。执行的命令被压入堆栈,每当按下撤消按钮时,顶部的命令就会弹出并撤消。
我能够 "collect" 命令并通过这样做来执行它们:
var id = 0;
var add = Rx.Observable.fromEvent($("#add"), 'click').map(function(){
var ret = "Command_"+id;
id++;
return ret
})
var invoke = Rx.Observable.fromEvent($("#invoke"), 'click')
var invokes = add.buffer(invoke)
buffer() 方法将流转换为数组流。我可以订阅调用流并获取命令数组:
invokes.subscribe(function(command_array){...})
或者我可以创建一个 Rx.Subject() ,我只需一个一个地推送命令:
var invoked_commands = new Rx.Subject()
invokes.subscribe(function(command_array){
for(var i=0; i < command_array.length; i++){
invoked_commands.onNext(command_array[i])
}
});
invoked_commands.subscribe(function(command){ ...});
老实说,我不知道哪种方法更好,但我又一次不知道这是否对我来说太相关了。我一直在想如何实现撤消功能,但我完全不知道该怎么做。
在我看来,它必须是这样的(抱歉格式化):
-c1---c2-c3-------->
----------------u---u->("u" = 单击撤消按钮)
----------------c3--c2>(从新到旧获取命令,调用undo()方法)
所以我的问题是双重的:
- 我收集命令的方法好吗?
- 如何实现撤消功能?
编辑:我正在比较转换式和反应式样式,并且我正在使用两者来实现此演示。因此,我想尽可能多地使用 Rx* 特性。
您必须继续维护撤消堆栈的状态。我认为您收集命令的方法是合理的。如果您保留 Subject
,您可以通过再次订阅该主题来将撤消功能与命令执行分离:
var undoQueue = [];
invoked_commands.subscribe(function (c) { undoQueue.unshift(c); });
Rx.Observable
.fromEvent($("#undo"), "click")
.map(function () { return undoQueue.pop(); })
.filter(function (command) { return command !== undefined; })
.subscribe(function (command) { /* undo command */ });
编辑:仅使用没有可变数组的 Rx。这似乎不必要地令人费解,但是哦,它是实用的。我们使用 scan
来维护撤消队列,并发出一个包含当前队列的元组以及是否应执行撤消命令。我们将执行的命令与撤消事件合并。执行命令添加到队列,撤消事件从队列中弹出。
var undo = Rx.Observable
.fromEvent($("#undo"), "click")
.map(function () { return "undo"; });
invoked_commands
.merge(undo)
.scan({ undoCommand: undefined, q: [] }, function (acc, value) {
if (value === "undo") {
return { undoCommand: acc.q[0], q: acc.q.slice(1) };
}
return { undoCommand: undefined, q: [value].concat(acc.q) };
})
.pluck("undoCommand")
.filter(function (c) { return c !== undefined })
.subscribe(function (undoCommand) { ... });
我刚刚创建了类似的东西,虽然有点复杂。 也许对某人有帮助。
// Observable for all keys
const keypresses = Rx.Observable
.fromEvent(document, 'keydown')
// Undo key combination was pressed
// mapped to function that undoes the last accumulation of pressed keys
const undoPressed = keypresses
.filter(event => event.metaKey && event.key === 'z')
.map(() => (acc) => acc.slice(0, isEmpty(last(acc)) && -2 || -1).concat([[]]))
// a 'simple' key was pressed
const inputChars = keypresses
.filter(event => !event.altKey && !event.metaKey && !event.ctrlKey)
.map(get('key'))
.filter(key => key.length === 1)
// the user input, respecting undo
const input = inputChars
.map((char) => (acc) =>
acc.slice(0, -1).concat(
acc.slice(-1).pop().concat(char)
)
) // map input keys to functions that append them to the current list
.merge(undoPressed)
.merge(
inputChars
.auditTime(1000)
.map(() => (acc) => isEmpty(last(acc)) && acc || acc.concat([[]]))
) // creates functions, that start a new accumulator 1 sec after the first key of a stroke was pressed
.scan(
(acc, f) => f(acc),
[[]],
) // applies the merged functions to a list of accumulator strings
.map(join('')) // join string
.distinctUntilChanged() // ignore audit event, because it doesn't affect the current string