博客 / 詳情

返回

一步一步理解Generator函數的原理

作者:麥樂

來源:恆生LIGHT雲社區

Generator函數基本用法

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
 
var hw = helloWorldGenerator();

Generator函數調用後生成的就是一個迭代器對象,可以通過調用迭代器的next方法,控制函數內部代碼的執行。

hw.next()
// { value: 'hello', done: false }
 
hw.next()
// { value: 'world', done: false }
 
hw.next()
// { value: 'ending', done: true }
 
hw.next()
// { value: undefined, done: true }

Generator函數遇到yield,可以在生成器函數內部暫停代碼的執行使其掛起。在可迭代對象上調用next()方法可以使代碼從暫停的位置開始繼續往下執行。

先來了解一下什麼是可迭代對象?

可迭代對象

要成為可迭代 對象, 一個對象必須實現 <strong>@@iterator</strong> 方法。這意味着對象(或者它原型鏈上的某個對象)必須有一個鍵為 @@iterator 的屬性,可通過常量 Symbol.iterator 訪問該屬性:

[Symbol.iterator] 一個無參數的函數,其返回值為一個符合迭代器協議的對象。

let someString = "hi";
typeof someString[Symbol.iterator];   
let iterator = someString[Symbol.iterator]();
iterator + "";                               // "[object String Iterator]"
 
iterator.next();         // { value: "h", done: false }
iterator.next();         // { value: "i", done: false }
iterator.next();         // { value: undefined, done: true }

再來看一下,掛起是怎麼回?有一個新的名詞“協程”。

什麼是協程?

協程(Coroutines)是一種比線程更加輕量級的存在,正如一個進程可以擁有多個線程一樣,一個線程可以擁有多個協程。

協程不是被操作系統內核所管理的,而是完全由程序所控制,也就是在用户態執行。這樣帶來的好處是性能大幅度的提升,因為不會像線程切換那樣消耗資源。

協程不是進程也不是線程,而是一個特殊的函數,這個函數可以在某個地方掛起,並且可以重新在掛起處外繼續運行。所以説,協程與進程、線程相比並不是一個維度的概念。

Generator 函數原理

Generator 函數是協程在 ES6 的實現,最大特點就是可以交出函數的執行權(即暫停執行)。

總結下來就是:

  • 一個線程存在多個協程
  • Generator函數是協程在ES6的實現
  • Yield掛起協程(交給其它協程),next喚起協程

講到這裏,應該會對Generator函數有一個重新的認識吧。在實際的開發中,直接使用Generator函數的場景並不常見,因為它只能通過手動調用next方法實現函數內部代碼的順序執行。如果想象很好是使用它,可以為Generator函數實現一個自動執行神器。

自動執行的Generator函數

可以根據g.next()的返回值{value: '', done: false}中done的值讓Generator函數遞歸自執行:

 function run(generator) {
            var g = generator();

            var next = function() {
                var obj = g.next()
                console.log(obj)
                if(!obj.done) {
                    next()
                }
            }
            next()
        }
        run(helloWorldGenerator)

這樣寫能實現自執行功能,但是 不能保證執行順序。模擬兩個異步請求:

    function sleep1() {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log('sleep1')
                    resolve(1)
                }, 1000)
            })
        }
        function sleep2() {
            return new Promise((resolve) => {
                setTimeout(() => {
                    console.log('sleep2')
                    resolve(2)
                }, 1000)
            })
        }

修改函數

function* helloWorldGenerator() {
            yield sleep1();
            console.log(1);
            yield sleep2();
            console.log(2);
        }

執行 run(helloWorldGenerator)看一下打印順序:

 title=

異步函數還是在同步代碼執行完以後執行的,如果想要實現異步代碼也能按照順序執行,可以對代碼進一步優化:

        function run(generator) {
            var g = generator();

            var next = function() {
                var obj = g.next();
                console.log(obj)
                if(obj.done) return;
                // 如果yield後面返回值不是promise,可以使用Promise.resolve包裹一下,防止報錯
                Promise.resolve(obj.value).then(() => {next()})
            }
            next()
        }

 title=

如果説sleep1是一個網絡請求的話,在yield後面就可以拿到請求返回的數據,看起來像是一種更優雅的異步問題解決方案。

如果想要拿到sleep函數resolve的值,也是可以實現的。

// 修改函數 變量接收yield語句返回結果
function* helloWorldGenerator() {
            var a = yield sleep1();
            console.log(a);
            var b = yield sleep2();
            console.log(b);
        }
// g.next(v); 傳遞結果值
      function run(generator) {
            var g = generator();

            var next = function(v) {
                var obj = g.next(v);
                console.log(obj)
                if(obj.done) return;
                // 如果yield後面返回值不是promise,可以使用Promise.resolve包裹一下,防止報錯
                Promise.resolve(obj.value).then((v) => {next(v)})
            }
            next()
        }

你會看到和上面一樣的打印結果。

仔細看上面實現方式跟async await很相似,實際上這就是async await的原理。

async await

async await本質上就是結合promise實現的一個自執行Generator函數。將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已

更詳細的代碼如下,感興趣的同學可以深入瞭解一下:

 // async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已
            // async函數返回一個 Promise 對象,可以使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操作完成,再接着執行函數體內後面的語句
            // Generator 函數的執行必須靠執行器,所以才有了co模塊,而async函數自帶執行器和普通函數一樣執行
            // async函數對 Generator 函數的改進:
            /*
            (1)內置執行器
            (2)更好的語義
            (3)更廣的適用性
            (4)返回值是 Promise
            */
 
  
            // async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裏。
            async function fn(args) {
                // ...
            }
            // 等同於 裏面的await 換成 yield
            function fn(args) {
                return spawn(function*() {
                    // ...
                });
            }
  
            function spawn(genF) {
                return new Promise(function(resolve, reject) {
                    const gen = genF();
                    function step(nextF) {
                        let next;
                        try {
                            next = nextF();
                        } catch (e) {
                            return reject(e);
                        }
                        if (next.done) {
                            return resolve(next.value); // 這也是為什麼 不使用try catch的話,異常異步請求後面的代碼不再執行的原因,從這裏不再繼續調用nex()方法了。
                        }
                        Promise.resolve(next.value).then(function(v) {
                            step(function() {
                                return gen.next(v);
                            });
                        }, function(e) {
                            step(function() {
                                return gen.throw(e); // 這裏可以解釋為什麼async 函數需要使用try catch來捕獲異常,生成器函數的throw,會讓代碼到catch裏面
                            });
                        });
                    }
                    step(function() {
                        return gen.next(undefined);
                    });
                });
            }

想向技術大佬們多多取經?開發中遇到的問題何處探討?如何獲取金融科技海量資源?

恆生LIGHT雲社區,由恆生電子搭建的金融科技專業社區平台,分享實用技術乾貨、資源數據、金融科技行業趨勢,擁抱所有金融開發者。

掃描下方小程序二維碼,加入我們!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.