YeonjuOHYE / javascript

0 stars 0 forks source link

제너레이터 #26

Open YeonjuOHYE opened 3 years ago

YeonjuOHYE commented 3 years ago

참고 및 출처 : https://mygumi.tistory.com/370

제너레이터

비동기 흐름 제어를 순차적/동기적으로 나타낼 수 있다.

Screen Shot 2021-02-23 at 4 29 20 AM

완전-실행(Run to Completion) 타파

var x = 1;
function foo() {
x++;
bar();
console.log("x:" , x);
}

function bar() {
  x++;
}

foo();

Q. bar()가 없는데도 x++, console.log(x) 사이에서 bar()가 실행될 수 있을까? A.

var x = 1;
function *foo() {
   x++;
   yield;
   console.log("x:" , x);
}
function bar() {
  x++;
}

foo();
var it = foo(); // 제너레이터를 제어할 이터레이터 
it.next(); // 제너레이터 시작, x++; 실행
x;  //2
bar(); // x++
x; //3
it.next(): // 멈췄던 곳에서 재개 console.log 실행

generator 표기법 : function foo() , function foo, function*foo => 취향대로

Generator도 함수로써 인자를 받고 반환하는 기능은 일반 함수와 같다.

function *foo(x,y) {
    return x * y;
}
var it = foo(6,7);
var res = it.next();
res.value;

next()의 결괏값은 *foo()가 반환한 값을 value 프로퍼티에 저장한 객체 yield는 실행 도중에 제너레이터로부터 값을 일종의 중간 반환 값 형태로 돌려줌

메세징

yield와 next를 통해 입력/출력 메시지를 주고 받는다

function *foo(x) {
 var y = x* (yield);
 return y;
}

var it = foo(6);
it.next();

var res = it.next(7);
res.value;
  1. it.next() 실행
  2. 제너레이터의 yield 중간에서 실행을 멈추고, yield 표현식에 해당하는 결괏값을 달라고 호출부 코드에 요청
  3. it.next(7)을 호출하면 7값이 yield 표현식의 결과값이 되도록 전달.
    y = 6*7가 반환

=>표현식 yield는 next 호출에 대응하여 메시지를 보낼 수 있고 next는 멈춘 yield 표현식으로 값을 전송할 수 있다.

function *foo(x) {
  var y = x * (yield "hello");
 return y;
}

var it = foo(6);
// 'foo()' 를 시작
it.next();
res.value; // hello
res = it.next(7);
res.value; // 42

다중 이터레이터

같은 제너레이터의 인스턴스를 (1)동시에 여러개 실행할 수 있고 (2)인스턴스끼리 상호 작용도 가능

function *foo() {
  var x = yield 2;
 z++:
 var y = yield(x * z);
console.log(x,y,z);
}
var z = 1;
var it1 = foo();
var it2 = foo();
var var1 = it1.next().value; // 2 <= yied 2
var var2 = it2.next().value; // 2 <= yield 2

val1  = it1.next(val2 * 10).value; // 40 <= x:20, z:2
val2 = it2.next(val1*5).value; // 600 <= x:200, z:3

it1.next(val2 /2); // y : 300
it2.next(val1/2) // y : 10

인터리빙

var a = 1;
var b = 2;

function foo() {
    a++;
    b = b * a;
    a = b + 3;
}

function bar() {
    b--;
    a = 8 + b;
    b = a * 2;
}

▲ 일반 자바스크립트 함수애서 함수의 개별문을 다른 함수에서 인터리빙하여 실행하는 것은 불가능 따라서 위의 경우 함수 순서에 따라 두가지 결과만 가능

하지만 제너레이터에서는 가능하다.

var a = 1;
var b = 2;

function *foo() {
    a++;
    yield;
    b = b * a;
    a = (yield b) + 3;
}

function *bar() {
    b--;
    yield;
    a = (yield 8) + b;
    b = a * (yield 2);
}
function step(gen) {
    var it = gen();
    var last;

    return function() {
        // 어떤 값이 'yield'되어 나오든지
        // 바로 다음에 그냥 반환
        last = it.next( last ).value;
    };
}
// make sure to reset `a` and `b`
a = 1;
b = 2;

var s1 = step( foo );
var s2 = step( bar );

// run `*foo()` completely first
s1(); 2,2
s1(); 2,4
s1(); 7,4

// now run `*bar()`
s2(); 7,3
s2(); 7,3
s2(); 11,3
s2(); 11,22

console.log( a, b );    // 11 22

제너레이터와 이터레이터

Q. 제너레이터로 어떻게 값들을 만들어 낼까? A. 제너레이터는 일종의 값을 생산하는 공장 이렇게 생산된 값들을 이터레이터 인터페이스의 next() 를 호출하여 한번에 하나씩 추출

제너레이터를 실행하면 이터레이터를 돌려받는다

무한수열 generator

function *something() {
    var nextVal;

    while (true) {
        if (nextVal === undefined) {
            nextVal = 1;
        }
        else {
            nextVal = (3 * nextVal) + 6;
        }

        yield nextVal;
    }
}

제너레이터는 이터레이터를 반환하기 때문에 for・・・of 루프에 얹을 수 있다.

for (var v of something) {
    console.log( v );

    // don't let the loop run forever!
    if (v > 500) {
        break;
    }
}
// 1 9 33 105 321 969

cf. 클로저를 이용한 무한수열 생성 함수 상태값 보존을 위해 매번 클로저를 반환받아야함

var something = (function(){
    var nextVal;

    return {
        // needed for `for..of` loops
        [Symbol.iterator]: function(){ return this; },

        // standard iterator interface method
        next: function(){
            if (nextVal === undefined) {
                nextVal = 1;
            }
            else {
                nextVal = (3 * nextVal) + 6;
            }

            return { done:false, value:nextVal };
        }
    };
})();

something.next().value;     // 1
something.next().value;     // 9
something.next().value;     // 33
something.next().value;     // 105

제너레이터 멈춤

제너레이터가 반환한 이터레이터에서 break, return등으로 for...of가 비정상 완료되면 자동으로 제너레이터를 중지하도록 신호 수동으로 제너레이터의 이터레이터를 멈추게 하려면 return () 문을 쓰면 된다.

   it.return( "Hello World" ).value

제너레이터 완료 된 후에도 제너레이터에안에서 try ... finally을 통해 후처리 가능

function *something() {
    try {
        var nextVal;

        while (true) {
            if (nextVal === undefined) {
                nextVal = 1;
            }
            else {
                nextVal = (3 * nextVal) + 6;
            }

            yield nextVal;
        }
    }
    // 정리 코드
    finally {
        console.log( "cleaning up!" );
    }
}
var it = something();
for (var v of it) {
    console.log( v );

    // 무한 루프가 되지 않게 하라
    if (v > 500) {
        console.log(
            // 제너레이터의 이터레이터를 마친다. 
            it.return( "Hello World" ).value
        );
        //  이터레이터는 done:true 상태로 `break` 문은 필요없다.
    }
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World

제너레이터를 비동기적으로 순회

제너레이터는 비동기 패턴과 무슨 상관이 있으며, 어떻게 콜백의 문제를 해결할까?

콜백

function foo(x,y,cb) {
    ajax(
        "http://some.url.1/?x=" + x + "&y=" + y,
        cb
    );
}

foo( 11, 31, function(err,text) {
    if (err) {
        console.error( err );
    }
    else {
        console.log( text );
    }
} );

이를 제너레이터로 표현하면

function foo(x,y) {
    ajax(
        "http://some.url.1/?x=" + x + "&y=" + y,
        function(err,data){
            if (err) {
                // throw an error into `*main()`
                it.throw( err );
            }
            else {
                // resume `*main()` with received `data`
                it.next( data );
            }
        }
    );
}

function *main() {
    try {
        var text = yield foo( 11, 31 );
        console.log( text );
    }
    catch (err) {
        console.error( err );
    }
}

var it = main();

// start it all up!
it.next();
if(err) {
// 에러를 제너레이터에 던져 yield를 멈춘다
 it.throw(err);
}

▲ 이러한 제너레이터의 yield 멈춤기능은 비동기 함수의 동기적 에러 처리가 가능하게 한다.

이처럼 비동기 코드에서 난 에러를 동기적인 모양새로 처리할 수 있다는 것은 코드 가독성, 추론성면에서 매우 큰 강점

제너레이터 + 프라미스

function foo(x,y) {
    return request(
        "http://some.url.1/?x=" + x + "&y=" + y
    );
}

function *main() {
    try {
        var text = yield foo( 11, 31 );
        console.log( text );
    }
    catch (err) {
        console.error( err );
    }
}
var it = main();

var p = it.next().value;

// wait for the `p` promise to resolve
p.then(
    function(text){
        it.next( text );
    },
    function(err){
        it.throw( err );
    }
);

프로미스 동시성

서로 다른 두 데이터 소스에 데이터를 각각 요청한 후 수신한 응답을 조합해서 다시 세번째 요청

function *foo() {
    var r1 = yield request( "http://some.url.1" );
    var r2 = yield request( "http://some.url.2" );

    var r3 = yield request(
        "http://some.url.3/?v=" + r1 + "," + r2
    );

    console.log( r3 );
}

// use previously defined `run(..)` utility
run( foo );

▲ 순차 진행, yield는 기껏해야 코드 한곳에서 멈추게 할 수 있지만 동시에 멈추는 것은 불가능

function *foo() {
    // make both requests "in parallel"
    var p1 = request( "http://some.url.1" );
    var p2 = request( "http://some.url.2" );

    // wait until both promises resolve
    var r1 = yield p1;
    var r2 = yield p2;

    var r3 = yield request(
        "http://some.url.3/?v=" + r1 + "," + r2
    );

    console.log( r3 );
}

// use previously defined `run(..)` utility
run( foo );

Promise.all로도 나타낼 수 있다.

function *foo() {
    // make both requests "in parallel," and
    // wait until both promises resolve
    var results = yield Promise.all( [
        request( "http://some.url.1" ),
        request( "http://some.url.2" )
    ] );

    var r1 = results[0];
    var r2 = results[1];

    var r3 = yield request(
        "http://some.url.3/?v=" + r1 + "," + r2
    );

    console.log( r3 );
}

// use previously defined `run(..)` utility
run( foo );

제너레이터 위임

function *foo() {
    console.log( "`*foo()` starting" );
    yield 3;
    yield 4;
    console.log( "`*foo()` finished" );
}

function *bar() {
    yield 1;
    yield 2;
    yield *foo();   // `yield`-delegation!
    yield 5;
}

var it = bar();

it.next().value;    // 1
it.next().value;    // 2
it.next().value;    // `*foo()` starting
                    // 3
it.next().value;    // 4
it.next().value;    // `*foo()` finished
                    // 5

메세지 위임

function *foo() {
    console.log( "inside `*foo()`:", yield "B" );

    console.log( "inside `*foo()`:", yield "C" );

    return "D";
}

function *bar() {
    console.log( "inside `*bar()`:", yield "A" );

    // `yield`-delegation!
    console.log( "inside `*bar()`:", yield *foo() );

    console.log( "inside `*bar()`:", yield "E" );

    return "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B

console.log( "outside:", it.next( 2 ).value );
// inside `*foo()`: 2
// outside: C

console.log( "outside:", it.next( 3 ).value );
// inside `*foo()`: 3
// inside `*bar()`: D
// outside: E

console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: 4
// outside: F

제너래이터가 아닌 일반 이터러블에도 다음과 같이 쓸 수 있다.

function *bar() {
    console.log( "inside `*bar()`:", yield "A" );

    // 제너레이터 아닌 객체에 yield 위임을 한다.
    console.log( "inside `*bar()`:", yield *[ "B", "C", "D" ] );

    console.log( "inside `*bar()`:", yield "E" );

    return "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// inside `*bar()`: 1
// outside: B

console.log( "outside:", it.next( 2 ).value );
// outside: C

console.log( "outside:", it.next( 3 ).value );
// outside: D

console.log( "outside:", it.next( 4 ).value );
// inside `*bar()`: undefined
// outside: E

console.log( "outside:", it.next( 5 ).value );
// inside `*bar()`: 5
// outside: F

에러/예외

function *foo() {
    try {
        yield "B";
    }
    catch (err) {
        console.log( "error caught inside `*foo()`:", err );
    }

    yield "C";

    throw "D";
}

function *bar() {
    yield "A";

    try {
        yield *foo();
    }
    catch (err) {
        console.log( "error caught inside `*bar()`:", err );
    }

    yield "E";

    yield *baz();

    // note: can't get here!
    yield "G";
}

function *baz() {
    throw "F";
}

var it = bar();

console.log( "outside:", it.next().value );
// outside: A

console.log( "outside:", it.next( 1 ).value );
// outside: B

console.log( "outside:", it.throw( 2 ).value );
// error caught inside `*foo()`: 2
// outside: C

console.log( "outside:", it.next( 3 ).value );
// error caught inside `*bar()`: D
// outside: E

try {
    console.log( "outside:", it.next( 4 ).value );
}
catch (err) {
    console.log( "error caught outside:", err );
}
// error caught outside: F

위임 재귀

function *foo(val) {
    if (val > 1) {
        // generator recursion
        val = yield *foo( val - 1 );
    }

    return yield request( "http://some.url/?v=" + val );
}

function *bar() {
    var r1 = yield *foo( 3 );
    console.log( r1 );
}

run( bar );
  1. run(bar) 는 *bar() generator를 실행한다.
  2. foo(3) *foo(..) 의 이터레이터 만들고 3 파라미터로 넘긴다.
  3. 3>1 이기 때문에 foo(2)는 또다른 iterator를 만들고 2를 파라미터로 넘긴다.
  4. 2>1 이기 때문에 foo(1)는 또다른 iterator를 만들고 1를 파라미터로 넘긴다.
  5. 1 > 1 은 거짓으로 request(..) 를 value1으로 호출 그리고 promise를 return 받는다.
  6. promise가 yield되면서, *foo(2) generator instance로 돌아간다.
  7. yield 는 이 프라미스를 다시 foo(3) 제너레이터 인스턴스로 보낸다. 또 다른 yield 는 이 프라미스를 bar() 제너레이터 인스턴스로 보내고 다시 또 다른 yield *는 run()유틸리티로 이 프라미스를 보내기를 반복하여 최초의 ajax 요청이 귀결되길 바란다.
  8. 프라미스가 귀결되면 이룸 메시지가 전송되어 bar()를 재게 yield 를 통해 foo(3) 인스턴스로 전달, foo(2) ... 일반 yield문에 이른다.
  9. 첫번째 호출 Ajax 응답값은 이제 바로 foo(3) 제너레이터 인스턴스로부터 넘겨받고 이 값을 다시 foo(2)인스턴스의 yied * 포현식 결과로 전송. 지역 변수 val에 할당
  10. ... 이런식으로 세번째 Ajax요청 실행되면 결국 bar()에서 대기 중인 yield 표현식에 도달하게 된다.

제너레이터 동시성

동시에 2개의 ajax 응답 처리기에서 ajax을 res배열에 순서 조정하여 넣기

var res = [];

function *reqData(url) {
    res.push(
        yield request( url )
    );
}
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );

var p1 = it1.next();
var p2 = it2.next();

p1
.then( function(data){
    it1.next( data );
    return p2;
} )
.then( function(data){
    it2.next( data );
} );

개선1

var res = [];

function *reqData(url) {
    var data = yield request( url );

    // 제어권 넘김
    yield;

    res.push( data );
}

var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );

var p1 = it.next();
var p2 = it.next();

p1.then( function(data){
    it1.next( data );
} );

p2.then( function(data){
    it2.next( data );
} );

Promise.all( [p1,p2] )
.then( function(){
    it1.next();
    it2.next();
} );

▲ 첫 번째 인스턴스가 완전희 끝난뒤 두 번째 인스턴스 실행 그러나 여기서는 두 인스턴스가 모두 각자의 응답이 돌아오자마자 데이터를 수신하고 인스턴스 제어권을 넘긴다.

개선2 runAll () 1.첫 번째 제너레이터는 첫 번째 AJAX 응답 프라미스를 얻고 다시 runAll() 유틸리티로 제어권을 넘겨준다. 2.두 번째 제너레이터는 AJAX 실행 후 runAll()에 제어권을 도로 갖다준다. 3.첫 번째 제너레이터 재개 후 프라미스 yield. 프라미스 귀결을 기다렸다가 같은 제너레이터 재개 p1이 귀결되면 runAll은 첫번째 제너레이터를 귀결 값으로 재개 후 res[0]에 해당값 할당

  1. 첫 번째 제너레이터가 끝나면 암시적으로 제어권 넘기이 이루어진다.

효과 두 제너레이터가 제어권 조정을 통해 res, yield된 메시지를 통해 url1, url2 두 값을 교류하면서 실질적인 통신을 한다.

// `request(..)` is a Promise-aware Ajax utility

var res = [];

runAll(
    function*(){
        var p1 = request( "http://some.url.1" );

        // transfer control
        yield;

        res.push( yield p1 );
    },
    function*(){
        var p2 = request( "http://some.url.2" );

        // transfer control
        yield;

        res.push( yield p2 );
    }
);

redux saga

redux-saga의 경우엔, 액션을 모니터링하고 있다가, 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식 => Generator 문법을 사용

function* increaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(increase()); // put은 특정 액션을 디스패치 해줍니다.
}
function* decreaseSaga() {
  yield delay(1000); // 1초를 기다립니다.
  yield put(decrease()); // put은 특정 액션을 디스패치 해줍니다.
}

export function* counterSaga() {
  yield takeEvery(INCREASE_ASYNC, increaseSaga); // 모든 INCREASE_ASYNC 액션을 처리
  yield takeLatest(DECREASE_ASYNC, decreaseSaga); // 가장 마지막으로 디스패치된 DECREASE_ASYNC 액션만을 처리
}