现在有很多人在谈论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的项目,因此也有兴趣了解更多关于saga方法的好处。

说实话,我还在找。读了这篇文章和许多喜欢它的人,“优点”是难以捉摸的。以上的答案似乎可以总结为:

可测试性(忽略实际的API调用), 很多辅助函数, 熟悉服务器端编码的开发人员。

许多其他的说法似乎过于乐观,误导或根本是错误的!我看到过许多不合理的说法,例如“坦克不能做X”。但是坦克是功能。如果一个函数不能做X,那么javascript也不能做X,所以saga也不能做X。

对我来说,缺点是:

confounding of concerns by using generator functions. Generators in JS return custom iterators. That is all. They do not have any special ability to handle async calls or to be cancellable. Any loop can have a break-out condition, any function can handle async requests, and any code can make use of a custom iterator. When people say thing like: generators have control when to listen for some action or generators are cancellable, but async calls are not then it creates confusion by implying that these qualities are inherent in - or even unique to - generator functions. unclear use-cases: AFAIK the SAGA pattern is for handling concurrent transaction issues across services. Given that browsers are single-threaded, it is hard to see how concurrency presents a problem that Promise methods can't handle. BTW: it is also hard to see why that class of problem should ever be handled in the browser. code traceability: By using redux middleware to turn dispatch into a kind of event-handling, Sagas dispatch actions that never reach the reducers, and so never get logged by Redux tools. While other libraries also do this, it is often unnecessarily complicated, given that browsers have event-handling built in. The advantage of the indirection is again elusive, when calling the saga directly would be more obvious.

如果这篇文章让我对传奇故事感到沮丧,那是因为我对传奇故事感到沮丧。它们似乎是一个寻找问题的伟大解决方案。国际海事组织。

其他回答

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

来自文献传播

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

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

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

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

在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维护者。

我们最近在Redux文档中添加了一个新的Side Effects methods页面,它将提供关于所有这些内容的许多信息,但我将尝试在这里写一些简短的内容,因为这个问题得到了大量的曝光。

在2022年,我们将监听器中间件添加到官方Redux工具包中,用于“响应式Redux逻辑”。它可以做sagas可以做的大部分事情(例外是通道),而不需要生成器语法和更好的TypeScript支持。 但这并不意味着你应该用侦听器中间件来编写所有的东西——我们建议在可能的情况下总是先使用thks,在thks不能做你想做的事情时再使用侦听器中间件。

一般来说,我们在2023年的立场是,只有当你有其他中间件无法满足的特定需求时,你才应该使用saga。(本质上:如果你需要频道。)

我们的建议是:

数据抓取

使用RTK Query作为数据获取和缓存的默认方法 如果RTKQ不完全适合,使用createAsyncThunk 只有在其他方法都没用的情况下,才会求助于手写的感谢信 不要使用saga或可观察对象来获取数据!

对动作/状态变化做出反应,异步工作流

使用RTK监听器作为默认响应存储更新和编写长时间运行的异步工作流 只有当监听器不能很好地解决你的用例时,才使用saga / observable

带有状态访问的逻辑

对于复杂的同步和适度的异步逻辑,包括对getState的访问和调度多个操作,可以使用坦克

坦克大战萨加斯

Redux- thunk和Redux- saga在一些重要的方面有所不同,它们都是Redux的中间件库(Redux中间件是通过dispatch()方法拦截进入商店的操作的代码)。

一个动作可以是任何东西,但是如果您遵循最佳实践,那么一个动作就是一个简单的javascript对象,其中包含一个类型字段,以及可选的有效负载、元和错误字段。如。

const loginRequest = {
    type: 'LOGIN_REQUEST',
    payload: {
        name: 'admin',
        password: '123',
    }, };

Redux-Thunk

除了分派标准的动作,redux -坦克中间件还允许你分派特殊的函数,称为坦克。

坦克(在Redux中)通常有以下结构:

export const thunkName =
   parameters =>
        (dispatch, getState) => {
            // Your application logic goes here
        };

也就是说,一个thunk是一个(可选的)接受一些参数并返回另一个函数的函数。内部函数接受一个分派函数和一个getState函数——这两个函数都将由Redux-Thunk中间件提供。

Redux-Saga

Redux-Saga中间件允许您将复杂的应用程序逻辑表示为称为saga的纯函数。从测试的角度来看,纯函数是可取的,因为它们是可预测和可重复的,这使得它们相对容易测试。

saga是通过称为生成器函数的特殊函数实现的。这些是ES6 JavaScript的新特性。基本上,在任何看到yield语句的地方,执行都会在生成器中跳跃。将yield语句看作是导致生成器暂停并返回yield值的语句。稍后,调用者可以在yield之后的语句处恢复生成器。

生成器函数是这样定义的。注意function关键字后面的星号。

function* mySaga() {
    // ...
}

一旦登录传奇注册到Redux-Saga。但是在第一行上的yield take将暂停saga,直到带有“LOGIN_REQUEST”类型的操作被分派到商店。一旦发生这种情况,执行将继续进行。

欲了解更多细节,请参阅本文。

一个小提示。生成器是可取消的,async/await - not。 举个例子,选什么并没有什么意义。 但对于更复杂的流,有时没有比使用生成器更好的解决方案了。

所以,另一个想法可能是使用带有还原坦克的发电机,但对我来说,这就像试图发明一辆有方轮子的自行车。

当然,生成器更容易测试。