dva分享

Posted by Eric on November 6, 2018

dva

  • Generator(async/await和promise)
  • redux-saga
  • 基础API
  • dva(umi)

Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

let hw = helloWorldGenerator();

hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }

Generator的特点:

  1. 分段执行,可以暂停
  2. 可以控制阶段和每个阶段的返回值
  3. 可以知道是否执行到结尾

利用Generator函数的暂停执行的效果,可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。==所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。==

async/await

ES7中加入了async/await,“终极”异步解决方案。不过async/await只是generator/promise的语法糖

用法:

  • async 表示这是一个async函数,await只能用在这个函数里面。
  • await 表示在这里等待异步操作返回结果,再继续执行。
  • await 后一般是一个promise对象,若不是则会用Promise.resolve()包一下

利用 Promise 编写的代码:

function logFetch(url) {
  return fetch(url)
    .then(response => response.text())
    .then(text => {
      console.log(text);
    }).catch(err => {
      console.error('fetch failed', err);
    });
}

以下是使用async/await具有相同作用的代码:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  }
  catch (err) {
    console.log('fetch failed', err);
  }
}

代码行数虽然相同,但去掉了所有回调。这可以提高代码的可读性,对不太熟悉 Promise 的人而言,帮助就更大了。

redux-saga

Redux thunk的写法:

import {
  API_BUTTON_CLICK,
  API_BUTTON_CLICK_SUCCESS,
  API_BUTTON_CLICK_ERROR,
} from './actions/consts';
import { getData } from './api';

const getDataStarted = () => ({type: API_BUTTON_CLICK});
const getDataSuccess = data => ({type: API_BUTTON_CLICK_SUCESS, payload: data});
const getDataError = message => ({type: API_BUTTON_CLICK_ERROR, payload: message});

const getDataFromAPI = (param) => {
  return dispatch => {
    dispatch(getDataStarted());

    getData(param)
      .then(data => {
        dispatch(getUserSucess(data));
      }).catch(err => {
        dispatch(getDataError(err.message));
      })
  }
}

Redux saga的写法:

import { call, put, takeEvery } from 'redux-saga/effects';
import {
    API_BUTTON_CLICK,
    API_BUTTON_CLICK_SUCCESS,
    API_BUTTON_CLICK_ERROR,
} from './actoins/consts';
import { getData } from './api';

export function* apiSideEffect(action) {
    try{
        const data = yield call(getData);
        yield put({ type: API_BUTTON_CLICK_SUCESS, payload: data});
    } catch(e) {
        yield put({type: API_BUTTON_CLICK_ERROR, payload: e.message});
    }
}

export function* apiSaga() {
    yield takeEvery(API_BUTTON_CLICK, apiSideEffect);
}

相比Redux Thunk,使用Redux Saga有几处明显的变化:

  • 在组件中,不再dispatch(action creator),而是dispatch(pure action)
  • 具体业务处理方法中,通过提供的call/put等帮助方法,声明式的进行方法调用
  • 使用ES6 Generator语法,简化异步代码
  • 方便测试:
import { call, put } from 'redux-saga/effects';

import { API_BUTTON_CLICK_SUCCESS, } from './actions/consts';
import { getData } from './api';

it('apiSideEffect - fetches data from API and dispatches a success action', () => {
    const generator = apiSideEffect();

    expect(generator.next().value).toEqual(call(getData));

    expect(generator.next().value).toEqual(put({type: API_BUTTON_CLICK_SUCCESS }));

    expect(generator.next()).toEqual({done: true, value: undefined});
});
  • 高级的异步控制流以及并发管理:可以使用简单的同步方式描述异步流,并通过 fork 实现并发任务。
  • 架构上的优势:将所有的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,增强组件复用性。

API

put
yield put({ type: 'PRODUCTS_RECEIVED', products })

相当于dispatch({ type: ‘PRODUCTS_RECEIVED’, products })

call

call用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个对象。

saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。

export  function getDataApi(reddit){
    return axios.get('http://localhost:3000/getData',{params:reddit.reddit});
}
export  function* requireDatas(reddit){
    const params = yield call(getDataApi, reddit);
    yield put( actions.default.receiveDatas(params.data) )
}

类似Promise.all

const [users, repos]  = yield call([
  call(fetch, '/users'),
  call(fetch, '/repos')
])
select

