一些使用 redux-saga
的预备知识
Callback 的问题
Promise 解决的问题
Generator 解决的问题
async/await
从哪里来
不会去惹隔壁家的孩子
- Function (aka callback, 基石, 异步界的先民)
- Promise (中流砥柱, 弄潮儿他妈)
- Generator (弄潮儿他爹)
- Async Function (aka async/await, 整条gai最靓的仔, 弄潮儿)
- Observable (别人家的孩子)
- Observer
- Pub-Sub
- CSP
这里不会去关注别的模式, 只看下语言本身的异步方案
Function / Promise / Generator / Async Function
What's the problem for callback?
$.get('/api', callback);
- Call the callback too early
- Call the callback too late (or never)
- Call the callback too few or too many times
- Fail to pass along any necessary environment/parameters to your callback
- Swallow any errors/exceptions that may happen
- ...
异步必须面对的问题: 串行, 并行
$.get('/api/a', (a) => {
$.get('/api/b/' + a, (b) => {
// do something with b
console.log(2);
});
});
console.log(1);
const result = {};
function saveResultAndCheckAndDoNext(field) {
return function(res) {
result[field] = res;
if (result.a && result.b) {
// do next with a and b
}
};
}
$.get('/api/a', saveResultAndCheckAndDoNext('a'));
$.get('/api/b', saveResultAndCheckAndDoNext('b'));
为了简化代码, 可能需要一些library, 比如 run-series, run-parallel What the heck, why does JS not provide these features?
fetch('/api')
.then(callback1)
.then(callback2)
.catch(handleError);
Problems solved by Promise (就相信一下promise的名字吧)
- Call the callback too early
- Call the callback too late
- Call the callback too few or too many times
- Fail to pass along any necessary environment/parameters to your callback
- Swallow any errors/exceptions that may happen
- run series
可以链式调用
then
, 模板代码then
以及必须写一些处理的callback
- run parallel
Promise.all
,Promise.race
function* hi(number) {
console.log(number);
yield 'hello';
yield 'world';
return 'hi';
}
const iterator = hi(1);
const duckIterator = {
[Symbol.iterator]() { return this; },
next() {
return { done, value }
},
return() {},
throw() {},
}
const iterator = hi(1);
// no output
const a = iterator.next();
// 1
// {value: "hello", done: false}
const b = iterator.next();
// {value: "world", done: false}
const c = iterator.next();
// {value: undefined, done: true}
for (let value of hi()) {
console.log(value);
}
// hello
// world
// undefined
- co: co-operative
- routines: functions
交互式地来段coroutine的代码 一个 normal function 有确定的目的/行为, 但是 generator function 你看不出来
function* hi() {
const a = yield 'hello';
const b = yield 'world';
return a + b;
}
// run manually, uppercase
const iterator = hi();
const hello = iterator.next();
console.log(hello); // {value: "hello", done: false}
const world = iterator.next(hello.value.toUpperCase());
console.log(world); // {value: "world", done: false}
const result = iterator.next(world.value.toUpperCase());
console.log(result); // {value: "HELLOWORLD", done: true}
// run manually, count characters
const iterator = hi();
const hello = iterator.next();
console.log(hello); // {value: "hello", done: false}
const world = iterator.next(hello.value.length());
console.log(world); // {value: "world", done: false}
const result = iterator.next(world.value.length());
console.log(result); // {value: 10, done: true}
// run automatically, uppercase
function run(gen) {
const iterator = gen();
function next(v) {
const { done, value } = iterator.next(v);
if (done) {
return value;
}
if (typeof value === 'string') {
return next(value.toUpperCase());
}
return next(value);
}
return next();
}
run(hi); // "HELLOWORLD"
yield a async stuff
function* foo() {
const res = yield asyncStuff;
}
What’s a thunk?!
A thunk is a function that wraps an expression to delay its evaluation
const x = 1 + 2;
const foo = () => 1 + 2;
function* foo() {
const result = yield $.get('/api');
}
function* foo() {
const result = yield () => $.get('/api');
}
function* foo() {
const result = yield (callback) => $.get('/api', callback);
}
function* foo() {
const res = yield (callback) => $.get('/api', callback);
}
function run(gen) {
const g = gen();
function next(v) {
const { done, value } = g.next(v);
if (done) return;
if (typeof value === 'function') {
value(next);
return;
}
next(value);
}
next();
}
function* foo() {
const result = yield fetch('/api');
}
function run(gen) {
const g = gen();
function next(v) {
const { done, value } = g.next(v);
if (done) return;
if (value instanceof Promise) {
value
.then(next)
.catch(g.throw);
return;
}
next(value);
}
next();
}
function* foo() {
const effect = {
'@@redux-saga/IO': true,
combinator: false,
type: 'CALL',
payload: { context: null, fn: [Function: fetch], args: [ '/api' ] }
};
const res = yield effect; // yield a side effect descriptor
}
function* modifier() {
yield 'nice';
yield 'new';
}
function* hi() {
yield 'hello';
const res = yield* modifier();
yield 'world';
}
for (let word of hi()) {
console.log(word);
}
pattern: generators yielding Promises that then control the generator's iterator to advance it to completion
co(function* foo() {
const res = yield fetch('/api');
})
async function foo() {
const res = await fetch('/api');
}
async function foo() {
return 'foo';
}
foo();
const delay = (n) => Promise.resolve(n);
async function* foo() {
yield 'hello';
const res = await delay(10);
yield 'world';
}
const asyncIterator = foo(); // [Symbol.asyncIterator]
for await (let word of asyncIterator) {
console.log(word);
}
比较流行的side effects middlewares
callback | promise | generator | observable |
---|---|---|---|
redux-thunk | redux-promise | redux-saga | redux-observable |
Where does middleware apply to?
UI event -> actionCreator -> dispatch(action) -> reducer -> update UI
|
middleware
// applyMiddleware.js
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
({ getState, dispatch }) => next => action => {}
an official middleware demo
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
}
Stack Overflow: Why do we need middleware for async flow in Redux?
Where should we do things that have side effects?
UI event -> actionCreator -> dispatch(action) -> reducer -> update UI
|_______________________________________|
const thunkMiddleware = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}
return isPromise(action.payload)
? action.payload
.then(result => dispatch({ ...action, payload: result }))
.catch(error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}
不像
redux-thunk
和redux-promise
,redux-saga
不会直接去处理原始action
, 而是匹配action
到不同的 watcher 去执行不同的操作
function sagaMiddleware({ getState, dispatch }) {
return next => action => {
const result = next(action); // hit reducers
channel.put(action);
return result;
}
}
UI event -> actionCreator -> dispatch(action) -> reducer -> update UI
| |
| \|/
| .
| channel.put(action)
| { type: 'GET_USER' }
/|\ |
| \|/
| .
| channel
| |
| \|/
| .
getUser ------- takeLatest('GET_USER', getUser)
exec side effects
or dispatch new action
Worker Watcher
import { takeLatest, call, put } from 'redux-saga/effects';
const API = {
getUser() {
return Promise.resolve({ name: 'foo' });
},
};
function* getUser() {
yield put({
type: 'GET_USER_PENDING',
});
const user = yield call(API.getUser);
yield put({
type: 'GET_USER_SUCCESS',
payload: user,
});
}
export default function* mySaga() {
yield takeLatest('GET_USER', getUser);
}
const epicMiddleware = _store => {
return next => {
return action => {
// Downstream middleware gets the action first,
// which includes their reducers, so state is
// updated before epics receive the action
const result = next(action);
// It's important to update the state$ before we emit
// the action because otherwise it would be stale
stateSubject$.next(store.getState());
actionSubject$.next(action);
return result;
};
};
}
作为 redux-saga 系列的前传, 希望有一个粗浅地理解.
coroutine, redux middleware, redux-saga 有些相同的模式, 比如都会根据不同的type去做不同的处理
coroutine | redux middleware | redux-saga |
---|---|---|
yield something | action/action.payload | Effect |
- Book: You Don't Know JS: Async & Performance
- Book: The 80/20 Guide to ES2015 Generators
- Article: Async Generator Functions in JavaScript
- YouTube: Understanding Generator Functions & Using Redux Saga
- YouTube: Netflix JavaScript Talks - RxJS + Redux + React = Amazing!
- YouTube: Advanced Async and Concurrency Patterns in JavaScript
- Stack Overflow: Dispatching Redux Actions with a Timeout
- Stack Overflow: Why do we need middleware for async flow in Redux?