1. 程式人生 > >用ES6 Generator替代回撥函式

用ES6 Generator替代回撥函式

http://www.html-js.com/article/A-day-to-learn-JavaScript-to-replace-the-callback-function-with-ES6-Generator

目前,已經有很多文章討論過了如何使用ES6 generators來取代JavaScript中經常遇到的“回撥金字塔”。但是,其中提到的絕大多數方法都需要依賴於某個庫,而對於其中的原理卻提及甚少。

在本文中,我們將一步一步的將一個基於回撥函式的例子修改為一個基於generator的例子。本文的目標是讓你透徹地理解使用generator替代回撥函式的原理。

Generator是JavaScript中一個新概念,但在程式語言中已經存在已久。你可能已經在其他的程式語言例如Python使用過它。如果沒有,也不要害怕,我們在後面已經為你準備了一個簡單明瞭的入門介紹。

如何執行例子

在我們開始之前,你需要安裝Node 0.11.* 來執行文章中的例子。當你在執行這些例子時,你需要告訴Node使用ES6(也就是Harmony)來執行:node -harmony example.js

什麼是一個generator

在我們深入講述如何使用generator替代回撥函式之前,我們先來說說什麼是generator。

Generator很像是一個函式,但是你可以暫停它的執行。你可以向它請求一個值,於是它為你提供了一個值,但是餘下的函式不會自動向下執行直到你再次向它請求一個值。

取號機也許是對generator的一個絕佳的比喻。你可以通過取一張票來向機器請求一個號碼。你接收了你的號碼,但是機器不會自動為你提供下一個。換句話說,取票機“暫停”直到有人請求另一個號碼,此時它才會向後執行。

ES6中的generator

Generator在ES6中像一個函式一樣被宣告,除了在之前有一個星號的差別外:

function* ticketGenerator(){}

當你想要一個generator提供一個值然後暫停時,你需要使用yield關鍵字。yield有點像是return關鍵字,因為它們都返回一個值,但是函式在yield之後會進入暫停狀態。

function ticketGenerator(){
    yield 1;
    yield 2;
    yield 3;
}   

在我們的例子中,我們定義了一個叫做ticketGenerator的迭代器。如果你向它請求一個值,它會返回1然後暫停。如果你再次向它發出一個請求,我們將得到2,然後是3。

當你呼叫一個generator時,它將返回一個迭代器物件。這個迭代器物件擁有一個叫做next的方法來幫助你重啟generator函式並得到下一個值。

next方法不僅返回值,它返回的物件具有兩個屬性:done和value。value是你獲得的值,done用來表明你的generator是否已經停止提供值。

現在我們從我們的取號機中取一些號碼:

var takeANumber = ticketGenerator();   

takeANumber.next();   

//>{value: 1, done: false}   

takeANumber.next();   
//>{value: 2, done: false}   

takeANumber.next();  
//>{value: 3, done: false}   

takeANumber.next();   
//>{value: undefined, done: true}  

現在我們的取號系統只能提供最多到3的號碼,這實在是沒什麼用。我們想要讓它無線增加下去,因此我們來建立一個迴圈。

function* ticketGenerator(){
    for(var i=0; true; i++){
        yield i;
    }
}   

現在,如果這是一個普通的函式,我們每次只會得到0。但是使用generator卻不一樣:

var takeANumber = ticketGenerator();   
console.log(takeANumber.next().value); //0   
console.log(takeANumber.next().value); //1   
console.log(takeANumber.next().value); //2  
console.log(takeANumber.next().value); //3  
console.log(takeANumber.next().value); //4  

每一次當我們呼叫next()時,generator執行下一個迴圈迭代然後暫停。這意味著我們擁有一個可以無限向下執行的generator。因為這個generator只是發生了暫停,你並沒有凍結你的程式。事實上,generator是一個建立無限迴圈的好方法。

影響generator的狀態

進一步探究迭代迭代generator物件的話,next()實際上還有另一個用途。如果你給next傳遞一個值,它會被視為generator中的一個yield語句的結果來對待。

因此next是一個在generator執行過程中向其傳遞資訊的方式。我們將以此來修改我們的取號generator以便它能夠被重置到0.我們希望能在任何時間點來重置取號機。

function* ticketGenerator(){
    for(var i=0; true; i++){
        var reset = yield i;
        if(reset) {i = -1;}
    }
}  

正如你所看到的,如果yield返回了一個true然後我們將i設定為-1。那麼for迴圈將會在迴圈的結尾將i增加1,因此下一次返回的i變成了0。

我們來看看實際情況如何:

var takeANumber = ticketGenerator(); console.log(takeANumber.next().value); //0  console.log(takeANumber.next().value); //1 console.log(takeANumber.next().value); //2 console.log(takeANumber.next(true).value); //0 console.log(takeANumber.next().value); //1     

