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的特点:
- 分段执行,可以暂停
- 可以控制阶段和每个阶段的返回值
- 可以知道是否执行到结尾
利用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,然后在这些文件之间来回切换。如下图:
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