Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何cancel effects #1749

Closed
dlamon opened this issue Jun 8, 2018 · 11 comments
Closed

如何cancel effects #1749

dlamon opened this issue Jun 8, 2018 · 11 comments

Comments

@dlamon
Copy link

dlamon commented Jun 8, 2018

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

@tsejx
Copy link

tsejx commented Jun 8, 2018

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

@dlamon
Copy link
Author

dlamon commented Jun 8, 2018

谢谢回答。这个方法我已经试过了,清除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,但是没有找到调用的方法。求指点。

@xujie-phper
Copy link

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

@godot007
Copy link

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

@KyrieChen
Copy link

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

@dlamon
Copy link
Author

dlamon commented Nov 7, 2018

@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现在好像没有支持。
如果你有更好的解决办法,请@我

@chengzibaba
Copy link

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

@wss1942
Copy link

wss1942 commented Dec 30, 2018

取消一个namespaceproductsmodeleffect的方法:
dispatch({ type: 'products/@@CANCEL_EFFECTS' });

就是这段代码

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

@dlamon
Copy link
Author

dlamon commented Dec 30, 2018

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

@wss1942
Copy link

wss1942 commented Dec 31, 2018

@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取消。

@dlamon
Copy link
Author

dlamon commented Jan 8, 2019

@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'],
    };
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants