본문으로 건너뛰기

redux-saga

무료2017-10-13#JS#redux saga effect#redux saga join#redux saga教程#redux saga入门

redux-saga 를 완전히 이해하다

일.목표 위치

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 는 다음과 같습니다:

大多 creator 의 의미는 매우 직관적이며, 1 개만 추가 설명이 필요 합니다:

  • join 은 非阻塞의 task 의 되돌림 결과를 취득하는 데 사용

그 중에서 forkspawn 은 모두 非阻塞型 메서드 호출이며, 양자의 차이는:

  • 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, takeLatesttake 의 상에서의 봉쇄, 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 참조

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성