Warning: file_get_contents(/data/phpspider/zhask/data//catemap/3/reactjs/21.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Javascript 在Redux中实现撤销/重做 背景_Javascript_Reactjs_Redux_Undo Redo - Fatal编程技术网

Javascript 在Redux中实现撤销/重做 背景

Javascript 在Redux中实现撤销/重做 背景,javascript,reactjs,redux,undo-redo,Javascript,Reactjs,Redux,Undo Redo,一段时间以来,我一直在绞尽脑汁研究如何在服务器交互中实现撤销/重做(通过ajax) 我提出了一个解决方案,使用where将操作注册为execute和undo方法作为命令,而不是分派操作,而是分派命令。命令然后存储在堆栈中,并在需要时引发新操作 我当前的实现使用中间件来截获调度、测试命令和调用命令的方法,如下所示: 中间件 行动 命令 应用程序 (一个更完整的例子) 我发现我目前的方法存在几个问题: 由于通过中间件实现,整个应用程序可能只存在一个堆栈 无法自定义UNDO命令类型 创建一个命令来调

一段时间以来,我一直在绞尽脑汁研究如何在服务器交互中实现撤销/重做(通过ajax)

我提出了一个解决方案,使用where将操作注册为
execute
undo
方法作为命令,而不是分派操作,而是分派命令。命令然后存储在堆栈中,并在需要时引发新操作

我当前的实现使用中间件来截获调度、测试命令和调用命令的方法,如下所示:

中间件 行动 命令 应用程序 (一个更完整的例子)

我发现我目前的方法存在几个问题:

  • 由于通过中间件实现,整个应用程序可能只存在一个堆栈
  • 无法自定义
    UNDO
    命令类型
  • 创建一个命令来调用相应的操作,从而返回承诺似乎非常复杂
  • 命令在操作完成之前添加到堆栈中。错误会发生什么
  • 由于命令未处于状态,因此无法添加可撤消功能
  • 您将如何实现乐观更新
帮助 那么,我的问题是,有人能提出一种更好的方法在Redux中实现这一功能吗

我现在看到的最大的缺陷是在操作完成之前添加的命令,以及如何在组合中添加乐观更新


非常感谢您的任何见解。

我不确定是否完全理解您的用例,但我认为在ReactJS中实现撤销/重做的最佳方式是通过模型。一旦您的模型是不可变的,您就可以在状态发生变化时轻松地维护状态列表。具体来说,您需要一个撤销列表和重做列表。在您的示例中,它类似于:

  var newState = ...;
  var previousState = undoList[undoList.length - 1]
  undoList.push(newState);
  post('server.com', buildServerRequestFrom(newState), onSuccess, err => { while(undoList[undoList.length-1] !== previousState) undoList.pop() };
  • 起始计数器值=0->[0],]
  • 添加5->[0,5],]
  • 添加10->[0,5,15],]
  • 撤消->[0,5],[15]
  • 重做->[0,5,15],]
  • 第一个列表中的最后一个值是当前状态(进入组件状态)

    这是一种比命令简单得多的方法,因为您不需要为要执行的每个操作分别定义撤消/重做逻辑

    如果需要与服务器同步状态,也可以这样做,只需将AJAX请求作为撤消/重做操作的一部分发送即可

    乐观更新也应该是可能的,您可以立即更新您的状态,然后发送您的请求,并在其错误处理程序中恢复到更改之前的状态。比如:

      var newState = ...;
      var previousState = undoList[undoList.length - 1]
      undoList.push(newState);
      post('server.com', buildServerRequestFrom(newState), onSuccess, err => { while(undoList[undoList.length-1] !== previousState) undoList.pop() };
    

    事实上,我相信你应该能够实现你用这种方法列出的所有目标。如果你不这么认为,你能更具体地说明你需要做什么吗?

    进一步讨论@vladimir rovensky建议的基于不变的实现…

    Immutable非常适合客户端撤消重做管理。您可以自己存储不可变状态的最后“N”个实例,也可以使用类似的库来存储不可变状态的最后“N”个实例。由于实例共享内置在不可变内存中,所以不会导致内存开销

    但是,如果您希望保持模型的简单性,那么每次将模型与服务器同步可能会花费高昂的成本,因为每次在客户端上修改模型时,您都需要将整个状态发送到服务器。根据州大小,这将无法很好地扩展

    更好的方法是只向服务器发送修改。当您最初将“修订”标题发送到客户端时,您的状态中需要一个“修订”标题。在客户机上对状态进行的每一次其他修改都应该只记录差异,并将其与修订一起发送到服务器。服务器可以执行diff操作并发回diff之后状态的新修订和校验和。客户端可以根据当前状态校验和来验证这一点,并存储新版本。差异也可以由服务器在其自己的撤消历史记录中存储,并标记有修订和校验和。如果需要在服务器上撤消,则可以反转差异以获取状态,并可以执行校验和检查。 我遇到的一个不可变的diffing库是。它创建RFC-6902样式的修补程序,您可以在服务器状态下使用这些修补程序执行

    优势-

    • 简化的客户端架构。服务器同步不会分散在客户端代码中。只要客户机状态发生变化,就可以从您的存储启动它
    • 简单的撤消/重做与服务器同步。不需要单独处理不同的客户端状态更改,也就是说,不需要命令堆栈。diff补丁以一致的方式跟踪几乎任何类型的状态变化
    • 服务器端撤消历史记录,无重大事务命中
    • 验证检查确保数据一致性
    • 修订标题允许多客户端同时更新

      • 您已经找到了最好的解决方案,是的,命令模式是异步撤消/重做的方式

        一个月前,我意识到ES6生成器被低估了,可能会给我们带来比计算斐波那契序列更好的结果。异步撤消/重做就是一个很好的例子

        在我看来,您的方法的主要问题是使用类和忽略失败的操作(在您的示例中,乐观更新过于乐观)。我尝试使用异步生成器解决这个问题。想法很简单,异步生成器返回的
        asyncierator
        可以在需要撤销时恢复,这基本上意味着您需要
        dispatch
        所有中间操作,
        yield
        最终乐观操作和
        return
        最终撤销操作。请求撤销后,您只需恢复函数并执行撤销所需的所有操作(应用程序状态变化/api调用/副作用)。另一个
        收益率
        意味着
        class Command {
          execute() {
            throw new Error('Not Implemented');
          }
        
          undo() {
            throw new Error('Not Implemented');
          }
        }
        
        class AddCommand extends Command {
            constructor(value) {
                super();
                this.value = value;
            }
        
            execute() {
                return add(this.value);
            }
        
            undo() {
                return sub(this.value);
            }
        }
        
        const store = createStoreWithMiddleware(appReducer);
        
        store.dispatch(new AddCommand(10)); // counter = 10
        store.dispatch(new AddCommand(5)); // counter = 15
        
        // Some time later
        store.dispatch(undo()); // counter = 10
        
          var newState = ...;
          var previousState = undoList[undoList.length - 1]
          undoList.push(newState);
          post('server.com', buildServerRequestFrom(newState), onSuccess, err => { while(undoList[undoList.length-1] !== previousState) undoList.pop() };
        
        export const addTodo = todo => async function*(dispatch) {
          let serverId = null;
          const transientId = `transient-${new Date().getTime()}`;
        
          // We can simply dispatch action as using standard redux-thunk
          dispatch({
            type: 'ADD_TODO',
            payload: {
              id: transientId,
              todo
            }
          });
        
          try {
            // This is potentially an unreliable action which may fail
            serverId = await api(`Create todo ${todo}`);
        
            // Here comes the magic:
            // First time the `next` is called
            // this action is paused exactly here.
            yield {
              type: 'TODO_ADDED',
              payload: {
                transientId,
                serverId
              }
            };
          } catch (ex) {
            console.error(`Adding ${todo} failed`);
        
            // When the action fails, it does make sense to
            // allow UNDO so we just rollback the UI state
            // and ignore the Command anymore
            return {
              type: 'ADD_TODO_FAILED',
              payload: {
                id: transientId
              }
            };
          }
        
          // See the while loop? We can try it over and over again
          // in case ADD_TODO_UNDO_FAILED is yielded,
          // otherwise final action (ADD_TODO_UNDO_UNDONE) is returned
          // and command is popped from command log.
          while (true) {
            dispatch({
              type: 'ADD_TODO_UNDO',
              payload: {
                id: serverId
              }
            });
        
            try {
              await api(`Undo created todo with id ${serverId}`);
        
              return {
                type: 'ADD_TODO_UNDO_UNDONE',
                payload: {
                  id: serverId
                }
              };
            } catch (ex) {
              yield {
                type: 'ADD_TODO_UNDO_FAILED',
                payload: {
                  id: serverId
                }
              };
            }
          }
        };
        
        export default ({dispatch, getState}) => next => action => {
          if (typeof action === 'function') {
            const command = action(dispatch);
        
            if (isAsyncIterable(command)) {
              command
                .next()
                .then(value => {
                  // Instead of using function closure for middleware factory
                  // we will sned the command to app state, so that isUndoable
                  // can be implemented
                  if (!value.done) {
                    dispatch({type: 'PUSH_COMMAND', payload: command});
                  }
        
                  dispatch(value.value);
                });
        
              return action;
            }
          } else if (action.type === 'UNDO') {
            const commandLog = getState().commandLog;
        
            if (commandLog.length > 0 && !getState().undoing) {
              const command = last(commandLog);
        
              command
                .next()
                .then(value => {
                  if (value.done) {
                    dispatch({type: 'POP_COMMAND'});
                  }
        
                  dispatch(value.value);
                  dispatch({type: 'UNDONE'});
                });
            }
          }
        
          return next(action);
        };