Dva: Como cancelar efeitos

Criado em 8 jun. 2018  ·  11Comentários  ·  Fonte: dvajs/dva

Código para reproduzir o problema: (forneça códigos ou etapas reproduzíveis)

Cenário: Se houver duas páginas A e B no projeto, correspondentes ao modelo A e ao modelo B, existem algumas solicitações de rede assíncronas em cada página, que são iniciadas em efeitos e os dados retornados são gravados no modelo atual por meio do redutor Estado.

Quando eu sair da página A ou da página B, desejo limpar os dados no modelo correspondente para não se tornarem dados sujos quando eu os reutilizar na próxima vez.
Eu escrevi um redutor claro no modelo, no método componentWillUnmount
despachar ({type: $ {model.namespace} / clear})

Comportamento esperado: (efeito normal esperado)

O usuário entra na página B da página A e os dados no modelo A são limpos e restaurados ao estado inicial.

Comportamento real: (efeito real)

Quando o usuário entra na página B da página A, se uma solicitação de rede assíncrona ocorre na página A e entra na página B antes que a solicitação de rede seja concluída, a página A primeiro limpará os dados do modelo A por meio do redutor de limpeza, mas quando a rede estiver funcionando Quando a solicitação for concluída, os dados retornados serão reescritos no modelo A, resultando em dados sujos do negócio anterior no modelo A.

Posso cancelar os efeitos especificados ao sair da página A ou cancelar todos os efeitos em um namespace? (Semelhante a cancelar na saga redux)

Versões dos pacotes usados: (qual versão de qual biblioteca apresenta o problema)

v2.2.3

Comentários muito úteis

@ wss1942 Obrigado!
Desta forma, os efeitos podem ser cancelados. É mais problemático exibir o status de carregamento. Você não pode usar diretamente o loading.effects que vem com dva para exibir o status de carregamento. Você precisa escrever seu próprio código para alterá-lo.
Eu modifiquei e postei um código de modelo completo

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;
    },
  },
};


Ao sair da transação (componentWillUnmount), a limpeza de despacho pode cancelar todos os efeitos não concluídos no modelo atual e getProductLoading e getCityLoading também podem ser usados ​​normalmente.

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

Todos 11 comentários

Você pode monitorar as mudanças de roteamento nas assinaturas do modelo e limpar manualmente o estado do modelo A quando a página for uma página não A (saia da página A) ou página A (insira a página A).

Obrigado por responder. Eu tentei esse método. Não há problema em limpar o próprio estado. Ele pode ser limpo em componentWillUnmount ou quando as assinaturas monitoram alterações de roteamento. O problema é que, após limpar o estado modal, os dados retornados pela operação assíncrona nos efeitos Ele ainda será reescrito no estado modal, resultando em dados sujos do negócio anterior.
Eu até monitoro as mudanças de roteamento nas assinaturas. Quando saio da página A, uso o unmodel para desinstalar o modelo A. É inútil entrar na página A e carregar manualmente o modelo A. Se a comunicação assíncrona voltar, desde que o modelo A seja encontrado, os dados serão gravados, independentemente de este modelo ser o último usado pela empresa ou do modelo recém-carregado.
Portanto, a melhor maneira é limpar o estado no modelo e cancelar todos os efeitos no namespace atual, mas nenhum método a ser chamado foi encontrado. Procure conselhos.

Encontrei o problema do autor da postagem original. Eu queria cancelar o efeito inacabado quando o componente foi desinstalado, mas descobri que não havia como. Dva não parece fornecer esse tipo de api @sorrycc

É possível cancelar os efeitos e separar as sagas

@dlamon encontrou a mesma situação e queria limpar quando a rota foi trocada. Existe uma solução agora?

Não se espera que @KyrieChen limpe durante a troca de rota, porque depois que a troca de rota for concluída, a comunicação pode não voltar e os dados sujos ainda serão gravados de volta na área de dados do modelo limpa após a comunicação voltar.

Minha abordagem atual é gerar uma área de sub-dados com UUID na área de dados do modelo correspondente à transação atual toda vez que eu entrar em uma transação (quando componentWillMount), que é usada para armazenar os dados usados ​​na transação atual. UUID é escrito e limpo quando a transação termina (componentWillUnmount). Semelhante à estrutura do modelo abaixo:
2018-11-07 7 25 31
Se a comunicação voltar após o modelo claro, ao gravar dados, uma vez que o UUID utilizado pelo redutor atual é o UUID utilizado na transação anterior, os dados sujos serão gravados na área de dados da transação anterior, e serão não será gerado para a próxima transação.

Mas esse método também não é bom: primeiro, ele aumenta a complexidade lógica e o segundo é que a obtenção do valor inicial requer a adição de um julgamento de valor nulo, o que aumenta a complexidade do código. Como o que eu faço é principalmente para o sistema financeiro, tenho medo desse tipo de dado sujo, então escolho esse método.
Eu acho que a melhor maneira é semelhante ao mecanismo de cancelar efeitos na saga redux, mas dva não parece suportá-lo agora.
Se você tiver uma solução melhor, @ 我

@dlamon Eu também encontrei. Eu mesmo escrevi uma demonstração e me sinto um pouco problemático https://codesandbox.io/s/yqwqpmvwvj

Cancelar um namespace a products a model no método effect :
dispatch({ type: 'products/@<strong i="10">@CANCEL_EFFECTS</strong>' });

É este código

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

@ wss1942 Obrigado por responder a este método. Eu tentei. Se dispatch ({type: 'products / @ @CANCEL_EFFECTS '}) fará com que o efeito no modal correspondente seja inválido. Semelhante a # 796

@dlamon dva não parece fornecer uma API para limpar um efeito. No entanto, várias sagas podem ser escritas no efeito definido no modelo.

 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 é a sua tarefa assíncrona, você pode iniciar a tarefa com a ação: iniciar e cancelar a tarefa com a
Além disso, se a tarefa assíncrona for uma solicitação de rede, também pode ser necessária uma operação para cancelar a solicitação de rede. Por exemplo, axios podem ser cancelados com axios.CancelToken.

@ wss1942 Obrigado!
Desta forma, os efeitos podem ser cancelados. É mais problemático exibir o status de carregamento. Você não pode usar diretamente o loading.effects que vem com dva para exibir o status de carregamento. Você precisa escrever seu próprio código para alterá-lo.
Eu modifiquei e postei um código de modelo completo

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;
    },
  },
};


Ao sair da transação (componentWillUnmount), a limpeza de despacho pode cancelar todos os efeitos não concluídos no modelo atual e getProductLoading e getCityLoading também podem ser usados ​​normalmente.

  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'],
    };
  }
Esta página foi útil?
0 / 5 - 0 avaliações