首页 文章

Redux中的排队操作

提问于
浏览
29

我已经看过各种各样的中间件,比如redux-promise,如果你知道连续的动作是在根源(缺乏一个更好的术语)被触发的行为,这似乎没什么问题 .

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

我将尝试给出一些用例和实现的背景信息 .

用例

假设您要创建一些列表并将其保留在服务器上 . 在列表创建时,服务器使用该列表的id进行响应,该ID用于与列表相关的后续API endpoints :

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点进行乐观更新,以增强用户体验 - 没有人喜欢看微调器 . 因此,当您创建列表时,会立即显示新列表,并在添加项目时添加选项:

+-------------+----------+
|  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函数调用一个fetch 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中这样做 .

任何帮助表示赞赏 .

5 回答

  • 0

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

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

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

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

    所有归功于丹阿布拉莫夫

  • 0

    我有完美的工具,可以满足您的需求 . 当你需要对redux进行大量控制时,(特别是任何异步)并且你需要按顺序执行redux动作,没有比Redux Sagas更好的工具了 . 它 Build 在es6生成器之上,为您提供了很多控制,因为从某种意义上说,您可以在某些点暂停代码 .

    您描述的 action queue 是所谓的 saga . 现在,因为它是为了与redux一起工作而创建的,所以可以通过在组件中调度来触发这些传奇 .

    由于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 时,它将停在该行并等待 . 在你告诉它之前,它不会继续下一行 . 所以看看 yield take('LOGIN_REQUEST') 的位置 . 该传奇将在此时产生或等待,直到你发送'LOGIN_REQUEST',之后传奇将调用授权方法,直到下一个收益 . 下一个方法是异步 yield call(Api.storeItem, {token}) ,因此在代码解析之前它不会转到下一行 .

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

    到目前为止,Redux Sagas是我最喜欢使用Redux的工具之一 . 它为您提供了对应用程序的大量控制,任何阅读代码的人都会感谢您,因为现在所有内容都会一次读取一行 .

  • 1

    您不必处理排队操作 . 它将隐藏数据流,它将使您的应用程序更加繁琐,无法进行调试 .

    我建议您在创建列表或项目时使用一些临时ID,然后在实际从商店收到实际ID时更新这些ID .

    这样的事可能吗? (不测试,但你得到的ID):

    EDIT :我一开始并不理解保存列表时需要自动保存的项目 . 我编辑了 createList 动作创建者 .

    /* REDUCERS & ACTIONS */
    
    // this "thunk" action creator is responsible for :
    //   - creating the temporary list item in the store with some 
    //     generated unique id
    //   - dispatching the action to tell the store that a temporary list
    //     has been created (optimistic update)
    //   - triggering a POST request to save the list in the database
    //   - dispatching an action to tell the store the list is correctly
    //     saved
    //   - triggering a POST request for saving items related to the old
    //     list id and triggering the correspondant receiveCreatedItem
    //     action
    const createList = (name) => {
    
      const tempList = {
        id: uniqueId(),
        name
      }
    
      return (dispatch, getState) => {
        dispatch(tempListCreated(tempList))
        FakeListAPI
          .post(tempList)
          .then(list => {
            dispatch(receiveCreatedList(tempList.id, list))
    
            // when the list is saved we can now safely
            // save the related items since the API
            // certainly need a real list ID to correctly
            // save an item
            const itemsToSave = getState().items.filter(item => item.listId === tempList.id)
            for (let tempItem of itemsToSave) {
              FakeListItemAPI
                .post(tempItem)
                .then(item => dispatch(receiveCreatedItem(tempItem.id, item)))
            }
          )
      }
    
    }
    
    const tempListCreated = (list) => ({
      type: 'TEMP_LIST_CREATED',
      payload: {
        list
      }
    })
    
    const receiveCreatedList = (oldId, list) => ({
      type: 'RECEIVE_CREATED_LIST',
      payload: {
        list
      },
      meta: {
        oldId
      }
    })
    
    
    const createItem = (name, listId) => {
    
      const tempItem = {
        id: uniqueId(),
        name,
        listId
      }
    
      return (dispatch) => {
        dispatch(tempItemCreated(tempItem))
      }
    
    }
    
    const tempItemCreated = (item) => ({
      type: 'TEMP_ITEM_CREATED',
      payload: {
        item
      }
    })
    
    const receiveCreatedItem = (oldId, item) => ({
      type: 'RECEIVE_CREATED_ITEM',
      payload: {
        item
      },
      meta: {
        oldId
      }
    })
    
    /* given this state shape :
    state = {
      lists: {
        ids: [ 'list1ID', 'list2ID' ],
        byId: {
          'list1ID': {
            id: 'list1ID',
            name: 'list1'
          },
          'list2ID': {
            id: 'list2ID',
            name: 'list2'
          },
        }
        ...
      },
      items: {
        ids: [ 'item1ID','item2ID' ],
        byId: {
          'item1ID': {
            id: 'item1ID',
            name: 'item1',
            listID: 'list1ID'
          },
          'item2ID': {
            id: 'item2ID',
            name: 'item2',
            listID: 'list2ID'
          }
        }
      }
    }
    */
    
    // Here i'm using a immediately invoked function just 
    // to isolate ids and byId variable to avoid duplicate
    // declaration issue since we need them for both
    // lists and items reducers
    const lists = (() => {
      const ids = (ids = [], action = {}) => ({
        switch (action.type) {
          // when receiving the temporary list
          // we need to add the temporary id 
          // in the ids list
          case 'TEMP_LIST_CREATED':
            return [...ids, action.payload.list.id]
    
          // when receiving the real list
          // we need to remove the old temporary id
          // and add the real id instead
          case 'RECEIVE_CREATED_LIST':
            return ids
              .filter(id => id !== action.meta.oldId)
              .concat([action.payload.list.id])
          default:
            return ids
        }
      })
    
      const byId = (byId = {}, action = {}) => ({
        switch (action.type) {
          // same as above, when the the temp list
          // gets created we store it indexed by
          // its temp id
          case 'TEMP_LIST_CREATED':
            return {
              ...byId,
              [action.payload.list.id]: action.payload.list
            }
    
          // when we receive the real list we first
          // need to remove the old one before
          // adding the real list
          case 'RECEIVE_CREATED_LIST': {
            const {
              [action.meta.oldId]: oldList,
              ...otherLists
            } = byId
            return {
              ...otherLists,
              [action.payload.list.id]: action.payload.list
            }
          }
    
        }
      })
    
      return combineReducers({
        ids,
        byId
      })
    })()
    
    const items = (() => {
      const ids = (ids = [], action = {}) => ({
        switch (action.type) {
          case 'TEMP_ITEM_CREATED':
            return [...ids, action.payload.item.id]
          case 'RECEIVE_CREATED_ITEM':
            return ids
              .filter(id => id !== action.meta.oldId)
              .concat([action.payload.item.id])
          default:
            return ids
        }
      })
    
      const byId = (byId = {}, action = {}) => ({
        switch (action.type) {
          case 'TEMP_ITEM_CREATED':
            return {
              ...byId,
              [action.payload.item.id]: action.payload.item
            }
          case 'RECEIVE_CREATED_ITEM': {
            const {
              [action.meta.oldId]: oldList,
              ...otherItems
            } = byId
            return {
              ...otherItems,
              [action.payload.item.id]: action.payload.item
            }
          }
    
          // when we receive a real list
          // we need to reappropriate all
          // the items that are referring to
          // the old listId to the new one
          case 'RECEIVE_CREATED_LIST': {
            const oldListId = action.meta.oldId
            const newListId = action.payload.list.id
            const _byId = {}
            for (let id of Object.keys(byId)) {
              let item = byId[id]
              _byId[id] = {
                ...item,
                listId: item.listId === oldListId ? newListId : item.listId
              }
            }
            return _byId
          }
    
        }
      })
    
      return combineReducers({
        ids,
        byId
      })
    })()
    
    const reducer = combineReducers({
      lists,
      items
    })
    
    /* REDUCERS & ACTIONS */
    
  • 1

    这就是我要解决这个问题的方法:

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

    在创建列表时,将后端标识的承诺添加到缓存

    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 .

  • 0

    我遇到了类似的问题 . 我需要一个队列来保证将乐观操作提交或最终提交(如果出现网络问题),并按照相同的顺序创建远程服务器,如果不可能则回滚 . 我发现仅使用Redux,因为我认为它不是为此而设计的,并且只使用promises这样做可能真的是一个难以理解的问题,除了你需要以某种方式管理你的队列状态这一事实 . .. 恕我直言 .

    我认为@Pcriulan 's suggestion on using redux-saga was a good one. At first sight, redux-saga doesn' t提供任何帮助,直到你到channels . 这为您打开了一扇门来处理其他语言所做的其他语言的并发性,特别是CSP(例如,参见Go或Clojure的async),感谢JS生成器 . 甚至有questions为什么以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);
          }
    
        }
      }
    }
    

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

    希望能帮助到你!

相关问题