用generator替代回撥函式

既然我們已經學到了一些關於generator的知識,現在讓我們來談談generator和回撥函式。正如你所知道的,當我們呼叫例如AJAX請求這樣的非同步程式碼時我們會使用回撥函式。為了簡單起見我們在例子中定義一個delay函式。

我們的delay函式將會是非同步的 – 在指定的時間過後我們提供給delay的回撥函式才會被執行,然後delay會給你的回撥函式傳遞一個字串告訴它究竟“沉睡”了多久。

在此期間你的其餘程式碼將會繼續執行下去。這就好像是進行一個AJAX請求一樣 – 你發出請求,你的程式碼繼續執行,當伺服器返回一個結果時你的回撥函式才執行。

現在,我們來定義delay函式:

function delay(time, callback){
    setTimeout(function(){
        callback("Slept for "+time);
    },time);
}   

到目前為止,還沒有什麼特別的東西。現在我們來使用它來delay兩次。首先我們將delay1000ms,然後當delay結束後我們再另外delay 1200ms。

delay(1000,function(msg){
    console.log(msg);
    delay(1200,function(msg){
        console.log(msg);
    });
})   

//...waits 1000ms   
// > "Slept for 1000"    
//...waits another 1200ms    
// > "Slept for 1200   

確保我們的兩個delay依次被呼叫的唯一方法就是確保第二個delay在第一個delay的回撥函式中。

如果我們要依次delay 12次,我們將需要巢狀的呼叫12次delay函式。這時你就會碰到“回撥金字塔”,程式碼也變得醜陋不堪。

引入generator

Generator是解決“回撥地獄”的有效方法之一。非同步呼叫是很困難的事情,因為我們的函式不會等待非同步呼叫完成,因此我們需要回調函式。

使用generator,我們可以讓我們的程式碼進行等待。無需巢狀回撥函式,我們可以使用generator確保當非同步呼叫在我們的generator函式執行一下行程式碼之前完成時暫停函式的執行。

因此,如果我們可以在一個非同步呼叫完成時暫停執行,這就意味著我們可以依次呼叫delay函式 – 就像delay函式是同步執行的一樣。

我們應該怎麼做

首先,我們知道我們進行非同步呼叫的程式碼需要在一個generator而不是一個一般的函式中進行,因此我們來定義一個generator。

function* myDelayedMessages() {
/* delay 1000ms然後列印結果 */    

/* delay 1000ms然後列印結果 */    
}   

接下來我們需要在我們的generator中呼叫delay。記住,delay接收一個回撥函式。這個回撥函式需要繼續我們的generator,但是我們現在還沒有一個generator因此我們先放上一個空函式。

function* myDelayedMessages(){
    console.log(delay(1000,function(){}));
    console.log(delay(1200,function(){}));
}   

我們程式碼依然是非同步的。這是因為我們還沒有將放入任何的yield語句。Generator只是在它們看大一個yield語句時才暫停。

function* myDelayedMessages() { 
    console.log(yield delay(1000, function(){})); 
    console.log(yield delay(1200, function(){}));
}   

我們現在已經更接近了一點了。然而,如果我們執行我們的generator什麼也不會發生。因為沒有什麼東西告訴它要向下執行。

在這裡你需要理解的最重要的概念是:generator需要在delay中的回撥函式執行完成後繼續往下執行,這就是它們如何知道暫停應該結束了的原因。

這意味著回撥函式中的東西需要知道如何向前推動generator。我們在其中傳遞一個叫做resume的函式來為我們做這件事。記住我們現在還依然沒有定義resume。

function* myDelayedMessages(resume) { 
    console.log(yield delay(1000, resume));
    console.log(yield delay(1200, resume));
}  

OK,現在我們的generator將會接收一個resume函式,這個函式將會向前推動generator。

現在到了關鍵步驟了,我們如何來編寫resume,它又是怎麼來了解我們的generator的。

如果你看看其他使用generator代替回撥函式的例子,你會看到generator函式總是被另一個函式包裹著 – 通常是一個叫做“run”或者“execute”的函式。這些函式的目的有以下幾個:

  • 接收一個generator作為引數
  • 使用這個generator來建立一個新的generator迭代器物件,我們將呼叫它的next方法
  • 建立一個resume函式來使用這個generator迭代器物件來推進generator
  • 將resume函式傳遞給這個generator以便generator能夠訪問resume
  • 在最開始時呼叫next()函式,以便我們的程式碼在碰到第一個yield之前開始執行

    現在我們來建立run函式:

    function run(generatorFunction) { var generatorItr = generatorFunction(resume); function resume(callbackValue) { generatorItr.next(callbackValue); } generatorItr.next() 
    }

現在我們有了一個能夠接收一個generator函式的函式,併為它傳遞了一個瞭解如何推進generator迭代器物件的函式。

注意到我們在resume函式中用到了next的第二個特性。resume是被傳遞給delay的回撥函式,因此它接收delay函式提供的值。resume將這個值傳遞給next,因此yield語句的結果實際上是我們非同步函式的結果!

我們現在要做的只是用run包裹上我們的generator函式,然後我們就能看到以下結果:

run(function* myDelayedMessages(resume) {
 console.log(yield delay(1000, resume)); 
 console.log(yield delay(1200, resume));
})
//...waits 1000ms
// > "Slept for 1000" //...waits 1200ms
// > "Slept for 1200"   

現在,你能看到我們呼叫delay兩次,並沒有使用巢狀回撥函式。如果你依然看到疑惑,我們現在概括的來講述以下究竟發生了什麼:

  • run接收了我們的generator並建立了一個resume函式
  • run建立了一個generator迭代器物件(我們在它上面呼叫next方法),提供了resume函式。接著它推動了generator迭代器向前執行。
  • 我們的generator碰到了第一個yield語句並且呼叫delay。接著這個generator暫停。
  • delay在1000ms之後完成然後呼叫resume。
  • resume告訴我們的generator進行下一步。它將結果傳遞給delay以便console能夠將它打印出來。
  • 我們的generator碰到了第二個yield,呼叫delay然後再次暫停。 
    delay等待1200ms之後呼叫resume回撥函式。
  • resume再次推進generator。
  • 再也沒有yield的呼叫,這個generator完成執行。

結論

我們已經成功的使用generator替代了回撥巢狀方法。總結一下,使用generator替代回撥函式要包含以下幾個步驟:

  • 建立一個run函式來接受一個generator,併為這個generator提供resume函式。
  • 建立一個resume函式來推進generator,然後在resume被非同步函式呼叫時將這個resume函式傳遞給generator。
  • 將resume作為回撥傳遞給我們所有的非同步回撥函式。這些非同步函式在完成時執行resume,這使得我們的generator在每個非同步呼叫完成之時僅僅向前一步。

雖然generator究竟是不是一個處理“回撥地獄”的好方法還在討論之中,但是它確實是一個加強你對ES6中generator和迭代器理解的練習。如果你在尋找一些不需要用到ES6的處理巢狀回撥函式的方法,可以考慮promises。


相關推薦

ES6 Generator替代函式

http://www.html-js.com/article/A-day-to-learn-JavaScript-to-replace-the-callback-function-with-ES6-Generator 目前,已經有很多文章討論過了如何使用ES6 gene

手把手教你Node使用Promise替代函式

async 的本質是一個流程控制。其實在非同步程式設計中,還有一個更為經典的模型,叫做 Promise/Deferred 模型(當然還有更多相關解決方法,比如 eventproxy,co 等,到時候遇到在挖坑) 首先,我們思考一個典型的非同步程式設計模型,考慮這樣一個題目:讀取一個檔案,在控制

