现在有很多人在谈论redux城的最新成员,redux-saga/redux-saga。它使用生成器函数来监听/分派动作。

在我把我的脑袋绕在它周围之前,我想知道使用redux-saga的优点/缺点,而不是下面的方法,我使用redux-thunk与async/await。

组件可能是这样的,像往常一样分派动作。

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);

然后我的动作看起来像这样:

// auth.js

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

// define constants
// define initial state
// export default 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 });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default 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 });
    }
}

// more 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))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

注意,我们通过简单地将模拟的数据注入迭代器的下一个方法来模拟api调用结果。模拟数据比模拟函数简单得多。

要注意的第二件事是yield take(ACTION)的调用。动作创建者在每个新动作(例如LOGIN_REQUEST)上调用链接。也就是说,动作会不断地推送给坦克,而坦克无法控制何时停止处理这些动作。

在redux-saga中,生成器拉动下一个动作。也就是说,他们可以控制什么时候听某些动作,什么时候不听。在上面的例子中,流指令被放置在一个while(true)循环中,因此它将侦听每个传入的动作,这在某种程度上模仿了thunk push行为。

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

处理注销用户操作 在第一次成功登录时,服务器返回一个令牌,该令牌将在某个延迟中过期,存储在expires_in字段中。我们必须在每个expires_in毫秒时在后台刷新授权 考虑到在等待api调用的结果时(无论是初始登录还是刷新),用户可能会中途注销。

你怎么用坦克来实现它;同时还为整个流程提供完整的测试覆盖率?以下是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)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

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

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

您可以找到上述流程的可运行演示

其他回答

在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))

// resume the generator with some dummy action
const mockAction = {user: '...', pass: '...'}
assert.deepEqual(
  iterator.next(mockAction).value, 
  call(request.post, '/login', mockAction)
)

// simulate an error result
const mockError = 'invalid user/password'
assert.deepEqual(
  iterator.throw(mockError).value, 
  put({ type: LOGIN_ERROR, error: mockError })
)

注意,我们通过简单地将模拟的数据注入迭代器的下一个方法来模拟api调用结果。模拟数据比模拟函数简单得多。

要注意的第二件事是yield take(ACTION)的调用。动作创建者在每个新动作(例如LOGIN_REQUEST)上调用链接。也就是说,动作会不断地推送给坦克,而坦克无法控制何时停止处理这些动作。

在redux-saga中,生成器拉动下一个动作。也就是说,他们可以控制什么时候听某些动作,什么时候不听。在上面的例子中,流指令被放置在一个while(true)循环中,因此它将侦听每个传入的动作,这在某种程度上模仿了thunk push行为。

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

处理注销用户操作 在第一次成功登录时,服务器返回一个令牌,该令牌将在某个延迟中过期,存储在expires_in字段中。我们必须在每个expires_in毫秒时在后台刷新授权 考虑到在等待api调用的结果时(无论是初始登录还是刷新),用户可能会中途注销。

你怎么用坦克来实现它;同时还为整个流程提供完整的测试覆盖率?以下是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)
      ])

      // user logged out, next while iteration will wait for the
      // next LOGIN_REQUEST action

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

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

您可以找到上述流程的可运行演示

除了图书馆作者相当彻底的回答之外,我将添加我在生产系统中使用saga的经验。

优点(使用saga):

Testability. It's very easy to test sagas as call() returns a pure object. Testing thunks normally requires you to include a mockStore inside your test. redux-saga comes with lots of useful helper functions about tasks. It seems to me that the concept of saga is to create some kind of background worker/thread for your app, which act as a missing piece in react redux architecture(actionCreators and reducers must be pure functions.) Which leads to next point. Sagas offer independent place to handle all side effects. It is usually easier to modify and manage than thunk actions in my experience.

Con:

发电机的语法。 有很多概念要学。 API的稳定性。redux-saga似乎还在添加新功能(比如Channels?),社区也没有那么大。如果库有一天进行了不向后兼容的更新,就会有问题。

这是一个结合了redux-saga和redux-thunk最好的部分(优点)的项目:你可以处理所有的副作用,同时通过调度相应的行动获得承诺: https://github.com/diegohaz/redux-saga-thunk

class MyComponent extends React.Component {
  componentWillMount() {
    // `doSomething` dispatches an action which is handled by some saga
    this.props.doSomething().then((detail) => {
      console.log('Yaay!', detail)
    }).catch((error) => {
      console.log('Oops!', error)
    })
  }
}

更简单的方法是使用redux-auto。

来自文献传播

Redux-auto通过允许您创建一个返回承诺的“action”函数来修复这个异步问题。以配合您的“默认”函数动作逻辑。

不需要其他Redux异步中间件。例如,thunk, promise-middleware, saga 轻松地允许您传递到redux承诺,并为您管理它 允许您将外部服务调用与它们将被转换的位置放在一起 将文件命名为“init.js”将在应用启动时调用它一次。这有利于在启动时从服务器加载数据

其思想是将每个操作放在一个特定的文件中。在文件中将服务器调用与“pending”、“completed”和“rejected”的reducer函数放在一起。这使得处理承诺非常容易。

它还自动将一个helper对象(称为“async”)附加到您的状态原型上,允许您在UI中跟踪请求的转换。

我只是想从我的个人经验(使用sagas和thunk)中补充一些评论:

传奇故事很适合测试:

您不需要模拟用效果包装的函数 因此,测试是干净的,可读的,易于编写的 在使用saga时,动作创建者通常会返回简单的对象字面量。与thunk的承诺不同,它也更容易测试和断言。

传说更有力量。你在一个坦克动作创造者中可以做到的所有事情,你也可以在一个传奇中做到,但反之亦然(至少不容易)。例如:

等待一个/多个动作被分派(采取) 取消现有的例程(取消,taklatest, race) 多个例程可以监听相同的动作(take, takeEvery,…)

Sagas还提供了其他有用的功能,它概括了一些常见的应用程序模式:

监听外部事件源的通道(例如websockets) Fork模型(Fork, spawn) 节气门 ...

Sagas are great and powerful tool. However with the power comes responsibility. When your application grows you can get easily lost by figuring out who is waiting for the action to be dispatched, or what everything happens when some action is being dispatched. On the other hand thunk is simpler and easier to reason about. Choosing one or another depends on many aspects like type and size of the project, what types of side effect your project must handle or dev team preference. In any case just keep your application simple and predictable.