Redux 中的排队操作

IT技术 javascript reactjs redux
2021-04-25 11:45:23

我目前遇到了一种情况,我需要连续运行 Redux Actions。我已经查看了各种中间件,例如 redux-promise,如果您知道在触发根操作(由于缺乏更好的术语)时的连续操作是什么,这似乎没问题

本质上,我想维护一个可以随时添加的操作队列。每个对象在其状态下都有一个该队列的实例,并且相关的动作可以相应地入队、处理和出队。我有一个实现,但这样做时我正在访问我的动作创建者中的状态,这感觉像是一种反模式。

我将尝试提供一些有关用例和实现的上下文。

用例

假设您想创建一些列表并将它们保存在服务器上。在创建列表时,服务器使用该列表的 ID 进行响应,该 ID 用于与列表相关的后续 API 端点:

http://my.api.com/v1.0/lists/           // POST returns some id
http://my.api.com/v1.0/lists/<id>/items // API end points include id

想象一下,客户端想要对这些 API 点执行乐观更新,以增强 UX - 没有人喜欢看微调器。因此,当您创建列表时,您的新列表会立即出现,并带有添加项目的选项:

+-------------+----------+
|  List Name  | Actions  |
+-------------+----------+
| My New List | Add Item |
+-------------+----------+

假设有人在初始创建调用的响应返回之前尝试添加项目。items API 依赖于 id,所以我们知道在我们拥有这些数据之前我们不能调用它。但是,我们可能希望乐观地显示新项目并将对项目 API 的调用排入队列,以便它在创建调用完成后触发。

一个潜在的解决方案

我目前用来解决这个问题的方法是为每个列表提供一个操作队列 - 即将连续触发的 Redux 操作列表。

列表创建的 reducer 功能可能如下所示:

case ADD_LIST:
  return {
    id: undefined, // To be filled on server response
    name: action.payload.name,
    actionQueue: []
  }

然后,在动作创建器中,我们会将动作排入队列,而不是直接触发它:

