【问题标题】:createAsyncThunk: abort previous requestcreateAsyncThunk:中止先前的请求
【发布时间】:2020-11-03 11:12:58
【问题描述】:

我正在使用createAsyncThunk 向某些 API 发出异步请求。在任何给定时刻,只有一个请求应该处于活动状态。 我知道,如果我从上一次调用中返回了 Promise,则可以使用提供的 AbortSignal 中止请求。问题是,thunk 本身能否以某种方式“自动”中止先前的请求? 我正在考虑两种选择:

  • 保持状态中的最后一个 AbortSignal。似乎是错误的,因为状态应该是可序列化的。
  • 将最后一个 Promise 及其 AbortSignal 保存在全局变量中。似乎也是错误的,因为,你知道,全局变量。

有什么想法吗?谢谢。

【问题讨论】:

  • 它不会中止请求,您可以在承诺上调用 abort ,就像调度 thunk 返回一样,但它不会中止 xhr 请求。我不确定您所说的“只有一个活动”是什么意思,这是否意味着您只想写声明解决的请求是否是最后一个?
  • @HMR:对信号调用 abort 将调用安装在该信号上的事件侦听器。 API 将在该事件上安装自己的侦听器并采取相应措施(顺便说一句,在我的情况下,它不是 HTTP API,它是一个视频播放器;但也可以通过 AbortSignal 中止 XHR 请求 - fetch 支持这一点)。 “只有一个活动”是指只有最后一个请求的结果应该写入状态,而且之前的调用应该接收到 abort 事件并停止做他们正在做的事情。

标签: javascript redux redux-toolkit


【解决方案1】:

我不知道您的特定 api 是如何工作的,但下面是一个工作示例,说明如何将中止逻辑放入 action 和 reducer,当进行更新的提取时,它将中止任何先前活动的假提取:

import * as React from 'react';
import ReactDOM from 'react-dom';
import {
  createStore,
  applyMiddleware,
  compose,
} from 'redux';
import {
  Provider,
  useDispatch,
  useSelector,
} from 'react-redux';
import {
  createAsyncThunk,
  createSlice,
} from '@reduxjs/toolkit';

const initialState = {
  entities: [],
};
// constant value to reject with if aborted
const ABORT = 'ABORT';
// fake signal constructor
function Signal() {
  this.listener = () => undefined;
  this.abort = function () {
    this.listener();
  };
}
const fakeFetch = (signal, result, time) =>
  new Promise((resolve, reject) => {
    const timer = setTimeout(() => resolve(result), time);
    signal.listener = () => {
      clearTimeout(timer);
      reject(ABORT);
    };
  });
// will abort previous active request if there is one
const latest = (fn) => {
  let previous = false;
  return (signal, result, time) => {
    if (previous) {
      previous.abort();
    }
    previous = signal;
    return fn(signal, result, time).finally(() => {
      //reset previous
      previous = false;
    });
  };
};
// fake fetch that will abort previous active is there is one
const latestFakeFetch = latest(fakeFetch);

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async ({ id, time }) => {
    const response = await latestFakeFetch(
      new Signal(),
      id,
      time
    );
    return response;
  }
);
const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  reducers: {},
  extraReducers: {
    [fetchUserById.fulfilled]: (state, action) => {
      state.entities.push(action.payload);
    },
    [fetchUserById.rejected]: (state, action) => {
      if (action?.error?.message === ABORT) {
        //do nothing
      }
    },
  },
});

