Generator與Promise的完美結合 -- async await函式誕生記

zifeiyu發表於2018-12-06

Generattor函式的yield next雙向通訊

function *foo(x) {
    var y = x * (yield "Hello");     // <-- yield一個值!
    return y;
}

var it = foo( 6 );

var res = it.next();    // 第一個next(),並不傳入任何東西
res.value;              // "Hello"

res = it.next( 7 );     // 向等待的yield傳入7
res.value;              // 42
複製程式碼

為了較為清晰的理解這個過程,特意做了一個簡單的圖示,加深自己的記憶。

Generator與Promise的完美結合 -- async await函式誕生記
這個過程就好像是絲綢之路上的商人之間的貿易一樣,next從yield哪裡拿到一些東西同時也會給yield一些東西。

多個迭代器

每次構建一個迭代器 ,實際上就隱式構建了生成器的一個例項,通過這個迭代器 來控制的是這個生成器例項

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 val1 = it1.next().value;           // 2 <-- yield 2
var val2 = 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
                                       // 20 300 3
it2.next( val1 / 4 );                  // y:10
                                       // 200 10 3
複製程式碼

迭代器深入理解

如果一個物件包含一個可以迭代的迭代器(iterator),那麼這個物件就是一個iterable(可迭代)

從一個iterable中回去迭代器的方法:

// 以陣列為例
var a = [1,2,3]
var it = a[Symbol.iterator]();
it.next().value // 1
it.next().value // 2
it.next().value // 3
複製程式碼

每一個iterable中都有一個函式Symbol.iterator。呼叫這個函式會返回一個迭代器。

for of 迴圈可以自動呼叫Symbol.iterator函式來構建一個迭代器。

讓我們手動實現一個iterator(同時也是一個iterable)

var something = (function() {
    let val
    return {
        // something物件是含有一個Symbol.iterator函式的,所以something是一個iterable;
        // 這個Symbol.iterator函式返回的就是something本身,而它本身是有next()方法,所以實際上something本身也是一個迭代器(iterator)
        [Symbol.iterator]: function() {return this},
        next: function() {
            val = val ? val + 2 : 1 
            return {
                // 這裡一直返回false,所以這個迭代器沒有終點
                done: false, value: val
            }
        }
    }
})
複製程式碼

真正的iterator可以具有三個方法next, return, throw;我們自己實現的iterator可以按照需要省略return和throw

  • return方法: 如果for...of迴圈提前退出(通常是因為出錯,或者有break語句)
var something = (function() {
    let val
    return {
        // something物件是含有一個Symbol.iterator函式的,所以something是一個iterable;
        // 這個Symbol.iterator函式返回的就是something本身,而它本身是有next()方法,所以實際上something本身也是一個迭代器(iterator)
        [Symbol.iterator]: function() {return this},
        next: function() {
            val = val ? val + 2 : 1 
            return {
                // 這裡一直返回false,所以這個迭代器沒有終點
                done: false, value: val
            }
        },
        return() {
            console.log('return 被觸發')
            return {done: true}
        }
    }
})

for(let i of something()) {
    if(i > 10) {break}
    console.log(i)
}
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// return 被觸發
複製程式碼
  • throw方法主要是配合生成器函式一起使用,一般的iterator用不到這個方法。 next()、throw()、return()在生成器函式中這三個方法本質上是一樣的, 都是讓Generator函式恢復執行,並且使用不同的語句替換yield表示式。

    next()是將一個值傳遞給yield

    const g = function* (x, y) {
      let result = yield x + y;
      return result;
    };
    
    const gen = g(1, 2);
    gen.next(); // Object {value: 3, done: false}
    
    gen.next(1); // Object {value: 1, done: true}
    // 相當於將 let result = yield x + y
    // 替換成 let result = 1;
    複製程式碼

    throw()是將一個錯誤傳遞給yield

    gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
    // 相當於將 let result = yield x + y
    // 替換成 let result = throw(new Error('出錯了'));
    複製程式碼

    return()是將return語句傳遞給yield

    gen.return(2); // Object {value: 2, done: true}
    // 相當於將 let result = yield x + y
    // 替換成 let result = return 2;
    複製程式碼

生成器(Generator) vs 迭代器

生成器並不是一個iterater,它執行的結果才是一個iterator

function * foo() {}
// it是一個itreator
var it = foo()
複製程式碼

所以我們嘗試用生成器實現上面的something:

function *something() {
    var val
    // 在生成器中使用while..true並沒有問題
    while(true) {
        val = val ? val + 2 : 1
        yield val
    }
}
複製程式碼

這個實現方式跟我們之前的閉包方式相比更加簡潔,不需要閉包來保持變數狀態了。

for (let i of something()) {
    if(i > 10){break}
    console.log(i)
}
複製程式碼

前面我們說過something()生成的是一個iterator,而for...of需要的是一個iterable; 實際上生成器something()生成的iterator同時也是一個iterable,其內部的Symbol.iterator實際上也是類似於return this 的做法。

生成器與Promise的完美結合

生成器函式給了我們一個看似同步的流程控制程式碼配合Promise(可信任可組合)的組合可能是js新世界中最美妙的事情。

所以,你應該想到了,ES78(ES2017)中提供的async/await正式這兩者完美結合的語法級支援。 實際上,asyn函式不過是Generator(生成器)函式的一個語法糖

// 手動結合Generator和Promise

// 這裡不直接定義promise而是通過foo返回是因為promise在定義的時候就會執行
function foo() {
    return new Promise(function(resolve, reject) {
        resolve(10)
    })
}

function *gen() {
    try{
        let text = yield foo()
        console.log(text)
    } catch(err) {
        console.log(err)
    }
}

let it = gen()
let p =it.next().value

p.then(
    function(res) {
        it.next(res)
    },
    function(err) {
        it.throw(err)
    }
)
// 10
複製程式碼

可以看到手動結合Promise和Generator是多麼的繁瑣;雖然帶來了同步的流程和可信任可組合的Promsie,這也不是我們願意看到的。

看看async/await如何實現上述過程

function foo() {
    return new Promise(function(resolve, reject) {
        resolve(10)
    })
}

async function aw() {
    let text = await foo()
    console.log(text)
}
aw() // 10
複製程式碼

是不是清爽了很多,async/await實際上是把Generator的啟動步驟和Promsie內部的next()實現細節隱藏到語法糖中,留給我們一個清爽的世界。如果希望瞭解實現細節,我們不放手動模仿以下這個語法糖函式

示例來自‘你不知道的javascript(中卷)’

function run(gen) {
    var args = [].slice.call( arguments, 1), it;

    // 在當前上下文中初始化生成器
    it = gen.apply( this, args );

    // 返回一個promise用於生成器完成
    return Promise.resolve()
        .then( function handleNext(value){
            // 對下一個yield出的值執行
            var next = it.next( value );

            return (function handleResult(next){
                // 生成器執行完畢了嗎?
                if (next.done) {
                    return next.value;
                }
                // 否則繼續執行
                else {
                    return Promise.resolve( next.value )
                        .then(
                            // 成功就恢復非同步迴圈,把決議的值發回生成器
                            handleNext,

                            // 如果value是被拒絕的 promise,
                            // 就把錯誤傳回生成器進行出錯處理
                            function handleErr(err) {
                                return Promise.resolve(
                                    it.throw( err )
                                )
                                .then( handleResult );
                            }
                        );
                }
            })(next);
        } );
}


複製程式碼

相關文章