export const createListItem = (name) => {
    return (dispatch) => {
        dispatch(addList(name));  // Optimistic action
        dispatch(enqueueListAction(name, backendCreateListAction(name));
    }
}

为简洁起见,假设 backendCreateListAction 函数调用获取 API,该 API 在成功/失败时分派消息以从列表中出列。

问题

这里让我担心的是 enqueueListAction 方法的实现。这是我访问状态以管理队列前进的地方。它看起来像这样(忽略名称上的匹配 - 这实际上使用了 clientId,但我试图使示例保持简单):

const enqueueListAction = (name, asyncAction) => {
    return (dispatch, getState) => {
        const state = getState();

        dispatch(enqueue(name, asyncAction));{

        const thisList = state.lists.find((l) => {
            return l.name == name;
        });

        // If there's nothing in the queue then process immediately
        if (thisList.actionQueue.length === 0) {
            asyncAction(dispatch);
        } 
    }
}

在这里,假设 enqueue 方法返回一个普通操作,该操作将异步操作插入到列表 actionQueue 中。

整个事情感觉有点不合时宜,但我不确定是否还有其他方法可以解决。此外,由于我需要在我的 asyncActions 中调度,我需要将调度方法传递给它们。

从列表中出队的方法中有类似的代码,如果存在则触发下一个动作:

const dequeueListAction = (name) => {
    return (dispatch, getState) => {
        dispatch(dequeue(name));

        const state = getState();
        const thisList = state.lists.find((l) => {
            return l.name === name;
        });

        // Process next action if exists.
        if (thisList.actionQueue.length > 0) {
            thisList.actionQueue[0].asyncAction(dispatch);
    }
}

一般来说,我可以接受这个,但我担心这是一种反模式,在 Redux 中可能有一种更简洁、更惯用的方式来做到这一点。

任何帮助表示赞赏。

4个回答

我有您正在寻找的完美工具。当您需要对 redux 进行大量控制(尤其是任何异步操作)并且需要按顺序发生 redux 操作时,没有比Redux Sagas更好的工具了它建立在 es6 生成器之上,为您提供了很多控制权,因为从某种意义上说,您可以在某些点暂停代码。

您描述操作队列就是所谓的saga现在,由于它是为与 redux 一起使用而创建的,因此可以通过在您的组件中进行调度来触发这些 saga 运行。

由于 Sagas 使用生成器,您还可以确定地确保您的调度以特定顺序发生并且仅在特定条件下发生。这是他们文档中的一个示例,我将引导您通过它来说明我的意思:

function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if (token) {
      yield call(Api.storeItem, {token})
      yield take('LOGOUT')
      yield call(Api.clearItem, 'token')
    }
  }
}

好吧,一开始看起来有点混乱,但这个传奇定义了登录序列需要发生的确切顺序。由于生成器的性质,允许无限循环。当您的代码达到收益时,它将停在该行并等待。除非您告诉它,否则它不会继续到下一行。所以看看它说的地方yield take('LOGIN_REQUEST')saga 将在此时屈服或等待,直到您发送 'LOGIN_REQUEST' 之后,saga 将调用授权方法,直到下一次产生。next 方法是异步的,yield call(Api.storeItem, {token})因此在该代码解析之前它不会转到下一行。

现在,这就是魔法发生的地方。传奇将再次停止,yield take('LOGOUT')直到您在应用程序中发送 LOGOUT 。这是至关重要的,因为如果您要在 LOGOUT 之前再次调度 LOGIN_REQUEST,则不会调用登录过程。现在,如果您调度 LOGOUT,它将循环回到第一个 yield 并等待应用程序再次调度 LOGIN_REQUEST。

到目前为止,Redux Sagas 是我最喜欢与 Redux 一起使用的工具之一。它使您可以对应用程序进行如此多的控制,任何阅读您代码的人都会感谢您,因为现在所有内容一次读取一行。

看看这个:https : //github.com/gaearon/redux-thunk

单独的 id 不应该通过减速器。在您的操作创建者 (thunk) 中,首先获取列表 ID,然后 ()执行第二次调用以将项目添加到列表中。在此之后,您可以根据添加是否成功来调度不同的操作。

您可以在执行此操作时分派多个操作,以报告服务器交互何时开始和完成。这将允许您显示消息或微调器,以防操作繁重且可能需要一段时间。

更深入的分析可以在这里找到:http : //redux.js.org/docs/advanced/AsyncActions.html

所有功劳归功于丹·阿布拉莫夫

我遇到了和你类似的问题。我需要一个队列来保证乐观操作以创建它们的相同顺序提交或最终提交(在网络问题的情况下)到远程服务器,或者如果不可能则回滚。我发现仅使用 Redux 就达不到这一点,主要是因为我相信它不是为此而设计的,并且仅使用 Promise 来实现它确实是一个难以推理的问题,除了您需要以某种方式管理队列状态的事实之外。 .. 恕我直言。

我认为@Pcriulan 关于使用 redux-saga 的建议是一个很好的建议。乍一看,redux-saga 不提供任何帮助,直到您进入channels多亏了 JS 生成器,这为您打开了一扇门,可以用其他语言以其他方式处理并发,特别是 CSP(例如,参见 Go 或 Clojure 的 async)。甚至还有关于为什么以 Saga 模式而不是 CSP 命名的问题哈哈……无论如何。

所以这是一个传奇如何帮助你处理你的队列:

export default function* watchRequests() {
  while (true) {
    // 1- Create a channel for request actions
    const requestChan = yield actionChannel('ASYNC_ACTION');
    let resetChannel = false;

    while (!resetChannel) {
      // 2- take from the channel
      const action = yield take(requestChan);
      // 3- Note that we're using a blocking call
      resetChannel = yield call(handleRequest, action);
    }
  }
}

function* handleRequest({ asyncAction, payload }) {
  while (true) {
    try {
      // Perform action
      yield call(asyncAction, payload);
      return false;
    } catch(e) {

      if(e instanceof ConflictError) {
        // Could be a rollback or syncing again with server?
        yield put({ type: 'ROLLBACK', payload });
        // Store is out of consistency so
        // don't let waiting actions come through
        return true;
      } else if(e instanceof ConnectionError) {
        // try again
        yield call(delay, 2000);
      }

    }
  }
}

所以这里有趣的部分是通道如何充当缓冲区(队列),它不断“侦听”传入的动作,但在完成当前动作之前不会继续执行未来的动作。您可能需要查看他们的文档以更好地掌握代码,但我认为这是值得的。重置频道部分可能会或不会满足您的需求:思考:

希望能帮助到你!

这是我将如何解决这个问题:

确保每个本地列表都有唯一的标识符。我不是在这里谈论后端 ID。名称可能不足以识别一个列表?尚未持久化的“乐观”列表应该是唯一可识别的,用户可能会尝试创建 2 个具有相同名称的列表,即使它是边缘情况。

在创建列表时,将后端 id 的Promise添加到缓存中

CreatedListIdPromiseCache[localListId] = createBackendList({...}).then(list => list.id);

添加项目时,尝试从 Redux 商店获取后端 ID。如果它不存在,则尝试从CreatedListIdCache. 返回的 id 必须是异步的,因为 CreatedListIdCache 返回一个Promise。

const getListIdPromise = (localListId,state) => {
  // Get id from already created list
  if ( state.lists[localListId] ) {
    return Promise.resolve(state.lists[localListId].id)
  }
  // Get id from pending list creations
  else if ( CreatedListIdPromiseCache[localListId] ) {
    return CreatedListIdPromiseCache[localListId];
  }
  // Unexpected error
  else {
    return Promise.reject(new Error("Unable to find backend list id for list with local id = " + localListId));
  }
}

在您的 中使用此方法addItem,以便您的 addItem 将自动延迟,直到后端 id 可用

// Create item, but do not attempt creation until we are sure to get a backend id
const backendListItemPromise = getListIdPromise(localListId,reduxState).then(backendListId => {
  return createBackendListItem(backendListId, itemData);
})

// Provide user optimistic feedback even if the item is not yet added to the list
dispatch(addListItemOptimistic());
backendListItemPromise.then(
  backendListItem => dispatch(addListItemCommit()),
  error => dispatch(addListItemRollback())
);

您可能想要清理 CreatedListIdPromiseCache,但它对于大多数应用程序来说可能不是很重要,除非您有非常严格的内存使用要求。


另一种选择是后端 id 是在前端计算的,类似于 UUID。您的后端只需要验证此 ID 的唯一性。因此,即使后端尚未回复,您也将始终拥有所有乐观创建的列表的有效后端 ID。