翻译版本,原文请见
14 NOVEMBER 2016
实现Saga代码
这篇文章是三篇关于Redux Saga系列文章的第三篇,也是最后一篇.在第一部分我们先对有关的概念进行了热身.在第二部分,我们和Redux关系推进到了另一个高度.我们把Saga付诸实施.在最后一篇文章,我们把最后一部分内容写完.如果你没有克隆app repo,可以在这个地址克隆,如果不想看这个repo,想直接跳到最终的app代码,可以看这个repo. live demo看这里.
第一部分译文请见,
第二部分译文请见,
第三部分译文请见.
What’s my name agian?
在上一篇文章中,我们设计了基础但是有点幼稚的需求,创建了很初级时钟,可以对用户的交互操作做出一些响应.(译者:后面有几句似乎和技术没有太大关系,我就没有翻译了.请参见原文).
我们决定只使用最小化的redux state值有一个字段“milliseconds”来代表时钟的时间.因为我们的时钟需要向前,向后,重置,所以创建了相应的redux action去执行增,减,重置actions,这些actions可以改变state.
当我们进入某些saga code代码,创建一个saga监听三个actions(开始,暂停,回拨),只有收到相应action的时候,输出日志内容.最后,我们的目标是把日志输出替换成时钟实际运行的流程,例如每一秒种发送一个递减的action,因此app state的异步操作需要好好的设计.最后我们创建一个简单的React组件,可以让我们根据期望的saga代码的工作流程测试redux和saga action,观察state的更新.
现在我们来完成这件事,我们想给朋友留下深刻的印象,看上去有意思一点,所以会使用一些非常酷的SVG技术,当时间增加的时候,时钟可以画出一些图形.但是最重要的是使用Saga可以是app唱歌,跳舞.开始编码.
实现Saga
我们的duck.js是非常小的,实际上仅仅需要很少几行代码去完成saga/redux.目前为止我们的duck代码是这样的:
export function* rootSaga () {
yield takeLatest(['start-clock', 'pause-clock', 'rewind-clock'], handleClockAction)
}
function* handleClockAction ({ type }) {
console.log('Pushed this action to handleClockAction: ', type)
}
正如我们上一篇文章所讨论的,takeLatest
将会监听action type数组中的任何一个action type,当每次发送的action和action type之一匹配的时候,会运行另一个流程(在我们的app中,是handleClockAction
).takeLatest
也会取消之前的运行流程,他也会传递整个action对象,我们可以在handleClockAction
使用这个对象.
现在让我们考虑一下整个app应该怎么工作.所有的实际流程都是在handelClockAction
中,在三个saga actions中,每一个action我们都做一些不同的工作.深入到代码的基础构架中.使用if/else if
代码块替换handleClockAction
的单行代码.这个代码块为每个action type提供分支判断逻辑,并且输出一些文本日志,验证代码的正确性.
给你几秒钟,耐心等待一下.
好了,下面的是我的handelClockAction
的实际样子:
//if else if代码块进行分支判断执行流程
function* handleClockAction ({ type }) {
if (type === 'start-clock') {
console.log(`Received ${type}. We need to run the clock forward here.`)
} else if (type === 'rewind-clock') {
console.log(`Received ${type}. We need to run the clock backwards here.`)
} else if (type === 'pause-clock') {
console.log(`Received ${type}. Guess what needs to be done here?`)
}
}
现在运行app(npm start),转到localhost:8080,打开javascript终端,测试一些saga actions(他们和SVG图下面的按钮是连接在一起的).我们可以看到每次action执行时,有相应的日志输出.
当我们开始clock action,我们想让时钟每100毫秒递增一次.为了这样做,从saga导入助手函数”delay”.编辑redux-saga文件.
import { delay, takeLatest } from 'redux-saga'
delay非常简单,功能不言自明.它接收一个参数,毫秒数,然后暂停执行对应的时间.例如yield delay(50)
.在if/else if
代码块中添加一些逻辑代码,每1000ms,输出日志.请记住,你需要这个过程无限制的重复.我们可以使用非常简单的原生javascript代码控制流来实现.我想让你先猜猜怎么实现.
不要偷看.
花点时间考虑一下.
好了,下面是我的代码
function* handleClockAction ({ type }) {
if (type === 'start-clock') {
while (true) {//就是这个while(true)语句保持始终运行
yield delay(1000)
console.log(`Received ${type}. We need to run the clock forward here.`)
}
} else if (type === 'rewind-clock') {
console.log(`Received ${type}. We need to run the clock backwards here.`)
} else if (type === 'pause-clock') {
console.log(`Received ${type}. Guess what needs to be done here?`)
}
}
在这一点上,打开js终端,查看日志输出,可以得到直观的信息.你将看到,start clock action暂停100毫秒,然后输出一些日志信息,之后无限地重复这个过程.注意一个关键事实,无论什么时间我们的takeLatest
函数接收到匹配的action的时候,它会退出当前运行的handleClockAction
.如果是暂停的action,什么事情也不做,但是还是要退出当前任何的action,猜猜我们应该怎么做?删除一点代码!暂停的时钟将会自动开始工作,因为takeLatest
会退出当前的handleClockAction
,不管时钟是向前还是向后运行,时钟都会停止.
确信你理解takeLatest
的关键特性.在handleClockAction
中做出合适的改变.应该想下面这样做:
function* handleClockAction ({ type }) {
if (type === 'start-clock') {
while (true) {
yield delay(1000)
console.log(`Received ${type}. We need to run the clock forward here.`)
}
} else if (type === 'rewind-clock') {
console.log(`Received ${type}. We need to run the clock backwards here.`)
}
}
现在让我们在循环内真正的dispatch一个action.
(旁白:我们没有实际dispatch一个action,仅仅请求中间件来做这件事,暂时不要担心,我后面会解释清楚的.)
在Saga里面dispatch一个action,我们要使用put
side effects,添加一行代码到duck文件里.
import { put } from 'redux-saga/effects'
通过put
请求一个dispatch的action,简单的使用yield put({ type: 'hi', data: 'I am an action' })
就可以了.意思是在put
里,可以调用一个action creator 返回一个单纯对象,像这样yield put(someAction())
.(在javascript中函数的参数会立即求值).还记得我们在开始创建的那些redux action吗?我们要使用一个,转到while循环,试试put
.
function* handleClockAction ({ type }) {
if (type === 'start-clock') {
while (true) {
yield delay(1000)
yield put(incrementMilliseconds())//注意这一句
}
} else if (type === 'rewind-clock') {
console.log(`Received ${type}. We need to run the clock backwards here.`)
}
}
在你的浏览器里试试.注意在SVG下面的时钟时间是递增的.
为了让时钟后退,可以重用start 时钟的大部分代码.
下面是回退的代码:
function* handleClockAction ({ type }) {
if (type === 'start-clock') {
while (true) {
yield delay(1000)
yield put(incrementMilliseconds())
}
} else if (type === 'rewind-clock') {
while (true) {
yield delay(1000)
yield put(decrementMilliseconds())//递减
}
}
}
现在测试一下所有的saga actions,确保每件事情都工作正常.如果工作不正常,那绝对是你的错,不是我的错.开个玩笑.(译者:后面几句没有翻译)
激动人心的时刻是运行saga代码并且通过实验学到的了一些东西.一个提示:delay
是阻塞的,其他的effect语句都是非阻塞的.试着使用for
循环.要么把while
条件改为false.我们已经放弃了promise的世界,现在我们可以使用这些简单的,可预测的,原生的javascript来表现出所有的控制流语句.
好了,把saga代码恢复到上面的样子.
做好了上面的恢复,我的saga代码已经完成,可以操作了(注意事项:在最终版本上,我已经把代码重新组织了一下,以便于阅读).
Saga’s的方式处理异步操作
好了.现在让我们再做一些事情,让我们谈谈代码是怎么工作的.Redux Saga中核心的概念是:我们实际没有像dispatch一个action到store一样执行异步操作.替代方法是:我们yield异步操作到saga middleware,然后middleware执行他们.简单的说,我们给Saga一个指示,告诉他我们希望发生什么.这些异步操作描述是遵循标准generator输出的单纯对象:{value:any,done:boolean}
,这和我们在第一部分看到的完全一样.
例如,yield put
发送一个对象到中间件,这个中间件包含一个action对象,可以dispatch一些内容给我们(译者:这个地方有点绕,其实是在异步操作完成以后,需要再次dispatch返回的结果,从而改变state,这样我们的组件才可以根据state的改变来渲染效果,例如从远程资源请求数据,通常都需要把整个操作分成几步来完成,每一步要改变不同的state,在dispatch中再dispatch)这样定义异步数据流有很多好处,其中之一是可测试性.我们可以很简单的在每一个side effect返回中间件的时候创建断言(继续关注后续的内容,有测试的深入介绍)
我们应该遵循redux-saga的最佳实践,做些小小的更新,从redux-saga/effects中导入call
,把yield delay(100)
改为yield call(delay,100)
.因为我们只想返回side-effects到saga中间件,saga提供了call
effect把non-effects转变为effects.与仅仅yielddelay
自己(promise对象)不同,我们通过call
把delay
转变为effect,然后yield它.所有的non-effect都应该做相同的处理.call
的语法调用一个函数,并且传递参数列表.例如:yield call(fetch,url,options)
.最后,注意,call
可以被promise对象,generators,或者函数使用.
代码不要太丑陋
我们需要创建一个文件作为app的配置,包含一些常量.为什么不叫做“config.js”在/src目录下创建,粘贴下面的内容.
const MAX_RADIUS = 40
let hands = [
{ ms: 144000, maxTicks: 1 },
{ ms: 36000, maxTicks: 4 },
{ ms: 12000, maxTicks: 3 },
{ ms: 2000, maxTicks: 6 },
{ ms: 400, maxTicks: 5 },
{ ms: 100, maxTicks: 4 }
]
export const STROKE_WIDTH = MAX_RADIUS / hands.length
hands = hands.map((hand, idx) => {
const radius = STROKE_WIDTH * (hands.length - idx)
return {
...hand,
radius,
circumference: 2 * Math.PI * radius,
alpha: 1 - idx / hands.length
}
})
export const CLOCK_HANDS = hands
export const MINIMUM_MS = 100
这里面都有什么内容?让我解释一下,它是一组常量和计算量,描述SVG的外观.
我不会详细讲这个内容,不是这里的重点.
好了,现在我们的saga代码已经就绪,我们也定义了所有SVG需要的配置文件.再更新一下SVG内容,好完成app.jsx的代码.
完成 HIM
下面是目前的app.jsx代码
import React from 'react'
import { connect } from 'react-redux'
import { incrementMilliseconds, decrementMilliseconds, resetClock, startClock, pauseClock, rewindClock } from 'duck'
class Clock extends React.Component {
render () {
const {
milliseconds,
incrementMilliseconds,
decrementMilliseconds,
resetClock,
startClock,
pauseClock,
rewindClock
} = this.props
return (
{ milliseconds }
)
}
}
export default connect(state => ({
milliseconds: state.milliseconds
}), ({
incrementMilliseconds,
decrementMilliseconds,
resetClock,
startClock,
pauseClock,
rewindClock
}))(Clock)
我们需要导入conifg内容,通过props传递到组件.删除一些不必要的导入文件.不再直接调用特定的actions,因为我们仅仅在saga中发出action.更新一下代码.
import React from 'react'
import { connect } from 'react-redux'
import { startClock, rewindClock, pauseClock, resetClock } from 'duck'
import { CLOCK_HANDS, STROKE_WIDTH } from 'config'
现在我们有了数据,可以在SVG内画出漂亮的圆环.简单点的说我们使用同心圆代表小时/分钟/秒.时间流逝的时候,改变每一个圆环的长度.为了做SVG图片,使用了一些小技巧CSS Trick blog post
我们不再通过props手段传递millseconds字段.每次更新milliseconds时,我们计算每一个时钟指针的位置.这像是烦人的数学计算.基本上我们遍历每一个指针,然后在下一个指针重复同样的过程,(时针就减掉小时的整倍好秒数,分钟和秒一样处理),把剩余的时间传递给下去.最小的指针对应的最小的精度单位,100ms(config.js中的MAXIMUM_MS),更新你的connect
export default connect(state => {
const currentTime = state.milliseconds
let remainingTime = currentTime
const getTicks = (hands, timeRemaining) => {
let [hand, ...tailHands] = hands
hand.ticks = Math.floor(timeRemaining / hand.ms)
return tailHands.length ? [hand, ...getTicks(tailHands, timeRemaining % hand.ms)] : [hand]
}
const hands = getTicks(CLOCK_HANDS, remainingTime)
.map((hand, idx) => {
const offset = state.milliseconds >= hand.ms ? 1 : 0
const position = hand.circumference - ((hand.ticks + offset) / hand.maxTicks * hand.circumference)
return {
...hand,
position
}
})
return {
hands
}
}, ({
startClock,
rewindClock,
resetClock,
pauseClock
}))(Clock)
现在我们需要在组件中导入新的指针
属性.在渲染方法中给props解构赋值:
render () {
const {
hands,
startClock,
rewindClock,
resetClock,
pauseClock
} = this.props
为了最终版本,我们做一下最终的改变.我们更新SVG,启动onMouseEnter
,回退onMouseLeave
,暂停onClick
.添加一下这些变化.
(译者:这一段也不翻了,原作者要喝伏特加).
做好了吧,现在我们在SVG内创建同心圆,遍历hands
并且创建
SVG元素,使用我们的计算值设置每个指针的半径,位置和透明度.
这是最终的更新,下面是render()
函数返回的内容:
return (
)
忘了件事情,返回duck.js文件,从config导入MINIMUM_MS:
import { MINIMUM_MS } from 'config'
现在在delay函数调用时使用MINIMUM_MS
替换1000
.
时钟每100ms就会滴答一下:
function* handleClockAction ({ type }) {
if (type === 'start-clock') {
while (true) {
yield delay(MINIMUM_MS)
yield put(incrementMilliseconds())
}
} else if (type === 'rewind-clock') {
while (true) {
yield delay(MINIMUM_MS)
yield put(decrementMilliseconds())
}
}
}
看看在saga中控制流程的逻辑有多容易?
给你的时钟上发条
现在给这段代码拧发条,npm start
,浏览器中打开localhost:8080.
这是一个能暂停,恢复,向前,向后和重置.看上去也很酷.
结束语
感谢阅读这个教程,我们涉及到了js generator和Redux saga的有关内容.我希望能帮助你理解怎么使用Redux Saga,同时还有使用的好处和背后的合理性.
如果你想了解更多的Redux Saga的内容,这里还有一些很好的教程.下面是我开始学习redux saga的时候帮助我入门的内容.
- Redux的官方文档是无价之宝
- Niels Gerritsen's的Redux Saga入门教程.这是我最开始学习Redux saga的教程之一,结构很清晰,帮助我跨国一些概念障碍.
- Joel Hooks的教程
- Jack Hsu的文章.我的实例实际是这个教程的扩展.
我也喜欢下面的文章,解密generators,激励我写这篇文章,没有这两个文章,我都不知道generators讲的是什么玩意.
- David Walsh的博客,关于ES6 generator的内容,作者是kyle Simpson
- Axel Rauschmeyer的ES6 generators的文章
感谢阅读!