從Co剖析和解釋generator的非同步原理

Vincent Ko發表於2018-07-30

generator的非同步原理

同樣的,這個內容感謝@MPJ老師在youtube上的funfunfuntion課程。 當年我看阮老師的generator雲裡霧裡,實在搞不懂,為什麼yield出來就可以非同步了?? @MPJ用了相反的過程,先給一個應用場景,再去實現非同步,我算是真的搞清楚,generator非同步的遠離了,和大家分享~~

部落格和web專案更新傳送門

從實際應用場景開始

假設我們有一個非同步的請求,想要去通過api獲取一些資料。這裡藉助node-fetch庫來獲取資料。 fetch可以非同步的獲取資料,並返回一個promise,所以常規的非同步操作和寫法,大致如下

var fetch = require('node-fetch');
fetch('http://jasonplacerholder.typecoder.com/posts/1')
    .then( res => res.json() )
    .then( post => post.title )
    .then( x => console.log('Title: ',x))
複製程式碼

好了,以上的程式碼就是一個獲取api,並拿到api中的title內容。 關於promise這裡不多說,fetch返回的就是一個promise。

genetor實現

那麼如果使用generator會如何實現實現同樣的一個非同步操作呢? 這裡先給結果,再來分析實現原理。這裡記住co,這個co是幹嘛的,一會分析並實現一個我們自己的co函式。

co接收一個genetor,所以我們可以認為co就是一個generator的發動機,或者自動執行器。

const co = require('co');
co(function *() {
    const url = 'http://jasonplacerholder.typecoder.com/posts/1';
    const response = yield fetch(url);
    const post = yield response.json();
    const title = post.title;
    console.log('Title: ',title);
})

複製程式碼

好了,結束,執行後,會輸出同樣的結果,似乎和promise沒有兩樣。下面先簡單的逐行分析,來看看在genetor中,做了什麼。

//從genetor的第一行開始
第一行: 定義了url
第二行: 宣告response,並將fetch(url)的結果.....yield
stop...
What is yield???
複製程式碼

嗯,所以,這個genetor的yield是幹什麼的?這是genetor和普通函式的不同之處,也是它可以做非同步的基礎。不同與普通函式,genetor遇到了yield之後,會將yield後面的處理內容丟擲。

genetor: 執行呀---執行呀---執行呀--yield? What?這是什麼鬼,我搞不定,老大你幫我搞定後再加我---out..

outer(執行器co): 收到yield返回的結果,處理----返回給genetor

genetor: 收到處理結果---執行---yeild?這又是什麼?你幫我搞定,out...

outer(執行器co): 收到yield返回的promise,處理---返回給genetor

這就是非同步的原理了,genetor遇到yield會把任務丟出去,它就暫時不執行了。 我們知道,yield丟出去的是一個iterator,當呼叫next()的時候,會返回genetor中。 所以其實co就是一個自動觸發和排程next()的函式。

實現co

知道了原理,我們自己來實現這個過程。然後就會比較清除整個過程了。

我們把函式改一下

run(function *() {
    const url = 'http://jasonplacerholder.typecoder.com/posts/1';
    const response = yield fetch(url);
    const post = yield response.json();
    const title = post.title;
    console.log('Title: ',title);
})

function run(generator) {
    const iterator = generator(); //genetor執行會返回一個iterator,然後呼叫next()才會執行到下一個yield
    iterator.next(); //這裡列印出來的結果看一下是{value: Promise {<pending>},done:false}

}
複製程式碼

解釋: 就如上面genetor和outer的對話,遇到yield,genetor會說:"我不知道怎麼搞這個promise,你來搞吧,給你..“ 於是,外面的就會接住這個promise

我們繼續寫

function run(generator) {
    const iterator = generator(); //genetor執行會返回一個iterator,然後呼叫next()才會執行到下一個yield
    const iteration = iterator.next(); //這裡列印出來的結果看一下是{value: Promise {<pending>},done:false}
    const promise = iteration.value;
    promise.then(x => iterator.next(x)) //ok,外部幫忙處理了promise,然後處理的結果,我們需要返回genetor,使其繼續執行
    //這個時候,genetor中的response拿到了值,就等於這裡的x
}
複製程式碼

分析到這裡,程式已經得到了response。 但是,下一句,立馬又遇到了response.json(),同樣又會丟出去一個內容,因此,我們這裡再處理一下,如下:

function run(generator) {
    const iterator = generator(); //genetor執行會返回一個iterator,然後呼叫next()才會執行到下一個yield
    const iteration = iterator.next(); //這裡列印出來的結果看一下是{value: Promise {<pending>},done:false}
    const promise = iteration.value;
    promise.then(x => {
        const anotherIterator = iterator.next(x);//注意,iterator.next()的含義,一方面會將運算結果返回,另一方面,genetor會繼續將下一個yield的任務丟擲,仍然是一個iterator
        const anotherPromise = anotherIterator.value;
        anotherPromise.then(post => iterator.next(post))
        //到此,因為iterator再也沒有yield,所以不會再次返回iterator了,也不用呼叫next()
    }) 
}
複製程式碼

至此,模擬的co方法已經實現了。

流程如下:

  1. run傳入一個genetor並執行,獲得一個iterator(generator())
  2. 呼叫next()方法,獲取到iteration,iteration的value是yield fetch(url)的結果,也即一個Promise。
  3. yield返回出的任務,由外部執行和處理,結束後在返回,於是使用then方法。
  4. 處理後的結果為x,呼叫iterator.next(x)把x返回的同時,拿到了下一個yield的丟擲的任務。
  5. 處理任務,得到post,並通過next(post)返回給genetor。
  6. 嗯,我拿到你們處理的結果了,下一次我遇到yield還給你們,反正我不會,我也不會學,這任務都是你們的。

也就是說,genetor的非同步,就在於能將執行緒彈出,遇到yield後,交出執行緒。所以,我們做一個能夠自動執行和觸發genetor的執行器,就可以實現非同步程式設計,而且看起來和同步的寫法很相似。 這就是庫co做的事情。

完善我們自己的co

剛才只有兩個yield,我們希望方法有通用性,我們寫個遞迴,讓它能不斷的觸發

function run(genetor) {
    const iterator = genetor();
    function autoRun(iteration) {
        if(iteration.done) {return iteration.value;}
        const anotherPromise = iteration.value;
        anotherPromise.then(x => {
            return autoRun(iterator.next(x));
        })
    }
    return autoRun(iterator.next());
}

複製程式碼

好了,這樣就完成了我們自己的簡易版co函式。

相關文章