拉取未来行动
到目前为止,我们一直使用辅助效果 takeEvery
来为每个传入的动作生成一个新任务。这在某种程度上模仿了 redux-thunk
的行为:例如,每次组件调用 fetchProducts
动作创建者时,动作创建者都会分派一个 thunk 来执行控制流。
实际上,takeEvery
只是在底层更强大的 API 之上构建的内部辅助函数的包装效果。在本节中,我们将看到一个新的 Effect,take
,它允许完全控制操作观察过程,从而可以构建复杂的控制流。
一个基本的日志记录器
让我们来看一个 Saga 的基本示例,它会监视发送到存储的所有操作,并将它们记录到控制台。
使用 takeEvery('*')
(使用通配符 *
模式),我们可以捕获所有发送的操作,无论其类型如何。
import { select, takeEvery } from 'redux-saga/effects'
function* watchAndLog() {
yield takeEvery('*', function* logger(action) {
const state = yield select()
console.log('action', action)
console.log('state after', state)
})
}
现在让我们看看如何使用 take
Effect 来实现与上面相同的流程
import { select, take } from 'redux-saga/effects'
function* watchAndLog() {
while (true) {
const action = yield take('*')
const state = yield select()
console.log('action', action)
console.log('state after', state)
}
}
take
就像我们之前看到的 call
和 put
一样。它创建另一个命令对象,告诉中间件等待特定操作。call
Effect 的最终行为与中间件挂起 Generator 直到 Promise 解析时相同。在 take
的情况下,它将挂起 Generator 直到发送匹配的操作。在上面的示例中,watchAndLog
会挂起,直到任何操作被发送。
请注意我们如何运行一个无限循环 while (true)
。请记住,这是一个 Generator 函数,它没有运行到完成的行为。我们的 Generator 将在每次迭代中阻塞,等待操作发生。
使用 take
对我们编写代码的方式有细微的影响。在 takeEvery
的情况下,调用的任务无法控制何时被调用。它们将在每次匹配的操作上被反复调用。它们也无法控制何时停止观察。
在 take
的情况下,控制被反转。Saga 不是被推送到处理程序任务,而是拉取操作。看起来 Saga 正在执行一个正常的函数调用 action = getNextAction()
,它将在操作被发送时解析。
这种控制反转使我们能够实现使用传统的推送方法难以实现的控制流。
作为一个基本的例子,假设在我们的 Todo 应用程序中,我们想要监视用户操作,并在用户创建了前三个 Todo 后显示一条祝贺消息。
import { take, put } from 'redux-saga/effects'
function* watchFirstThreeTodosCreation() {
for (let i = 0; i < 3; i++) {
const action = yield take('TODO_CREATED')
}
yield put({type: 'SHOW_CONGRATULATION'})
}
我们不是使用 while (true)
,而是使用 for
循环,它只迭代三次。在获取了前三个 TODO_CREATED
操作后,watchFirstThreeTodosCreation
将导致应用程序显示一条祝贺消息,然后终止。这意味着 Generator 将被垃圾回收,不再进行观察。
拉取模式的另一个好处是,我们可以使用熟悉的同步风格来描述我们的控制流。例如,假设我们要实现一个包含两个动作的登录流程:LOGIN
和 LOGOUT
。使用 takeEvery
(或 redux-thunk
),我们需要编写两个独立的任务(或 thunk):一个用于 LOGIN
,另一个用于 LOGOUT
。
结果是我们的逻辑现在分散在两个地方。为了让阅读我们代码的人理解它,他们必须阅读两个处理程序的源代码,并在脑海中建立两者之间逻辑的联系。换句话说,这意味着他们必须通过在脑海中重新排列代码中不同位置的逻辑,以正确的顺序重建流程模型。
使用拉取模型,我们可以将我们的流程写入同一个地方,而不是重复处理相同的动作。
function* loginFlow() {
while (true) {
yield take('LOGIN')
// ... perform the login logic
yield take('LOGOUT')
// ... perform the logout logic
}
}
loginFlow
Saga 更清楚地传达了预期的动作顺序。它知道 LOGIN
动作应该始终紧随 LOGOUT
动作,并且 LOGOUT
始终紧随 LOGIN
(一个好的 UI 应该始终强制执行一致的动作顺序,通过隐藏或禁用意外的动作)。