const reducer = usersSlice.reducer;
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(
      ({ dispatch, getState }) => (next) => (action) =>
        typeof action === 'function'
          ? action(dispatch, getState)
          : next(action)
    )
  )
);
const App = () => {
  const dispatch = useDispatch();
  React.useEffect(() => {
    //this will be aborted as soon as the next request is made
    dispatch(
      fetchUserById({ id: 'will abort', time: 200 })
    );
    dispatch(fetchUserById({ id: 'ok', time: 100 }));
  }, [dispatch]);
  return 'hello';
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

如果您只需要在它是最新请求的 Promise 时解决一个 Promise,而无需中止或取消正在进行的 Promise(如果它不是最新的则忽略 Resolve),那么您可以执行以下操作:

const REPLACED_BY_NEWER = 'REPLACED_BY_NEWER';
const resolveLatest = (fn) => {
  const shared = {};
  return (...args) => {
    //set shared.current to a unique object reference
    const current = {};
    shared.current = current;
    fn(...args).then((resolve) => {
      //see if object reference has changed
      //  if so it was replaced by a newer one
      if (shared.current !== current) {
        return Promise.reject(REPLACED_BY_NEWER);
      }
      return resolve;
    });
  };
};

this 回答中演示了它的使用方式

【讨论】:

  • 它需要更多的工作来等待之前的承诺真正完成(这通常不需要,但在我的情况下是必要的)。但这是个好主意,谢谢。|对于好奇的读者:主要部分是“最新”功能。它从原始有效负载创建者创建一个新函数(包装器)并将其返回。包装器将其信号存储在“上一个”变量中,该变量在“最新”函数内部是局部的,因此每个包装器都有自己的变量。要使用它,请包装您的有效负载创建者并将其传递给 createAsyncThunk。
  • @ondrej.par 我用一个包装器更新了答案,它可以让所有承诺解决,但如果它不是一组活动请求中的最新请求,则拒绝它们。
  • 不需要在中止时手动拒绝,因为 redux-toolkit 在中止信号时会自动执行此操作。
  • 请注意,如果您尝试多次使用latestFakeFetch(针对不同的资源),这将不起作用。相反,您需要为每个独立资源调用一次latest()
  • 我编辑了你的答案有两个原因:1)redux-toolkit 已经提供了 AbortSignal 实例,所以应该使用一个 2)在完成时,previous 只有在它仍然是我们的信号时才应该被清除(它可能已被更新的替换)。
【解决方案2】:

根据@HMR 的回答,我能够把它放在一起,但它相当复杂。

以下函数创建执行实际工作的“内部”异步 thunk,以及委托给内部的异步 thunk 并中止先前的调度(如果有)。

内部 thunk 的有效负载创建者也被包装为:1)等待有效负载创建者的先前调用完成,2)如果操作在等待时中止,则跳过调用真正的有效负载创建者(以及 API 调用) .

import { createAsyncThunk, AsyncThunk, AsyncThunkPayloadCreator, unwrapResult } from '@reduxjs/toolkit';

export function createNonConcurrentAsyncThunk<Returned, ThunkArg>(
  typePrefix: string,
  payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg>,
  options?: Parameters<typeof createAsyncThunk>[2]
): AsyncThunk<Returned, ThunkArg, unknown> {
  let pending: {
    payloadPromise?: Promise<unknown>;
    actionAbort?: () => void;
  } = {};

  const wrappedPayloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg> = (arg, thunkAPI) => {
    const run = () => {
      if (thunkAPI.signal.aborted) {
        return thunkAPI.rejectWithValue({name: 'AbortError', message: 'Aborted'});
      }
      const promise = Promise.resolve(payloadCreator(arg, thunkAPI)).finally(() => {
        if (pending.payloadPromise === promise) {
          pending.payloadPromise = null;
        }
      });
      return pending.payloadPromise = promise;
    }

    if (pending.payloadPromise) {
      return pending.payloadPromise = pending.payloadPromise.then(run, run); // don't use finally(), replace result
    } else {
      return run();
    }
  };

  const internalThunk = createAsyncThunk(typePrefix + '-protected', wrappedPayloadCreator);

  return createAsyncThunk<Returned, ThunkArg>(
    typePrefix,
    async (arg, thunkAPI) => {
      if (pending.actionAbort) {
        pending.actionAbort();
      }
      const internalPromise = thunkAPI.dispatch(internalThunk(arg));
      const abort = internalPromise.abort;
      pending.actionAbort = abort;
      return internalPromise
        .then(unwrapResult)
        .finally(() => {
          if (pending.actionAbort === abort) {
            pending.actionAbort = null;
          }
        });
    },
    options
  );
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-08
    • 1970-01-01
    • 2012-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多