使用ES6生成器(Generators)和redux-saga与使用ES2017的async/await和redux-thunk相比的优缺点。

发布时间 2023-11-13 14:50:59作者: 小满独家

内容来自 DOC https://q.houxu6.top/?s=使用ES6生成器(Generators)和redux-saga与使用ES2017的async/await和redux-thunk相比的优缺点。

目前关于redux的最新讨论焦点是redux-saga/redux-saga。它使用生成器函数来监听/分发actions。

在我深入研究之前,我想了解使用redux-saga与下面使用redux-thunk和async/await的方法相比的优缺点。

一个组件可能如下所示,像往常一样分发actions。

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
 <input type="text" ref="user" />
 <input type="password" ref="pass" />
 <button onClick={::this.onClick}>Sign In</button>
 </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

然后我的actions看起来像这样:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// 定义常量
// 定义初始状态
// 导出默认reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// 更多actions...


// user.js

import request from 'axios';

// 定义常量
// 定义初始状态
// 导出默认reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// 更多actions...


在redux-saga中,与上面的示例等效的代码如下:

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN\_REQUEST)
    try {
      let { data } = yield call(request.post, '/login', { user, pass });
      yield fork(loadUserData, data.uid);
      yield put({ type: LOGIN\_SUCCESS, data });
    } catch(error) {
      yield put({ type: LOGIN\_ERROR, error });
    }  
  }
}

export function* loadUserData(uid) {
  try {
    yield put({ type: USERDATA\_REQUEST });
    let { data } = yield call(request.get, `/users/${uid}`);
    yield put({ type: USERDATA\_SUCCESS, data });
  } catch(error) {
    yield put({ type: USERDATA\_ERROR, error });
  }
}

首先要注意的是,我们使用yield call(func, ...args)的形式调用API函数。call并不执行实际的效果,它只是创建一个普通对象,类似于{type: 'CALL', func, args}。执行任务的工作委托给redux-saga中间件,它负责执行函数并恢复生成器并返回结果。

主要优点是您可以在Redux之外使用简单的相等性检查来测试生成器。

const iterator = loginSaga()

assert.deepEqual(iterator.next().value, take(LOGIN\_REQUEST))

// 使用一些虚拟的操作恢复生成器
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// 模拟错误结果
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN\_ERROR, error: mockError })
)

请注意,我们通过向迭代器的next方法注入模拟数据来模拟API调用结果。与模拟函数相比,模拟数据要简单得多。

第二个要注意的是对yield take(ACTION)的调用。Thunk是由动作创建者在每个新动作(例如LOGIN_REQUEST)上调用的。即动作持续地“推送”给thunk,而thunk无法控制何时停止处理那些动作。

在redux-saga中,生成器“拉取”下一个动作。也就是说,它们可以控制何时监听某个动作以及何时不监听。在上面的示例中,流程指令放置在一个while(true)循环内,因此它将监听每个传入的动作,从某种程度上模拟了thunk的推送行为。

拉取的方式允许实现复杂的控制流程。例如,假设我们想要添加以下要求:

  • 处理LOGOUT用户动作
  • 在首次成功登录时,服务器返回一个以某个字段expires\_in存储的一段时间后过期的令牌,我们需要在每个expires\_in毫秒上后台刷新授权
  • 注意,在等待api调用的结果时(无论是初始登录还是刷新),用户可能会在其间注销。

如何使用thunk实现这一点,并为整个流程提供完整的测试覆盖率?以下是使用Sagas可能的实现方式:

function* authorize(credentials) {
  const token = yield call(api.authorize, credentials)
  yield put( login.success(token) )
  return token
}

function* authAndRefreshTokenOnExpiry(name, password) {
  let token = yield call(authorize, {name, password})
  while(true) {
    yield call(delay, token.expires\_in)
    token = yield call(authorize, {token})
  }
}

function* watchAuth() {
  while(true) {
    try {
      const {name, password} = yield take(LOGIN\_REQUEST)

      yield race([
        take(LOGOUT),
        call(authAndRefreshTokenOnExpiry, name, password)
      ])

      // 用户已注销,下一个while迭代将等待下一个LOGIN\_REQUEST动作

    } catch(error) {
      yield put( login.error(error) )
    }
  }
}

在上面的示例中,我们使用race来表达我们的并发需求。如果take(LOGOUT)赢得了比赛(即用户点击了注销按钮),比赛将自动取消authAndRefreshTokenOnExpiry后台任务。如果authAndRefreshTokenOnExpirycall(authorize, {token})调用的中间被阻塞,它也会被取消。取消会自动向下传播。

您可以在此流程的可运行演示中找到。