Skip to content

Commit

Permalink
Introducing all effect - explicit parallel effect (against implicit a…
Browse files Browse the repository at this point in the history
…rrays of effects). This also allows to create labeled all effect like the race one
  • Loading branch information
Andarist committed Apr 29, 2017
1 parent 6ae3bf6 commit 7078c20
Show file tree
Hide file tree
Showing 18 changed files with 195 additions and 96 deletions.
12 changes: 6 additions & 6 deletions README_zh-cn.md
Expand Up @@ -122,7 +122,7 @@ Sagas 工作方式是不一样的,他们不是在Action Creators内被触发
#A common abstraction: Effect
一般来说,等待一个未知的action,等待像`yield delay(1000)`这样的未知的函数调用结果,或者等待一个调度的结果,这些都是相同的概念。在所有情况下,我们迭代某些形式的Effect。Saga所做的,实际上就是把所有这些Effect组合在一起,去实现期望的控制流。最简单的是一个接着一个的顺序执行yield来迭代Effect。你也可以使用常见的控制操作(if,while,for)去实现更复杂的控制流。或者你可以使用提供的Effect组合去表达并发 (yield race) 和 平行 (yield [...])。你也可以迭代调用其他Saga,允许强大的常规或者子程序模式。
一般来说,等待一个未知的action,等待像`yield delay(1000)`这样的未知的函数调用结果,或者等待一个调度的结果,这些都是相同的概念。在所有情况下,我们迭代某些形式的Effect。Saga所做的,实际上就是把所有这些Effect组合在一起,去实现期望的控制流。最简单的是一个接着一个的顺序执行yield来迭代Effect。你也可以使用常见的控制操作(if,while,for)去实现更复杂的控制流。或者你可以使用提供的Effect组合去表达并发 (yield race) 和 平行 (yield all([...]))。你也可以迭代调用其他Saga,允许强大的常规或者子程序模式。
举例来说,`incrementAsync` 使用了无限循环 `while(true)`,它意味着这将会在整个应用程序的生命周期都会存在。
Expand Down Expand Up @@ -280,13 +280,13 @@ const users = yield call(fetch, '/users'),
因为第二个Effect将等到第一个执行结束后再执行,我们必须改成如下形式:
```javascript
import { call } from 'redux-saga'
import { call, all } from 'redux-saga/effects'

// correct, effects will get executed in parallel
const [users, repose] = yield [
const [users, repose] = yield all([
call(fetch, '/users'),
call(fetch, '/repose')
]
])
```
当我们迭代一个Effect数组,生成器是被阻塞的直到所有的Effect都被执行完成(或者当其中有一个被拒绝,就像 `Promise.all`的运行机制 )。
Expand Down Expand Up @@ -370,7 +370,7 @@ function* watchFetch() {
```javascript
function* mainSaga(getState) {
const results = yield [ call(task1), call(task2), ...]
const results = yield all([ call(task1), call(task2), ...])
yield put( showResults(results) )
}
```
Expand Down Expand Up @@ -591,7 +591,7 @@ function* subtask2() {
1- 在一个`race` effect。所有的比赛竞争对手,除了胜利者,其它都自动取消。
2- 在一个并行effect (`yield [...]`)。当其中一个子effect失败(于Promise.all相似), 在这个例子中其他的子effect全部自动取消。
2- 在一个并行effect (`yield all([...])`)。当其中一个子effect失败(于Promise.all相似), 在这个例子中其他的子effect全部自动取消。
不同于手动取消,未处理的取消异常不会冒泡到实际saga运行的race/parallel effect。然而,假如取消任务并且没有处理取消异常,一个警告log会写到控制台。
Expand Down
12 changes: 6 additions & 6 deletions README_zh-hant.md
Expand Up @@ -121,7 +121,7 @@ Sagas 運作方式不同,並不是由 Action Creators 所觸發,而是與你
為了一般化,等待未來的 action;等待未來的結果,像是呼叫 `yield delay(1000)`;或者等待分派的結果,都是相同的概念。所有的案例都在引起某些 Effects 形式。
而 Saga 所做的事,實際上是將這些所有 effects 組合在一起,以便實作想要的控制流程。最簡單的方式是一個 yeidls 接著另一個 yields,循序引起 Effects。也可以使用熟悉的控制流程操作子(if、while、for)來實作複雜的控制流程。或者你想要使用 Effects 協調器來表達並發(concurrency,yield race)及平行(parallelism,yield [...])。甚至可以引起其他的 Sagas,讓你擁有強大的 routine/subroutine 樣式。
而 Saga 所做的事,實際上是將這些所有 effects 組合在一起,以便實作想要的控制流程。最簡單的方式是一個 yeidls 接著另一個 yields,循序引起 Effects。也可以使用熟悉的控制流程操作子(if、while、for)來實作複雜的控制流程。或者你想要使用 Effects 協調器來表達並發(concurrency,yield race)及平行(parallelism,yield all([...]))。甚至可以引起其他的 Sagas,讓你擁有強大的 routine/subroutine 樣式。
舉例來說,`incrementAsync` 使用無窮迴圈 `while(true)` 來表示將會永遠運作於應用程式的生命週期之內。
Expand Down Expand Up @@ -285,13 +285,13 @@ const users = yield call(fetch, '/users'),
因為直到第 1 個呼叫解決之前,第 2 個 effect 並不會執行。取而代之,我們要寫成
```javascript
import { call } from 'redux-saga'
import { call, all } from 'redux-saga/effects'

// 正確,effects 將會平行地執行
const [users, repose] = yield [
const [users, repose] = yield all([
call(fetch, '/users'),
call(fetch, '/repose')
]
])
```
當我們引起一個陣列的 effects,generator 將會阻塞直到所有 effects 都被解決(或者一旦其中有一個被拒絕,如同 `Promise.all` 行為)。
Expand Down Expand Up @@ -375,7 +375,7 @@ function* watchFetch() {
```javascript
function* mainSaga(getState) {
const results = yield [ call(task1), call(task2), ...]
const results = yield all([ call(task1), call(task2), ...])
yield put( showResults(results) )
}
```
Expand Down Expand Up @@ -594,7 +594,7 @@ function* subtask2() {
1- 在 `race` effect 中。所有 race 競爭者,除了贏家,其餘皆會自動取消。
2- 在平行 effect(`yield [...]`)中。一旦有一個 sub-effects 被拒絕,平行 effect 將很快的被拒絕(如同 Promise.all)。這個情況下,所有其他的 sub-effects 將會自動取消。
2- 在平行 effect(`yield all([...])`)中。一旦有一個 sub-effects 被拒絕,平行 effect 將很快的被拒絕(如同 Promise.all)。這個情況下,所有其他的 sub-effects 將會自動取消。
#動態啟動 Sagas — runSaga
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/ComposingSagas.md
Expand Up @@ -27,7 +27,7 @@ for them to finish, then resume with all the results

```javascript
function* mainSaga(getState) {
const results = yield [call(task1), call(task2), ...]
const results = yield all([call(task1), call(task2), ...])
yield put(showResults(results))
}
```
Expand Down
14 changes: 7 additions & 7 deletions docs/advanced/ForkModel.md
Expand Up @@ -55,18 +55,18 @@ The attentive reader might have noticed the `fetchAll` saga could be rewritten u

```js
function* fetchAll() {
yield [
yield all([
call(fetchResource, 'users'), // task1
call(fetchResource, 'comments'), // task2,
call(delay, 1000)
]
])
}
```

In fact, attached forks shares the same semantics with the parallel Effect:

- We're executing tasks in parallel
- The parent will terminate after all launched tasks terminate
- The parent will terminate after all launched tasks terminate


And this applies for all other semantics as well (error and cancellation propagation). You can understand how
Expand All @@ -79,11 +79,11 @@ Following the same analogy, Let's examine in detail how errors are handled in pa
for example, let's say we have this Effect

```js
yield [
yield all([
call(fetchResource, 'users'),
call(fetchResource, 'comments'),
call(delay, 1000)
]
])
```

The above effect will fail as soon as any one of the 3 child Effects fails. Furthermore, the uncaught error will cause
Expand Down Expand Up @@ -125,8 +125,8 @@ function* main() {
If at a moment, for example, `fetchAll` is blocked on the `call(delay, 1000)` Effect, and say, `task1` failed, then the whole
`fetchAll` task will fail causing

- Cancellation of all other pending tasks. This includes:
- The *main task* (the body of `fetchAll`): cancelling it means cancelling the current Effect `call(delay, 1000)`
- Cancellation of all other pending tasks. This includes:
- The *main task* (the body of `fetchAll`): cancelling it means cancelling the current Effect `call(delay, 1000)`
- The other forked tasks which are still pending. i.e. `task2` in our example.

- The `call(fetchAll)` will raise itself an error which will be caught in the `catch` body of `main`
Expand Down
4 changes: 2 additions & 2 deletions docs/advanced/RunningTasksInParallel.md
Expand Up @@ -14,10 +14,10 @@ Because the 2nd effect will not get executed until the first call resolves. Inst
import { call } from 'redux-saga/effects'

// correct, effects will get executed in parallel
const [users, repos] = yield [
const [users, repos] = yield all([
call(fetch, '/users'),
call(fetch, '/repos')
]
])
```

When we yield an array of effects, the generator is blocked until all the effects are resolved or as soon as one is rejected (just like how `Promise.all` behaves).
2 changes: 1 addition & 1 deletion docs/advanced/TaskCancellation.md
Expand Up @@ -120,4 +120,4 @@ Besides manual cancellation there are cases where cancellation is triggered auto

1. In a `race` effect. All race competitors, except the winner, are automatically cancelled.

2. In a parallel effect (`yield [...]`). The parallel effect is rejected as soon as one of the sub-effects is rejected (as implied by `Promise.all`). In this case, all the other sub-effects are automatically cancelled.
2. In a parallel effect (`yield all([...])`). The parallel effect is rejected as soon as one of the sub-effects is rejected (as implied by `Promise.all`). In this case, all the other sub-effects are automatically cancelled.
34 changes: 28 additions & 6 deletions docs/api/README.md
Expand Up @@ -35,7 +35,8 @@
* [`cancelled()`](#cancelled)
* [`Effect combinators`](#effect-combinators)
* [`race(effects)`](#raceeffects)
* [`[...effects] (aka parallel effects)`](#effects-parallel-effects)
* [`all([...effects]) (aka parallel effects)`](#alleffects-parallel-effects)
* [`all(effects)`](#alleffects)
* [`Interfaces`](#interfaces)
* [`Task`](#task)
* [`Channel`](#channel)
Expand Down Expand Up @@ -745,7 +746,7 @@ function* saga() {
### `race(effects)`

Creates an Effect description that instructs the middleware to run a *Race* between
multiple Effects (this is similar to how `Promise.race([...])` behaves).
multiple Effects (this is similar to how [`Promise.race([...])`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) behaves).

`effects: Object` - a dictionary Object of the form {label: effect, ...}

Expand Down Expand Up @@ -778,10 +779,10 @@ will be a single keyed object `{cancel: action}`, where action is the dispatched

When resolving a `race`, the middleware automatically cancels all the losing Effects.

### `[...effects] (parallel effects)`
### `all([...effects]) - parallel effects`

Creates an Effect description that instructs the middleware to run multiple Effects
in parallel and wait for all of them to complete.
in parallel and wait for all of them to complete. It's quite the corresponding API to standard [`Promise#all`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise/all).

#### Example

Expand All @@ -791,10 +792,31 @@ The following example runs two blocking calls in parallel:
import { fetchCustomers, fetchProducts } from './path/to/api'

function* mySaga() {
const [customers, products] = yield [
const [customers, products] = yield all([
call(fetchCustomers),
call(fetchProducts)
]
])
}
```

### `all(effects)`

The same as [`all([...effects])`](#alleffects-parallel-effects) but let's you to pass in a dictionary object of effects with labels, just like [`race(effects)`](#alleffects)

`effects: Object` - a dictionary Object of the form {label: effect, ...}

#### Example

The following example runs two blocking calls in parallel:

```javascript
import { fetchCustomers, fetchProducts } from './path/to/api'

function* mySaga() {
const { customers, products } = yield all({
customers: call(fetchCustomers),
products: call(fetchProducts)
})
}
```

Expand Down
6 changes: 3 additions & 3 deletions docs/introduction/BeginnerTutorial.md
Expand Up @@ -108,7 +108,7 @@ function render() {
<Counter
value={store.getState()}
onIncrement={() => action('INCREMENT')}
onDecrement={() => action('DECREMENT')}
onDecrement={() => action('DECREMENT')}
onIncrementAsync={() => action('INCREMENT_ASYNC')} />,
document.getElementById('root')
)
Expand Down Expand Up @@ -160,10 +160,10 @@ Now we have 2 Sagas, and we need to start them both at once. To do that, we'll a
```javascript
// single entry point to start all Sagas at once
export default function* rootSaga() {
yield [
yield all([
helloSaga(),
watchIncrementAsync()
]
])
}
```

Expand Down
6 changes: 3 additions & 3 deletions examples/real-world/sagas/index.js
@@ -1,5 +1,5 @@
/* eslint-disable no-constant-condition */
import { take, put, call, fork, select } from 'redux-saga/effects'
import { take, put, call, fork, select, all } from 'redux-saga/effects'
import { api, history } from '../services'
import * as actions from '../actions'
import { getUser, getRepo, getStarredByUser, getStargazersByRepo } from '../reducers/selectors'
Expand Down Expand Up @@ -120,11 +120,11 @@ function* watchLoadMoreStargazers() {
}

export default function* root() {
yield [
yield all([
fork(watchNavigate),
fork(watchLoadUserPage),
fork(watchLoadRepoPage),
fork(watchLoadMoreStarred),
fork(watchLoadMoreStargazers)
]
])
}
6 changes: 3 additions & 3 deletions examples/shopping-cart/src/sagas/index.js
@@ -1,6 +1,6 @@
/* eslint-disable no-constant-condition */

import { take, put, call, fork, select, takeEvery } from '../../../../src/effects'
import { take, put, call, fork, select, takeEvery, all } from '../../../../src/effects'
import * as actions from '../actions'
import { getCart } from '../reducers'
import { api } from '../services'
Expand Down Expand Up @@ -43,9 +43,9 @@ export function* watchCheckout() {
}

export default function* root() {
yield [
yield all([
fork(getAllProducts),
fork(watchGetProducts),
fork(watchCheckout)
]
])
}
24 changes: 23 additions & 1 deletion src/effects.js
@@ -1 +1,23 @@
export { take, takem, put, race, call, apply, cps, fork, spawn, join, cancel, select, actionChannel, cancelled, flush, getContext, setContext, takeEvery, takeLatest, throttle } from './internal/io'
export {
take,
takem,
put,
all,
race,
call,
apply,
cps,
fork,
spawn,
join,
cancel,
select,
actionChannel,
cancelled,
flush,
getContext,
setContext,
takeEvery,
takeLatest,
throttle
} from './internal/io'
15 changes: 9 additions & 6 deletions src/internal/io.js
@@ -1,9 +1,10 @@
import { sym, is, ident, check, deprecate, createSetContextWarning, SELF_CANCELLATION } from './utils'
import { sym, is, ident, check, deprecate, updateIncentive, createSetContextWarning, SELF_CANCELLATION } from './utils'
import { takeEveryHelper, takeLatestHelper, throttleHelper } from './sagaHelpers'

const IO = sym('IO')
const TAKE = 'TAKE'
const PUT = 'PUT'
const ALL = 'ALL'
const RACE = 'RACE'
const CALL = 'CALL'
const CPS = 'CPS'
Expand All @@ -19,9 +20,6 @@ const SET_CONTEXT = 'SET_CONTEXT'

const TEST_HINT = '\n(HINT: if you are getting this errors in tests, consider using createMockTask from redux-saga/utils)'

const deprecationWarning = (deprecated, preferred) =>
`${ deprecated } has been deprecated in favor of ${ preferred }, please update your code`

const effect = (type, payload) => ({[IO]: true, [type]: payload})

export function take(patternOrChannel = '*') {
Expand All @@ -43,7 +41,7 @@ take.maybe = (...args) => {
return eff
}

export const takem = deprecate(take.maybe, deprecationWarning('takem', 'take.maybe'))
export const takem = deprecate(take.maybe, updateIncentive('takem', 'take.maybe'))

export function put(channel, action) {
if(arguments.length > 1) {
Expand All @@ -64,7 +62,11 @@ put.resolve = (...args) => {
return eff
}

put.sync = deprecate(put.resolve, deprecationWarning('put.sync', 'put.resolve'))
put.sync = deprecate(put.resolve, updateIncentive('put.sync', 'put.resolve'))

export function all(effects) {
return effect(ALL, effects)
}

export function race(effects) {
return effect(RACE, effects)
Expand Down Expand Up @@ -189,6 +191,7 @@ const createAsEffectType = type => effect => effect && effect[IO] && effect[type
export const asEffect = {
take : createAsEffectType(TAKE),
put : createAsEffectType(PUT),
all : createAsEffectType(ALL),
race : createAsEffectType(RACE),
call : createAsEffectType(CALL),
cps : createAsEffectType(CPS),
Expand Down

0 comments on commit 7078c20

Please sign in to comment.