dvajs学习笔记- effects API

7,624 阅读5分钟

最近在学习dva,但发现官方文档有点滞后,所以自己记录下笔记,如有笔误或者理解不当的地方,还请指正😊

dva是一个基于 redux 和 redux-saga 的数据流方案,其中effects是Action 处理器,处理异步动作,基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程,计算以外的操作都属于 Effect,典型的就是 I/O 操作、数据库读写

在本model中调用action是不用加namespace,如果加上甚至会抛出警告,跨model调用才要加上namespace区分

  • call 阻塞 用于调用异步逻辑,支持 promise

    const result = yield call(fetch, payload);
    
  • put 不阻塞 用于触发 action,一般来触发reducer改变state

    yield put({ type: 'todos/add', payload: 'Learn Dva' });
    
  • put.resolve 阻塞 功能与put一样,区别是put.resolve是阻塞的,执行完才进行下一步

  • select 不阻塞 用于从 state 里获取数据

    const { args } = yield select(state => state[namespace]);
    
  • take 阻塞 dva封装了take,可以监听action的开始和结束阶段,take会阻塞到监听的事件触发,才执行下一步

    it('take', done => {
    const app = create();
    app.model({
      namespace: 'count',
      state: 0,
      reducers: {
        add(state, { payload }) {
          return state + payload || 1;
        },
      },
      effects: {
        *addDelay({ payload }, { put, call }) {
          yield call(delay, payload.delay || 100);
          yield put({ type: 'add', payload: payload.amount });
        },
        *test(action, { put, select, take }) {
          yield put({ type: 'addDelay', payload: { amount: 2 } });
          // @@start 是监听put操作的第一步,即payload与state还没合并,这时state为0
          //yield take('addDelay/@@start');
          //@@end 是监听put操作的最后一步,即将payload与state合并,返回新的state,这时state为2
          yield take('addDelay/@@end');
          const count = yield select(state => state.count);
          yield put({ type: 'addDelay', payload: { amount: count, delay: 0 } });
        },
      },
    });
    app.start();
    app._store.dispatch({ type: 'count/test' });
    setTimeout(() => {
      expect(app._store.getState().count).toEqual(4);
      done();
    }, 300);
    });
    

    可以监听action数组,满足一个即可

    it('take with array of actions', () => {
    const app = create();
    let takenCount = 0;
    app.model({
      namespace: 'count',
      state: null,
      reducers: {
        addRequest() {
          return 1;
        },
        addFailure() {
          return -1;
        },
        addSuccess() {
          return 0;
        },
      },
      effects: {
        *add(action, { put }) {
          yield put({ type: 'addRequest' });
          if (action.amount > 0.5) {
            yield put({ type: 'addSuccess' });
          } else {
            yield put({ type: 'addFailure' });
          }
        },
        *test(action, { put, take }) {
          yield put({ type: 'add', amount: action.amount });
          yield take(['addSuccess', 'addFailure']);
          takenCount += 1;
        },
      },
    });
    app.start();
    app._store.dispatch({ type: 'count/test', amount: 0 });
    expect(app._store.getState().count).toEqual(-1);
    app._store.dispatch({ type: 'count/test', amount: 1 });
    expect(app._store.getState().count).toEqual(0);
    expect(takenCount).toEqual(2);
    });
    

    可以监听其他model的action,必须要加对应的namespace

  • takeLatest 不阻塞 使用 takeLatest 来启动一个新的 action。 由于 takeLatest 取消了所有之前启动且未完成的任务,即使用户以极快的速度连续多次触发 action,我们都只会以最后的一个结束。

    it('type: takeLatest', done => {
    const app = create();
    app.model({
      namespace: 'count',
      state: 0,
      reducers: {
        add(state, { payload }) {
          return state + payload || 1;
        },
      },
      effects: {
        addDelay: [
          function*({ payload }, { call, put }) {
            yield call(delay, 100);
            yield put({ type: 'add', payload });
          },
          { type: 'takeLatest' }
        ],
      },
    });
    app.start();
    
    // Only catch the last one.
    app._store.dispatch({ type: 'count/addDelay', payload: 2 });
    app._store.dispatch({ type: 'count/addDelay', payload: 3 });
    
    setTimeout(() => {
      expect(app._store.getState().count).toEqual(3);
      done();
    }, 200);
    });
    
  • throttle 不阻塞 它在派生一次任务之后,仍然将新传入的 action 接收到底层的 buffer 中,至多保留(最近的)一个。但与此同时,它在 ms 毫秒内将暂停派生新的任务 —— 这也就是它被命名为节流阀(throttle)的原因。其用途,是在处理任务时,无视给定的时长内新传入的 action。

    it('type: throttle', done => {
    const app = create();
    app.model({
     namespace: 'count',
     state: 0,
     reducers: {
       add(state, { payload }) {
         return state + payload || 1;
       },
     },
     effects: {
       addDelay: [
         function*({ payload }, { call, put }) {
           yield call(delay, 100);
           yield put({ type: 'add', payload });
         },
         { type: 'throttle', ms: 100 }, // ms必须存在
       ],
     },
    });
    app.start();
    
    // Only catch the last one.
    app._store.dispatch({ type: 'count/addDelay', payload: 2 });
    app._store.dispatch({ type: 'count/addDelay', payload: 3 });
    
    setTimeout(() => {
     expect(app._store.getState().count).toEqual(2);
     done();
    }, 200);
    });
    
  • watcher 监听事件,不过只能执行一次

     it('type: watcher', done => {
    const watcher = { type: 'watcher' };
    const app = create();
    app.model({
      namespace: 'count',
      state: 0,
      reducers: {
        add(state, { payload }) {
          return state + payload || 1;
        },
      },
      effects: {
        addWatcher: [
          function*({ take, put, call }) {
            while (true) {
              const { payload } = yield take('addWatcher');
              yield call(delay, 100);
              yield put({ type: 'add', payload });
            }
          },
          watcher,
        ],
      },
    });
    app.start();
    
    // Only catch the first one.
    app._store.dispatch({ type: 'count/addWatcher', payload: 2 });
    app._store.dispatch({ type: 'count/addWatcher', payload: 3 });
    
    setTimeout(() => {
      expect(app._store.getState().count).toEqual(2);
      done();
    }, 200);
    });
    
  • poll
    轮询

     it('type: poll', done => {
    const app = create();
    app.model({
      namespace: 'count',
      state: 0,
      reducers: {
        add(state, { payload }) {
          return state + payload || 1;
        },
      },
      effects: {
        pollAdd: [
          function*(_, { put }) {
            yield put({ type: 'add', payload: 1 });
          },
          { type: 'poll', delay: 1000 },
        ],
      },
    });
    app.start();
    
    app._store.dispatch({ type: 'count/pollAdd-start' });
    
    setTimeout(() => {
      app._store.dispatch({ type: 'count/pollAdd-stop' });
      expect(app._store.getState().count).toEqual(2);
      done();
    }, 2000);
    });
    

    以上面例子来说=> { type: 'poll', delay: 1000 }来表示是轮询,delay表示间隔多少秒轮询一次。dispatch({ type: 'count/pollAdd-start' })表示轮询开始,dispatch({ type: 'count/pollAdd-stop' })表示轮询结束,但不会中断最后一次的请求。

    app._store.dispatch({ type: 'count/pollAdd-start' });
    // 即使紧接着取消轮询,但还是会执行一次
    app._store.dispatch({ type: 'count/pollAdd-stop' });
    

    dispatch({ type: 'count/pollAdd-start' }, payload: 2)可以携带参数,即从轮询开始到结束每次轮询都会传进去

    ...
    effects: {
        pollAdd: [
          function*({ payload }, { put }) {
            yield put({ type: 'add', payload });
          },
          { type: 'poll', delay: 1000 },
        ],
      },
    ...
    app._store.dispatch({ type: 'count/pollAdd-start', payload: 2 });
    
    setTimeout(() => {
      app._store.dispatch({ type: 'count/pollAdd-stop' });
      expect(app._store.getState().count).toEqual(4);
      done();
    }, 2000);
    

    轮询开始dispatch({ type: 'count/pollAdd-start' })后,只能dispatch({ type: 'count/pollAdd-stop' })结束轮询,才能开始新的一轮轮询。重复开始或者结束执行无效。

    ...
    state: 0,
      reducers: {
        add(state, { payload }) {
          return state + payload || 1;
        },
      },
     effects: {
        pollAdd: [
          function*({ payload }, { put }) {
            yield put({ type: 'add', payload });
          },
          { type: 'poll', delay: 1000 },
        ],
      }
      ...
    //1.
    app._store.dispatch({ type: 'count/pollAdd-start', payload: 2 });
    
    setTimeout(() => {
      // second start should not work
      app._store.dispatch({ type: 'count/pollAdd-start', payload: 3 });
      app._store.dispatch({ type: 'count/pollAdd-stop' });
      expect(app._store.getState().count).toEqual(6);
      done();
    }, 3000);
    //2.
    app._store.dispatch({ type: 'count/pollAdd-start', payload: 1 });
    // second start should not work
    app._store.dispatch({ type: 'count/pollAdd-start', payload: 1 });
    
    setTimeout(() => {
      app._store.dispatch({ type: 'count/pollAdd-stop' });
      expect(app._store.getState().count).toEqual(3);
      done();
    }, 3000);
    //3.
    app._store.dispatch({ type: 'count/pollAdd-start', payload: 1 });
    app._store.dispatch({ type: 'count/pollAdd-stop' });
    app._store.dispatch({ type: 'count/pollAdd-start', payload: 1 });
    
    setTimeout(() => {
      app._store.dispatch({ type: 'count/pollAdd-stop' });
      expect(app._store.getState().count).toEqual(3);
      done();
    }, 2000);
    });
    

    poll 源码:

    case 'poll':
      return function*() {
        function delay(timeout) {
          return new Promise(resolve => setTimeout(resolve, timeout));
        }
        function* pollSagaWorker(sagaEffects, action) {
          const { call } = sagaEffects;
          while (true) {
            yield call(sagaWithOnEffect, action);
            yield call(delay, delayMs);
          }
        }
        const { call, take, race } = sagaEffects;
        while (true) {
          const action = yield take(`${key}-start`);
          yield race([call(pollSagaWorker, sagaEffects, action), take(`${key}-stop`)]);
        }
      };
    

参考资料