select用于获取state

* getTopicData({ payload }, { call, put, select }) {
    const setting = yield select(state => ({
        pageIndex: state.topicList.pageIndex,
        pageSize: state.topicList.pageSize,
        tab: state.topicList.tab
        })
    );
    const topicData = yield call(services.GetTopics, setting);

    let { topicDatasource } = yield select(state => ({
        topicDatasource: state.topicList.topicDatasource
    }));
    yield put({
        type: 'changeState',
        payload: { key: 'topicDatasource', value: topicDatasource.concat(topicData.data.data) }
    });
}
takeEvery

用来监听action。当监听到触发此action的时候,如果执行异步操作的话,无论第一次异步是否完成,当发生第二次的时候,都会继续执行。

export  function* requireDatas(reddit){
    const params = yield call(getDataApi, reddit);
    yield put( actions.default.receiveDatas(params.data) )
}
export  function* watchGetData(){
     yield takeEvery(GETDATA,requireDatas)
}

在上面的例子中,takeEvery 允许多个 requireDatas实例同时启动。即使第一个还没结束,第二个依然会启动。

takeLatest

作用同takeEvery一样,唯一的区别是它只关注最后,也就是最近一次发起的异步请求,如果上次请求还未返回,则会被取消。

export  function* watchGetData(){
     yield takeLatest(GETDATA,requireDatas)
}

只执行最后一次出发的 requireDatas;

fork

非阻塞任务调用机制:相对于generator函数来说,call操作是阻塞的,只有等promise回来后才能继续执行,而fork是非阻塞的 ,当调用fork启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。 例子:

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const task = yield fork(authorize, user, password)
    const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
    if(action.type === 'LOGOUT')
      yield cancel(task)
    yield call(Api.clearItem('token'))
  }
}
take

takeEvery 在每个 action 来到时派生一个新的任务,它只是监听每个action,然后执行处理函数。被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听。 而take则不一样,我们可以在generator函数中决定何时响应一个action,以及一个action被触发后做什么操作。

cancel

cancel的作用是用来取消一个还未返回的fork任务。防止fork的任务等待时间太长或者其他逻辑错误。

race

类似Promise中的race操作,可以将多个异步操作作为参数参入race函数中,如果有一个race操作失败或者成功,则本次race操作执行完毕。

yield race({
    task: call(backgroundTask),
    cancel: take('CANCEL_TASK')
})

dva

dva 是一个基于 redux 和 redux-saga 的数据流方案,为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。当然也有简化版dva-core,只包含redux和redux-saga,不涉及路由,所以可以在多端使用,例如react-native、小程序。

在没有 dva 之前,我们通常会创建 sagas/xxx.js, reducers/xxx.js 和 actions/xxx.js,然后在这些文件之间来回切换。如下图:

image

dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。

import key from 'keymaster';

app.model({
  namespace: 'todos', // 全局 state 上的 key
  state: [], // 初始值
  reducers: {
    add(state, { payload: todo }) {
      return state.concat(todo);
    },
  },
  effects: {
    *addRemote({ payload: todo }, { put, call, select }) {
      const todos = yield select(state => state.todos); // 这边的 state 来源于全局的 state,select 方法提供获取全局 state 的能力
      yield call(addTodo, todo); // 用于调用异步逻辑
      yield put({ type: 'add', payload: todo }); // 用于触发 action 。这边需要注意的是,action 所调用的 reducer 或 effects 来源于本 model 那么在 type 中不需要声明命名空间,如果需要触发其他非本 model 的方法,则需要在 type 中声明命名空间,如 yield put({ type: 'namespace/fuc', payload: xxx });
    },
  },
  subscriptions: {
    keyEvent(dispatch) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
};
Effect

Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数式编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。

dva 为了控制副作用的操作,底层引入了redux-sagas做异步流程控制,将异步转成同步写法,从而将effects转为纯函数。

纯函数优势:

  • 可缓存性
  • 可测试性
  • 并行代码
Subscription

Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

dva的优势

  • 使项目架构清晰,页面负责渲染,绝大部分业务逻辑交给model处理。
  • 方便测试
  • 可以处理更加复杂的异步逻辑
  • 多插件,提高开发效率。比如dva-hmr、dva-loading