Dva: 如何cancel effects

创建于 2018-06-08  ·  11评论  ·  资料来源: dvajs/dva

Code to reproduce the issue: (请提供可复现的代码或者步骤)

场景:如果项目中存在两个页面A和B,分别对应model A和model B,在每个页面中都存在一些异步的网络请求, 在effects中发起并将返回数据通过reducer写入到当前model的state中。

在离开页面A或页面B时,我希望清空对应model中的数据,以免成为下次重新使用时的脏数据。
我在model 里编写了 clear 的 reducer,在componentWillUnmount方法中
dispatch({ type:${model.namespace}/clear})

Expected behavior: (预期的正常效果)

用户从A页面进入B页面,model A中的数据被清理,恢复到初始状态。

Actual behavior: (实际效果)

用户从A页面进入B页面时,如果A页面中发生了异步网络请求,在网络请求还没有完成时进入B页面,A页面会首先通过clear的reducer清除掉model A数据,但是当effects中的网络请求完成时,会将返回数据重新写入model A,导致model A中存在上一次业务的脏数据。

请问是否能够在离开A页面时cancel指定的effects,或者cancel掉一个namespace中所有的effects。(类似redux-saga中的cancel)

Versions of packages used: (哪个库的哪个版本出现的问题)

v2.2.3

最有用的评论

@wss1942 感谢!
这种方式可以实现取消effects,在显示loading状态的时候要麻烦一些,不能直接使用dva自带的loading.effects来显示loading状态,需要自己手工编写代码来变更。
我修改了下,贴一个完整的model代码

import { getProduct } from '@/services/Products';
import { isRespSucc, showErrorMsg } from '@/utils/utils';

const initState = {};

export default {
  namespace: 'product',

  state: initState,

  effects: {
    /**
      在两个 Effects 之间触发一个竞赛(race)
      如果task先结束,竞赛结束。
      如果task未结束时收到cancel,race effect 将自动取消 task。
    */
    *cancelable({ task, payload }, { call, race, take }) {
      yield race({
        task: call(task, payload),
        cancel: take('cancel'),
      });
    },

    /**
      取消所有未完成的任务,并执行数据清理
    */
    *clear(_, { put }) {
      yield put.resolve({ type: 'cancel' });
      yield put({ type: 'clearState' });
    },

    *getProduct({ payload }, { call, put, cancelled }) {
      // eslint-disable-next-line
      yield put.resolve({ type: 'cancelable', task: getProductCancelable, payload });

      function* getProductCancelable(params) {
        try {
          // 调用网络请求
          const response = yield call(getProduct, params);
          // 返回结果判断
          if (!isRespSucc(response)) {
            showErrorMsg(response);
            return;
          }
          // 取值
          const { productName } = response.data;
          // 调用reducer存值
          yield put({
            type: 'saveState',
            payload: { productName },
          });
        } finally {
          if (yield cancelled()) {
            // TODO: 取消网络请求
          }
        }
      }
    },
    *getCity(_, { call, put, cancelled }) {
      // eslint-disable-next-line
      yield put.resolve({ type: 'cancelable', task: getCityCancelable });

      function* getCityCancelable() {
        // TODO: 具体实现
      }
  },

  reducers: {
    saveState(state, { payload }) {
      const newState = { ...state, ...payload };
      return newState;
    },
    clearState() {
      return initState;
    },
  },
};


在离开交易时(componentWillUnmount),dispatch clear就可以取消当前model中所有未完成的effects,getProductLoading和getCityLoading也可以正常使用。

  componentWillMount() {
    const { dispatch } = this.props;
    dispatch({
      type: 'product/getProduct',
      payload: {
        productNumber: '123456',
      },
    });
    dispatch({
      type: 'product/getCity',
    });
  }

  componentWillUnmount() {
    const { dispatch } = this.props;
    dispatch({
      type: 'product/clear',
    });
  }

  function mapStateToProps(state) {
    return {
      getProductLoading: state.loading.effects['product/getProduct'],
      getCityLoading: state.loading.effects['product/getCity'],
    };
  }

所有11条评论

你可以在 model 的 subscriptions 监听路由变化,当页面为非A页面(离开A页面)或A页面(进入A页面)时手动清除 model A 的state 状态。

谢谢回答。这个方法我已经试过了,清除state状态本身没有什么问题,无论是在componentWillUnmount时清除还是在subscriptions监听路由变化时清除都可以,问题是清除了modal 的state后,effects中的异步操作返回的数据还是会重新写入modal的state中,导致上一次业务的脏数据。
我什至在subscriptions监听路由变化,离开A页面时使用unmodel卸载掉model A,进入A页面再手动加载model A都没用,如果异步通信回来时,只要发现存在model A,都会把数据写进入,不管这个model是上一次业务使用的model还是新加载的model。
所以最好的办法是清除model中state的同时cancel掉当前namespace中所有的effects,但是没有找到调用的方法。求指点。

楼主的问题,我也遇到过,想在组件卸载时取消掉未完成的effect,但是发现没有办法,dva好像没有提供这类api啊@sorrycc

是否可以取消effects,将sagas独立出来

@dlamon 遇到的同样的情况,想在路由切换的时候清理。请问现在有解决方法吗?

@KyrieChen 在路由切换时清理估计也不行,因为路由切换完成后,通讯有可能还没回来,通讯回来后仍然会把脏数据写回到清理后的model数据区。

我现在的做法是每次进入交易时(componentWillMount时),在当前交易对应的model数据区中生成一个带UUID的子数据区,用于存储当次交易使用的数据,在reducer写入数据时带UUID写入,在交易退出(componentWillUnmount)时清理(clear)。类似于下面的model结构:
2018-11-07 7 25 31
如果通讯在clear model后才回来,写入数据时由于当前reducer使用的UUID是上一次交易使用的UUID,则会把脏数据写入到上一个交易的数据区中,不会对下次交易产生影响。

但是这种方式也不算好,第一增加了逻辑复杂性,第二是获取初始值需要增加空值判断,增加了代码复杂性。由于我做的主要是针对金融系统,很怕这种脏数据,才选择这种方式。
我觉得最好的方式还是类似于redux-saga中cancel effects机制,但是dva现在好像没有支持。
如果你有更好的解决办法,请@我

@dlamon 我也碰到了,自己写了个demo,感觉有点问题 https://codesandbox.io/s/yqwqpmvwvj

取消一个namespaceproductsmodeleffect的方法:
dispatch({ type: 'products/@<strong i="10">@CANCEL_EFFECTS</strong>' });

就是这段代码

        yield sagaEffects.fork(function*() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });

@wss1942 感谢回复
这个方法我试过,如果dispatch({ type: 'products/@@CANCEL_EFFECTS' })会导致对应modal里面的effect无效。类似于#796

@dlamon dva貌似没有提供清除某个effect的api。但是model中定义的effect中可以写多个saga。

 namespace: 'products',
 effects: {
  *start(){},
  *stop(){},
  watchLogin: [
      function* ({ take, put, call, cancel, fork, cancelled }) {
          yield take('start');
          const timerTask = yield fork(timer)
          const bgSyncTask = yield fork(bgSync)
          yield take('stop')
          yield cancel(bgSyncTask)
          yield cancel(timerTask)

        function* bgSync() {
          try {
            while (true) {
              const result = yield call(delay, 5 * 1000);
              yield put({ type: 'stop' })
            }
          } finally {
            if (yield cancelled())
              yield put({ type: 'log', payload: 'fetch🛑' })
          }
        }
        function* timer(time) {
          let i=0;
          while (true) {
            yield put({ type: 'log', payload: i++ })
            yield delay(1000)
          }
        }
      },
      { type: 'watcher' },
    ],
}

bgSync是你的异步任务,可以action:start开始任务action:stop取消任务。这能实现cancel某个effect。
另外,异步任务如果是网络请求,可能还需要一个取消网络请求的操作,比如axios可以用axios.CancelToken取消。

@wss1942 感谢!
这种方式可以实现取消effects,在显示loading状态的时候要麻烦一些,不能直接使用dva自带的loading.effects来显示loading状态,需要自己手工编写代码来变更。
我修改了下,贴一个完整的model代码

import { getProduct } from '@/services/Products';
import { isRespSucc, showErrorMsg } from '@/utils/utils';

const initState = {};

export default {
  namespace: 'product',

  state: initState,

  effects: {
    /**
      在两个 Effects 之间触发一个竞赛(race)
      如果task先结束,竞赛结束。
      如果task未结束时收到cancel,race effect 将自动取消 task。
    */
    *cancelable({ task, payload }, { call, race, take }) {
      yield race({
        task: call(task, payload),
        cancel: take('cancel'),
      });
    },

    /**
      取消所有未完成的任务,并执行数据清理
    */
    *clear(_, { put }) {
      yield put.resolve({ type: 'cancel' });
      yield put({ type: 'clearState' });
    },

    *getProduct({ payload }, { call, put, cancelled }) {
      // eslint-disable-next-line
      yield put.resolve({ type: 'cancelable', task: getProductCancelable, payload });

      function* getProductCancelable(params) {
        try {
          // 调用网络请求
          const response = yield call(getProduct, params);
          // 返回结果判断
          if (!isRespSucc(response)) {
            showErrorMsg(response);
            return;
          }
          // 取值
          const { productName } = response.data;
          // 调用reducer存值
          yield put({
            type: 'saveState',
            payload: { productName },
          });
        } finally {
          if (yield cancelled()) {
            // TODO: 取消网络请求
          }
        }
      }
    },
    *getCity(_, { call, put, cancelled }) {
      // eslint-disable-next-line
      yield put.resolve({ type: 'cancelable', task: getCityCancelable });

      function* getCityCancelable() {
        // TODO: 具体实现
      }
  },

  reducers: {
    saveState(state, { payload }) {
      const newState = { ...state, ...payload };
      return newState;
    },
    clearState() {
      return initState;
    },
  },
};


在离开交易时(componentWillUnmount),dispatch clear就可以取消当前model中所有未完成的effects,getProductLoading和getCityLoading也可以正常使用。

  componentWillMount() {
    const { dispatch } = this.props;
    dispatch({
      type: 'product/getProduct',
      payload: {
        productNumber: '123456',
      },
    });
    dispatch({
      type: 'product/getCity',
    });
  }

  componentWillUnmount() {
    const { dispatch } = this.props;
    dispatch({
      type: 'product/clear',
    });
  }

  function mapStateToProps(state) {
    return {
      getProductLoading: state.loading.effects['product/getProduct'],
      getCityLoading: state.loading.effects['product/getCity'],
    };
  }
此页面是否有帮助?
0 / 5 - 0 等级