C語言函式熟練—使用方法(構建程式框架方便好

通俗點不行嗎?啊,不行嗎?老外把國人玩的都不是人了。國人還自己玩自己。非把一個簡單的東西複雜化。叫那麼難理解!!窩裡鬥。。。。。。典型!!!!!!!! 不說那麼複雜的,誰是狗屎,豬屎。就說怎麼用回撥。使用步驟: 1.寫一個函式A,A裡面有一個引數是個指標函式 比如: int shao(in

es6函式

回撥函式需求 var arr = [10,33,44,55,88,20,32] 第⼀層回撥函式 在不不修改本身情況下 每個val 加10 第⼆層回撥函式 在不不修改本身情況下 每個val * 10 第三層回撥函式 過濾掉所以 ⼩小於400 的值 var arr = [10, 33, 44, 55, 88,

Uncaught ReferenceError: jp2 is not defined,jsonp抓取qq音樂總是說函式沒有定義

問題如下參考連結:https://segmentfault.com/q/1010000010051040 用jsonp抓取qq音樂總是說回撥函式沒有定義, 我的要實現時候的步驟 1。第一步 我要實現的目的 問題:如題 我的部分程式碼: import originJSON

【C/C++】函式實現計算器

一、問題概述 用C語言實現一個簡易計算器,可以用來實現加減乘除的功能 名詞解釋: 函式指標:一個指標,用於指向一個函式 函式指標陣列:是一個數組,裡面存放多個函式指標 回撥函式:一個函式,若引數中有函式指標,那麼這個函式便是回撥函式 二、問題分析 這個問題大可用switc

簡單函式指標陣列和函式實現計算器

利用函式指標陣列簡單實現計算器 函式指標陣列:以char *(*p[3])(char *)為例解釋,這是一個數組,陣列名為p,陣列記憶體儲了3個指向函式的指標 這些指標指向一些返回值型別為指向字元的指

openlayer是互動畫一個點、線、面,執行函式

graphicLayer 是一個vector圖層。 callback是回撥函式。 呼叫方法如下:  if (typeof newlayer != 'undefined' && newlayer != null) {                     v

函式實現氣泡排序

(一)什麼是回撥函式呢? 答:回撥函式就是通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用為呼叫它所指向的函式時,我們就說這是回撥函式。 (二)回撥函式的實現機制 1.定義一個回撥函式 2.提供函式實現的一方在初始化時。將回調函式的

vue-cli專案中axios response函式使用箭頭函式 函式this無反應問題

es6使用函式用的是箭頭函式,回撥函式中使用this 或在之前定義好的this,都沒問題; 但是有時es6語法在ie中不支援,修改時,改成一般函式形式,使用this,就會造成this指向找不到問題,也不報錯,打斷點不執行,好像阻塞了,所以之前需定義  var that =

谷歌的AsyncHttpClient簡單模仿安卓的AsyncHttpClient,實現非同步請求函式返回值

實現思路 既然要呼叫Future.get() 才能激發訪問,那麼就想到了使用一個執行緒去訪問。我們就不需要等待阻塞了。 模仿安卓的AsyncHttpClient回撥。根據狀態回撥不同的函式。 實現的效果 執行程式碼...

promise解決函式問題

回撥函式:就是將後續的邏輯傳入到當前要做的事情中,事情做好後呼叫此函式。 let a=''; function buy(callback){ setTimeout(()=>{ a='白菜'; callback() },2000) }

【C/C++開發】函式指標與函式

C++很多類庫都喜歡用回撥函式,MFC中的定時器,訊息機制,hook機制等待,包括現在在研究的cocos2d-x中也有很多的回撥函式。 1.回撥函式 什麼是回撥函式呢?回撥函式其實就是一個通過函式指標呼叫的函式!假如你把A函式的指標當作引數傳給B函式,然後在B函式中通過A函式傳進來的這個指標

emWin介面庫注意事項之自定義函式之後,控制代碼為0

        由於在嵌入式裝置上可供使用的介面庫很少,專案當中所使用的介面庫為德國SEGGER公司開發的emWin介面庫。使用上和windows的GDI大致類似,也提供了豐富的API介面。如果我們需要對控制元件進行自繪的話,一定要進行的一個操作是通過設定回撥

26、【支付模組開發】——支付寶函式實現和查詢使用者訂單狀態介面編寫

####1、支付寶回撥函式實現 我們在除錯支付寶沙箱環境的時候,支護寶會有一個回撥函式,也就是在支付成功之後,可以呼叫我們支付之後需要執行的相關方法,從而達到資料庫的資料和我們的操作相統一。 首先我們先在本地將回調函式編寫好~ 在OrderController類中新建我們的支付寶回撥函式

PHP過濾器及函式寫法

名稱 id 說明 選項options 回撥過濾器(callback) 1024 呼叫自定義函式來過濾資料 callable函式或方法 回撥函式實現 回撥函式必須

關於ssm,前臺html頁面jquery的success函式實現跳轉重新整理問題

$(function(){ $.ajax({ type:“post”, url:"…/…/b/k.action", dataType: “json”, success:function(data){ $(data).each(function(k,v){ $("tbody").a

beginthreadex()函式在建立多執行緒傳入函式時,好像只能傳入全域性函式或類的靜態成員函式,請問能不能傳入類的成員函式呢(非靜態)?

C++類成員函式直接作為執行緒回撥函式2009年06月01日 星期一 17:01我以前寫執行緒時要麼老老實實照著宣告寫,要麼使用C++類的靜態成員函式來作為回撥函式,經常會因為執行緒程式碼而破壞封裝.之前雖然知道類成員函式的展開形式,但從沒想過利用過它,昨天看深入ATL時無意中學

egret 全屏, 和載入資源, 以及函式

1, 有時候在手機瀏覽器中因為有  虛擬按鍵以及標題欄, 使得即便設定了全屏也沒有辦法變成全屏, 但是好像JS 中有方法向瀏覽器請求全屏 2, 載入資源, 關閉後解除安裝, 第二次再進來的時候依然很快, 這是因為瀏覽器有快取 3, egret的回撥函式十分的隨便, 帶引數的回撥函式

js函式傳參

回撥函式是沒有引數的,那怎麼傳遞引數呢? 1 function getEntity(url,callBackFun){ 2 if(callBackFun!=undefined && typeof callBackFun=='function'){ 3 cal