일.목표 위치
redux-saga is a library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.
Redux 미들웨어로서, Redux 애플리케이션 중의 부작용 (즉 외부 환경에 의존/영향하는 불순한 부분) 처리를 더욱 우아하게 수행하는 것을 목표로 합니다
이.설계 이념
Saga 는 독립 스레드처럼, 부작용 처리를 전문으로 담당하며, 여러 Saga 는 직렬/병렬로 조합할 수 있고, redux-saga 가 스케줄링 관리를 담당합니다
Saga 는 유래가 큽니다 (1W star 는 허명이 아닙니다), 어떤 논문 에서 제안된 분산 트랜잭션 메커니즘 으로, 장기 실행 비즈니스 프로세스를 관리하는 데 사용됩니다
P.S. Saga 배경에 대한 더 많은 정보는, Background on the Saga concept 참조
삼.코어 구현
generator 를 이용하여, 비동기 플로우 제어를 읽기 쉽고, 우아하게, 테스트하기 쉽게 만듭니다
In redux-saga, Sagas are implemented using Generator functions. To express the Saga logic we yield plain JavaScript Objects from the Generator.
구현상, 핵심적인 점 은:
-
generator 형식으로 로직 시퀀스를 조직 (
function* + yield), 일련의 직렬/병렬 조작을yield로 분할 -
iterator 의「일시 정지/회복」특성 (
iter.next()) 을 이용하여 단계적으로 실행 -
iterator 를 통해 내부 상태에 영향 (
iter.next(result)), 비동기 조작 결과를 주입 -
iterator 의 오류 캡처 특성 (
iter.throw(error)) 을 이용하여, 비동기 조작 예외를 주입
generator/iterator 를 사용하여 구현하는 것은, 그것이 플로우 제어의 씬에 매우 적합 하기 때문이며,体现在:
-
yield가 직렬/병렬의 비동기 조작을 기술하는 것을 매우 우아하게 만듭니다 -
동기 형식으로 비동기 조작 결과를 취득하며, 순서 실행의 직관에 부합
-
동기 형식으로 비동기 오류를 캡처하며, 우아하게 비동기 오류를 캡처
P.S. generator 와 iterator 의 관계 및 generator 기초 용법에 대해서는, [generator(생성기)_ES6 노트 2](/articles/generator(생성기)-es6 노트 2/) 참조
예를 들어:
const ts = Date.now();
function asyncFn(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`${id} at ${Date.now() - ts}`);
resolve(id);
}, 1000);
});
}
function* gen() {
// 串行异步
let A = yield asyncFn('A');
console.log(A);
let B = yield asyncFn('B');
console.log(B);
// 并行异步
let C = yield Promise.all([asyncFn('C1'), asyncFn('C2')]);
console.log(C);
// 串行/并行组合异步
let D = yield Promise.all([
asyncFn('D1-1').then(() => {
return asyncFn('D1-2');
}),
asyncFn('D2')
]);
console.log(D);
}
// test
let iter = gen();
// 尾触发顺序执行 iter.next
let next = function(prevResult) {
let {value: result, done} = iter.next(prevResult);
if (result instanceof Promise) {
result.then((res) => {
if (!done) next(res);
}, (err) => {
iter.throw(err);
});
}
else {
if (!done) next(result);
}
};
next();
실제 결과는 예상대로:
A at 1002
A
B at 2012
B
C1 at 3015
C2 at 3015
["C1", "C2"]
D1-1 at 4019
D2 at 4020
D1-2 at 5022
["D1-2", "D2"]
실행 순서는: A -> B -> C1,C2 -> D1-1 -> D2 -> D1-2
redux-saga 의 코어 제어 부분은 위 예시와 유사 (네, 정확히 co 같습니다), 구현에서 보면, 그 비동기 제어의 핵심은 尾触发 순서 실행 iter.next 입니다. 예시는 Effect 라는 층의 설명 오브젝트를 추가하지 않았지만, 기능상 Effect 는 중요하지 않습니다 (Effect 의 작용은 아래 용어 개념 부분 참조)
Effect 층이 구현해야 할 것은 2 부분을 포함:
-
비즈니스 조작 -> Effect
Effect creator API 형식으로 제공, 각종 의미의 Effect 를 생성하기 위한 툴 함수를 제공. 예를 들어 dispatch action 을
put에 패키징, 메서드 호출을call/apply에 패키징 -
Effect -> 비즈니스 조작
실행 시 내부에서 변환. 예를 들어
[Effect1, Effect2]를 병렬 호출로 변환
装箱 (비즈니스 조작을 Effect 로 감싸기) 과拆箱 (Effect 내의 비즈니스 조작 실행) 과 유사.此外, 완전한 redux-saga 는 더욱 구현해야 합니다:
-
middleware 로서 Redux 에接入
-
Redux state 의 읽기/쓰기 인터페이스를 제공 (select/put)
-
action 을 감시하는 인터페이스를 제공 (take/takeEvery/takeLatest)
-
Sagas 조합, 통신
-
task 순서 제어, 취소
-
action 병행 제어
-
...
거의 大而全의 비동기 플로우 제어 라이브러리 로, 구현에서 보면, 增强版의 co 에 상당합니다
사.용어 개념
Effect
Effect 는 설명 오브젝트를 가리키며, redux-saga 미들웨어가 식별 가능한 조작 명령에 상당. 예를 들어 지정 비즈니스 메서드를 호출 (call(myFn)), 지정 action 을 dispatch(put(action))
An Effect is simply an object which contains some information to be interpreted by the middleware.
Effect 층이 존재하는 주요 의의는 테스트 용이성 때문이며, 따라서 간단한 설명 오브젝트로 조작을 나타내고, 이 층의 명령을 추가
직접 Promise 를 yield 할 수도 있지만 (예를 들어 위 코어 구현의 예시처럼), 테스트 케이스 중에서 2 개의 promise 가 등가인지 비교할 수 없습니다. 따라서 이 문제를 해결하기 위해 설명 오브젝트의 층을 추가하고, 테스트 케이스 중에서 간단히 설명 오브젝트를 비교할 수 있으며, 실제로 작용하는 Promise 는 redux-saga 내부에서 생성
이렇게 하는 이점은 유닛 테스트 중에서 비동기 메서드를 mock 할 필요가 없고 (일반 유닛 테스트 중에는 모든 비동기 메서드를 교체하고,传入 파라미터가 같은지만 비교하며, 실제 조작은 수행하지 않음), 조작 명령 (Effect) 이 등가인지 간단히 비교할 수 있습니다. 유닛 테스트���角度来看, Effect 는 파라미터를 추출한 것에 상당하며, 「传入 파라미터가 같은지 비교」이 단계를 밖에서 통일하여 수행할 수 있고,逐个 mock 교체할 필요가 없습니다
P.S. 테스트 용이성에 대한 더 많은 정보는, Testing Sagas 참조
另外, mock 테스트는 비교 번거로울 뿐만 아니라, 신뢰성이 낮습니다.畢竟 진정한 씬/플로우와 차이가 있습니다. 프레임워크 제약을 통해, 설명 오브젝트의 층을 추가하여 mock 을 회피
이렇게 하는 것은 완전히 완벽하지 않으며, 아직 2 개의 문제가 존재:
-
비즈니스 코드가 조금 번거로움 (직접
yield promise/dispatch action하지 않고, 모두 프레임워크가 제공하는 creator(call, put) 로 감싸야 함) -
추가의 학습 비용이 있음 (각 creator 의 의미를 이해하고, 먼저 감싸는玩法에 적응)
예를 들어:
// 直接
const userInfo = yield API.fetch('user/info', userId);
// 包一层 creator
const userInfo = yield call(API.fetch, 'user/info', userId);
// 并指定 context,默认是 null
const userInfo = yield call([myContext, API.fetch], 'user/info', userId);
형식상은 fn.call 과 유사 (실제로도 apply creator 를 제공, 형식은 fn.apply 와 유사), 내부 처리도 유사:
// call 返回的描述对象(Effect)
{
@@redux-saga/IO: true,
CALL: {
args: ["user/info", userId],
context: myContext,
fn: fetch
}
}
// 实际执行
result = fn.apply(context, args)
쓰기가 그다지 직접적이지는 않지만, 테스트 용이성이 가져오는 이점 (비동기 함수를 mock 할 필요가 없음) 과 비교하면, 이는 그다지 과분하지 않습니다
주의, 비동기 함수를 mock 할 필요가 없는 것은 유닛 테스트의 1 개의环节을 간소화했을 뿐 으로, 이러한 설명 오브젝트를 대비하는 방식을 사용해도, 여전히 예상 데이터를 제공할 필요 가 있습니다. 예를 들어:
// 测试场景直接执行
const iterator = fetchProducts()
// expects a call instruction
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
// 预期接口返回数据
const products = {}
// expects a dispatch instruction
assert.deepEqual(
iterator.next(products).value,
put({ type: 'PRODUCTS_RECEIVED', products }),
"fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED', products })"
)
P.S. 이 설명 오브젝트의 套路는, Flux/Redux 의 action 과 동일: Effect 는 Action 에 상당, Effect creator 는 Action Creator 에 상당. 차이 는 Flux 는 action 으로 메시지를 기술 (무엇이 발생했는가), redux-saga 는 Effect 로 조작 명령을 기술 (무엇을 하는가)
Effect creator
redux-saga/effects 는 Effect 를 생성하기 위한 많은 툴 메서드를 제공합니다.常用的 Effect creator 는 다음과 같습니다:
-
阻塞型 메서드 호출:call/apply 상세는 Declarative Effects
-
非阻塞型 메서드 호출:fork/spawn 상세는 redux-saga's fork model
-
task 병렬 실행:all/race 상세는 Running Tasks In Parallel, Starting a race between multiple Effects
-
state 읽기/쓰기:select/put 상세는 Pulling future actions
-
task 제어:join/cancel/cancelled 상세는 Task cancellation
大多 creator 의 의미는 매우 직관적이며, 1 개만 추가 설명이 필요 합니다:
join은 非阻塞의 task 의 되돌림 결과를 취득하는 데 사용
그 중에서 fork 와 spawn 은 모두 非阻塞型 메서드 호출이며, 양자의 차이는:
-
spawn으로 실행되는 task 는 완전히 독립적이며, 현재의 saga 와 무관현재의 saga 는 그것이 실행 완료했는지 여부 신경 쓰지 않으며,
cancel/error가 발생해도 현재의 saga 에 영향을 주지 않음효과는 지정 task 를 독립적으로 톱레벨에서 실행시키는 것에 상당,
middleware.run(rootSaga)와 유사 -
fork로 실행되는 task 는 현재의 saga 와 관계fork가所在하는 saga 는 forked task 를 대기, 모든 forked task 가 실행 종료 후에만, 현재의 saga 가 종료fork의 실행 메커니즘은all과 완전히 일치, cancel 과 error 의 전달 방식을 포함하여, 따라서 任一 task 에 미캡처의 error 가 있으면, 현재의 saga 도 종료
另外, cancel 메커니즘 은 매우 재미있습니다:
실행 중인 task 시퀀스에 대해, 모든 task 가 자연스럽게 완성된 때, 결과를 위에 전달하여 대首에, 상층의某个 yield 의 되돌림 값으로. 만약 task 시퀀스가 처리過程中에 cancel 된 경우, cancel 신호를 아래에 전달하여, 실행 중인 모든 pending task 를 취소.另外, cancel 신호를 join 鏈을 따라 위에 전달하여, 해당 task 에 의존하는 모든 task 의 실행을 취소
간단히 말하면:complete 신호는 호출鏈을 따라反向로 전달, cancel 신호는 task 鏈을 따라正向로 전달, join 鏈을 따라反向로 전달
주의:yield cancel(task) 도 非阻塞 (fork 와 유사), cancel 된 task 는善後로직을 완성 후 즉시 돌아갑니다
P.S. join 을 통해 의존 관계를建立 (task 결과를 취득), 예를 들어:
function* rootSaga() {
// Returns immediately with a Task object
const task = yield spawn(serverHello, 'world');
// Perform an effect in the meantime
yield call(console.log, "waiting on server result...");
// Block on the result of serverHello
const result = yield join(task);
}
Saga
용어 Saga 는 일련의 조작의 집합을 가리키며, 실행 시의 추상 개념
redux-saga 내의 Saga 는 형식상 generator 로, 한 세트의 조작을 기술하는 데 사용, generator 는 구체적인 정적 개념
P.S. redux-saga 내에서 말하는 Saga 는 대부분의 경우 generator 형식의 한 세트의 조작을 가리키며, redux-saga 자신을 가리키는 것이 아닙니다. 간단히 이해 한다면:redux-saga 내에서, Saga 는 generator, Sagas 는 여러 개의 generator
Sagas 에는 2 종의 순서 조합 방식:
-
yield* saga() -
call(saga)
마찬가지로, 직접 yield* iterator 를 실행 시에 전개해도 테스트하기 어려운 문제 에 직면하므로, call 로 Effect 의 층을 감쌉니다.另外, yield* 는 1 개의 iterator 만을 받아들여, 조합하는 것이 매우 불편. 예를 들어:
function* saga1() {
yield 1;
yield 2;
}
function* saga2() {
yield 3;
yield 4;
}
function* rootSaga() {
yield 0;
// 组合多个 generator 不方便
yield* (function*() {
yield* saga1();
yield* saga2();
})();
yield 5;
}
// test
for (let val of rootSaga()) {
console.log(val); // 0 1 2 3 4 5
}
주의:실제, call(saga) 가 되돌리는 Effect 는其它타입의 Effect 와 본질적인 차이는 없으며, all/race 를 통해 조합하는 것도 가능
Saga Helpers
Saga Helper 는 action 을 감시하는 데 사용, API 형식은 takeXXX, 그 의미는 addActionListener 에 상당:
-
take:의미는 once 에 상당
-
takeEvery:의미는 on 에 상당, 병행 action 을 허용 (이전의 것이 미완료라도 즉시 다음을 시작)
-
takeLatest:제한판의 on, 병행 action 을 허용하지 않음 (pending 시에 또 오면 pending 을 cancel, 최신만 실행)
takeEvery, takeLatest 는 take 의 상에서의 봉쇄, take 가底层 API, 유연성이 최대, 수동으로 각종 씬을 만족
P.S. 3 자의 관계에 대한 더 많은 정보는, Concurrency 참조
pull action 과 push action
제어 방식上讲, take 는 pull 의 방식, takeEvery, takeLatest 는 push 의 방식
pull 과 push 란:
-
pull action:업무 측이主動으로 action 을 취득하는 것을 요구 (
yeild take()는 action 을 되돌림) -
push action:프레임워크가 외부에서 action 을 주입 (
takeEvery/takeLatest로 등록된 Saga 는 action 파라미터를 주입)
pull 방식의 우위 는:
-
더욱 정밀한 제어를 허용
예를 들어 수동으로 takeN 의 효과를 실현 (某数회의 action 만을关注, 사용 끝나면 해제)
-
동기 형식으로 제어 플로우를 기술
takeEvery, takeLatest는 단 action 만을 서포트, action 시퀀스의 경우는拆分,take를 사용하면 관련 로직 블록의 완전성을 보존, 예를 들어 로그인/로그아웃 -
타인이 이해하기 쉬움
제어 로직은 비즈니스 코드 내에 있고, 프레임워크 내부 메커니즘에 숨어 있지 않아, 일정 정도로 유지 비용을 저감
P.S. pull/push 에 대한 더 많은 정보는, Pulling future actions 참조
오.씬 예시
몇 가지 인상이 깊은 씬이 있으며, redux-saga 의 우아함을 충분히 체현
인터페이스 액세스
function* fetchProducts() {
try {
const products = yield call(Api.fetch, '/products')
yield put({ type: 'PRODUCTS_RECEIVED', products })
}
catch(error) {
yield put({ type: 'PRODUCTS_REQUEST_FAILED', error })
}
}
put 가 dispatch action 을 나타내는 것을 아는 외, 거의 주석은 불필요, 실제 상황은 당신이 생각한 대로
로그인/로그아웃
function* loginFlow() {
while (true) {
yield take('LOGIN')
// ... perform the login logic
yield take('LOGOUT')
// ... perform the logout logic
}
}
pull action 은 관련 action 의 처리 순서를 보존할 수 있고, 추가의 외부 상태 제어는 불필요. 이렇게 하여 LOGOUT 이 항상 LOGIN 을 실행한 후의某个시각에 발생하는 것을 보증, 코드는 매우 아름답게 보입니다
특정 조작 힌트
// 在创建第 3 条 todo 的时候,给出提示消息
function* watchFirstThreeTodosCreation() {
for (let i = 0; i < 3; i++) {
const action = yield take('TODO_CREATED')
}
yield put({type: 'SHOW_CONGRATULATION'})
}
// 接口访问异常重试
function* updateApi(data) {
for(let i = 0; i < 5; i++) {
try {
const apiResponse = yield call(apiRequest, { data });
return apiResponse;
} catch(err) {
if(i < 4) {
yield call(delay, 2000);
}
}
}
// attempts failed after 5 attempts
throw new Error('API request failed');
}
즉 takeN 의 예시, 이렇게 하여 본래 reducer 중에 존재해야 할 부작용을 밖에 끌어올리고, reducer 의 순도를 보증
육.장단점
장점:
-
테스트하기 쉽고, 각종 case 의 테스트 방안을 제공, mock task, 분기 커버리지 등을 포함
-
大而全의 비동기 제어 라이브러리, 비동기 플로우 제어에서 병행 제어까지 모두 있음
-
완비된 오류 캡처 메커니즘, 阻塞型 오류는 try-catch 가능, 非阻塞型 은 소속 Saga 에 통보
-
우아한 플로우 제어, 가독성/정련도는
async&await와 그다지 차이가 없고, 병렬 조작을 기술하기 쉬움
단점:
-
체적이 다소 크고, 1700 행, min 판 24KB, 실제 병행 제어 등의 기능은 사용하는 것이 어려움
-
ES6 generator 특성에 의존, polyfill 이 필요할지도 모름
P.S. redux-saga 는其它환경에도接入가능 (Redux 와綁定하지 않음), 상세는 Connecting Sagas to external Input/Output 참조
아직 댓글이 없습니다