promise、generator、async的簡單應(yīng)用

javascript的運行機制是單線程處理,即只有上一個任務(wù)完成后,才會執(zhí)行下一個任務(wù),這種機制也被稱為“同步”。

“同步”的最大缺點,就是如果某一任務(wù)運行時間較長,其后的任務(wù)就無法執(zhí)行。這樣會阻塞頁面的渲染,導(dǎo)致頁面加載錯誤或是瀏覽器不響應(yīng)進入假死狀態(tài)。

如果這段任務(wù)采用“異步”機制,那么它就會等到其他任務(wù)運行完后再執(zhí)行,不會阻塞線程。

一、es6之前實現(xiàn)異步的方式:

最常用的方法是采用回調(diào)函數(shù),但普通的回調(diào)函數(shù)并不能實現(xiàn)異步效果:

(1)同步回調(diào)
function testFn(data, callback){
  callback(data);
}
testFn('0', function(data2){
  for (var i=data2; i<300000000; i++);
  console.log(1);
});

console.log(2);

等一秒鐘左右for循環(huán)結(jié)束,控制臺輸出1后,才會輸出2。這是因為 testFn 的回調(diào)函數(shù) callback 是同步執(zhí)行,所以for循環(huán)會阻塞后面的任務(wù)。

(2)異步回調(diào)

想要實現(xiàn)回調(diào)的異步執(zhí)行,必須要借助js的其它方法,例如將上面 testFn 的回調(diào)函數(shù)放到延時器中調(diào)用,延遲的時間設(shè)置為0:

function testFn(data, callback){
  setTimeout(function(){
    callback(data)
  },0);
}

這樣控制臺會先輸出2,接著大約一秒鐘后再輸出1,可見回調(diào)函數(shù)并沒有阻塞后面的任務(wù),實現(xiàn)了異步效果。

除了使用定時器,實現(xiàn)異步執(zhí)行的方法還有:事件監(jiān)聽(例如點擊事件“click”)、requestAnimationFrame、XMLHttpRequest(jq中ajax方法的核心)、WebSocket、Worker以及Node.js 的 fs.readFIle等。

二、promise、generator和async/await:

es6 引入了 generator 和 promise,es7 又增加了async/await。這三種函數(shù)有一個重要的作用,就是解決回調(diào)函數(shù)的異步執(zhí)行和嵌套問題。

(因為網(wǎng)絡(luò)上介紹這三種方法的文章很多,所以這里只介紹個人覺得相對常用的知識點)
(一)普通的回調(diào)嵌套:

定義一個函數(shù),要求只有在獲取兩個數(shù)據(jù)(例如"data1"和"data2")后,才會執(zhí)行“console.log”任務(wù),通過對回調(diào)函數(shù)進行嵌套就可以達到目的:

function Test1(data, callback){
    if(data){          // 本例是同步,要實現(xiàn)異步可以添加延時器
        callback(data);
    };
};

Test1("data1",function(){        
    Test1("data2",function(data2){
        console.log(data2);     // 如果不傳數(shù)據(jù)就不會執(zhí)行回調(diào)函數(shù)
    });
});

回調(diào)函數(shù)的執(zhí)行需要依賴上一個函數(shù),這樣的缺點是如果有多個回調(diào)函數(shù),就需要嵌套很多層。這會使代碼的可讀性較差,并增加調(diào)試和維護的難度。

(二)promise

Promise是一個構(gòu)造函數(shù),它接收一個匿名函數(shù)作為參數(shù):

new Promise(function(resolve, reject){
  resolve(console.log(1));
  reject(console.log(2));
  console.log(3);
})

console.log(4);
// 輸出的順序為1、2、3、4 

1、因為 promise 是構(gòu)造函數(shù)(帶有屬性或方法的函數(shù)就叫構(gòu)造函數(shù)),所以必須使用 new 實例化才能調(diào)用。而作為參數(shù)的匿名函數(shù)不需要額外調(diào)用就能執(zhí)行。

2、匿名函數(shù)只能有 resolve 和 reject 兩個參數(shù)。結(jié)合if判斷使用的話,resolve 是條件正確時執(zhí)行的方法,reject 則是錯誤時執(zhí)行的方法。

3、將某一任務(wù)直接放到 resolve、reject 方法里,或者放到匿名函數(shù)里并不能實現(xiàn)異步效果。

function Test2(){
  return new Promise(function(resolve, reject){
    resolve(1);                     
  });
};

Test2().then(function(num){
  console.log(num);
  return num;
}).then(function(num2){
  console.log(num2);
})
console.log(2);      
// 數(shù)字輸出的順序是2、1、1

4、為了防止匿名函數(shù)的自動執(zhí)行,需要再定義一個函數(shù),并將 promise 函數(shù)作為該函數(shù)的返回值。

5、將回調(diào)函數(shù)寫在 then 或 catch 方法里才能實現(xiàn)異步,then 接受的是 resolve 方法傳遞的數(shù)據(jù),catch 則對應(yīng)的是 reject。

6、resolve 只能向第一個 then 里的函數(shù)傳遞數(shù)據(jù),后面 then 里的函數(shù)只能通過前一個 then 里函數(shù)的返回值獲取參數(shù)。

function pro1(){
  return new Promise(function(resolve, reject){
    resolve(1);
  });
};

function pro2(){
  return new Promise(function(resolve, reject){
  });
};

Promise.all([ pro1(), pro2() ]).then().catch();
Promise.race([ pro1(), pro2() ]).then().catch();

7、all方法的作用:只有 pro1 和 pro2 都執(zhí)行 resolve,Promise 才會執(zhí)行 then 方法。如果其中有一個函數(shù)執(zhí)行 reject,那么Promise 就執(zhí)行catch方法。

8、race方法的作用:pro1 和 pro2 中只要有一個狀態(tài)發(fā)生改變,Promise的狀態(tài)就跟著發(fā)生改變,不論是resolve還是reject。

promise最大的作用

(一)能把嵌套改成鏈式調(diào)用。對上面的普通嵌套進行改造:

function Test2(data){
  return new Promise(function(resolve, reject){
    if(data){
      resolve(data);
    }
  });
};

Test2(1).then(function( data1 ){
  console.log( data1 );
  return 2;     
}).then(function( data2 ){
  console.log( data2 );
})

console.log(3);
// 輸出結(jié)果為3、1、2

實現(xiàn)效果:
(1)先輸出3,表示 then 方法里的回調(diào)函數(shù)是異步執(zhí)行。
(2)如果調(diào)用 Test2 時不傳數(shù)字“1”,就不會執(zhí)行 resolve 方法,這樣即使第一個 then 方法里的函數(shù)有返回值“2”,也不會執(zhí)行第二個 then 方法,實現(xiàn)了需求。

(二)解決 ajax 不能傳值給外部變量的問題
ajax 在 success 獲取的數(shù)值無法傳遞給外部變量,除非設(shè)置為同步模式,而 promise 的 resolve 方法可以解決這個問題。

設(shè)一個外部變量 outsideData:

var outsideData ;
function TestAjax(data){
  return new Promise(function(resolve, reject){
    $.ajax({
        url: 'xxx.php';
        type: 'GET',
        datatype: 'json',
        data: ' ',
        success: function(res){
          resolve(res.data)
        }
    });
  });
};

TestAjax().then(function( data ){
  outsideData = data;
})
(三)async/await

先說 async/await,是因為 async 函數(shù)是 Generator 函數(shù)的語法糖,容易理解,而且和 promise 函數(shù)也有很大的關(guān)聯(lián)。

用 async function 定義一個函數(shù):

async function Test(){      
  return 1;         
}
Test().then(function(num){
  console.log(num);      
});
console.log( Test() );

得到的結(jié)果是

1、async function 返回的是一個 promise 對象,所以該函數(shù)可以調(diào)用 then 方法,并因此實現(xiàn)異步執(zhí)行效果。

async function Test(){      
  await console.log( 1 );
  console.log( 2 );
  await console.log( 3 );
}
Test();
console.log( 4 );
// 結(jié)果為1、4、2、3

2、await 可以代替 then 方法,以實現(xiàn)異步效果。

3、但第一個 await 是同步執(zhí)行。不論跟的是 promise,還是其他的函數(shù)或方法。

4、第一個 await 后面的任務(wù),不論有沒有 await 都是異步執(zhí)行。但是如果要在 async 函數(shù)里再放一個函數(shù),那前面就必須添加 await,否則會報錯。

function proFn(){
  return new Promise(function(resolve, reject){
    resolve(2);
  });
};

async function Test(){
  var data1 = await proFn();
  console.log(1);
  console.log(data1);
};
Test();
console.log(3);
// 結(jié)果為3、1、2

5、對于第一個 await,最好的方法是使其等于一個變量,然后對這個變量進行處理。

6、如果 await 后面跟的是 promise ,那么匿名函數(shù)必須執(zhí)行 resolve 方法,否則 await 后面的任務(wù)就無法執(zhí)行。

async/await 最大的作用就是替代 promise 的 then 方法:
function proFn(data){
  return new Promise(function(){
    if(data){
      resolve(data);
    };
  });
};

async function Test(){
  var data1 = await proFn(1);
  var data1 = await proFn(2);
  console.log(data1);
  console.log(data2);
};
Test();

console.log(3);
// 結(jié)果為3、1、2

(1)async/await 將鏈式調(diào)用變得更加簡化。
(2)async/await 傳遞參數(shù)的方式也比 then 簡單,不需要通過 return 來傳遞參數(shù)。

(四)generator

和 promise、async/await 不同,generator 本身并不具有異步執(zhí)行的功能。它在異步中的主要應(yīng)用,是管理異步回調(diào)的執(zhí)行流程。

generator 函數(shù)的特征是使用 * 和關(guān)鍵詞 yield:

function* Gen(){
  yield 1;
}
var runGen = Gen();
console.log(runGen.next());  
// 輸出結(jié)果為 {value: 1, done: false};

1、函數(shù)名后面加小括號并不能調(diào)用 generator 函數(shù),只是創(chuàng)建了一個指針對象,next 方法才會執(zhí)行函數(shù)。

2、next 方法返回的對象帶有兩個屬性,分別是“done”和“value”。done 的值表示 generator 函數(shù)是否運行完。value 對應(yīng)的是 yield 后面的值。

function* Gen (num){
  var num2 = yield console.log(num);
  yield console.log(num2);
}
var runGen = Gen(1); 
runGen.next(2);
runGen.next(3);
// 結(jié)果輸出1和3。                      

3、next 方法可以傳遞參數(shù),該參數(shù)是上一個 yield 表達式的值。
之所以沒有輸出“2”,就是因為第一個 next 方法并沒有與之對應(yīng)的"上一個 yield",所以傳參無效。

generator 函數(shù)管理流程的應(yīng)用:

使用 generator 管理異步函數(shù),需要用到三個知識點,thunk函數(shù)、next方法得到的done屬性、遞歸。

1、thunk函數(shù)
普通的多參數(shù)函數(shù)在調(diào)用時,需要一次性傳入多個數(shù)據(jù)。
thunk 函數(shù)則是把多個參數(shù)拆開,使得在調(diào)用時,數(shù)據(jù)可以分開傳入。

實現(xiàn)方法是定義一個函數(shù),并且該函數(shù)的返回值也是一個函數(shù),這樣就能將參數(shù)拆開放在兩個函數(shù)里:

// 普通的多參數(shù)函數(shù):
function Test(data, callback){};
// 調(diào)用普通函數(shù):
Test(data, callback);


// thunk函數(shù):
function Test(data){
 return function(callback){
   callback(); 
 }
}
// 調(diào)用thunk函數(shù):
var runThunk = Test(data);
runThunk(callback);

將 callback 參數(shù)提取出來,放在作為返回值的匿名函數(shù)里,在調(diào)用該函數(shù)時 callback 和 data 所對應(yīng)的數(shù)據(jù)就可以分兩步傳入。

2、通過 done 屬性控制流程:

generator 函數(shù)代替“嵌套”去控制流程的思路,就是通過上一個 yield 的執(zhí)行情況,來決定下一個 next 方法是否執(zhí)行,這需要用到 done 屬性:

function Test(){
  setTimeout(function(){
    console.log(1);
  },0);     
}
function* Gen(){
  yield Test();
  yield console.log(2);
}

var runGen = Gen();
var genObj = runGen.next();

if(!genObj.done){
  runGen.next();
}
// 結(jié)果為2、1

(1)直接給 next 方法外面添加 if 判斷的缺點是,假如某個 yield 后面跟的是異步函數(shù),那么其他 yield 所對應(yīng)的非異步任務(wù)就會優(yōu)先執(zhí)行。

如果必須保證前一個任務(wù)運行完后,才會執(zhí)行下一步,就需要把 next 方法放到 value 屬性里:

function Test2(){
  return function(callback){
      console.log(3);
      callback();
  }
}
function* Gen2(){
  yield Test2();
  yield console.log(4);
}

var runGen2 = Gen2();
var genObj2 = runGen2.next();

genObj2.value( function(){
  runGen2.next();
} );
// 得到的結(jié)果是 3、4

(2)想在 value 里使用 next 方法,需要將 value 變成一個函數(shù),這就要用到 thunk 函數(shù)。而 value 后面加一個小括號,就能調(diào)用作為返回值的函數(shù)了。

(3)如果把 next 方法直接放到 value 里,那么 next 方法得到的結(jié)果會被當成 value 的參數(shù),先輸出。所以需要給 next 方法外面再包一層函數(shù)。

下面的函數(shù)就是最終形態(tài):

function Test3(){
  return function(callback){
    setTimeout(function(){
       console.log(5);
       callback();
    },0)  
  }
}
function* Gen3(){
  yield Test3();
  yield console.log(6);
}

var runGen3 = Gen3();
var genObj3 = runGen3.next();

genObj3.value( function(){
  if(genObj3.done) return;
  runGen3.next();
} );
// 結(jié)果為5、6
3、使用遞歸自動執(zhí)行 generator 函數(shù):

首先看看手動執(zhí)行 generator 的例子:

// 為了簡潔,本例并沒有使用異步
function Thunk(num){
  return function(callback){
    console.log(num);
    callback();          
  }; 
};

function* Gen(){
  yield Thunk(1);
  yield Thunk(2);
}

var runGen = Gen();

var runNext = runGen.next();
if(runNext.done) return;
runNext.value(function(){
  
  var runNext2 = runGen.next();
  if(runNext.done) return;
  runNext2.value(function(){
   
  });
});
// 結(jié)果輸出1、2

假如存在多個 yield,就需要寫很多 next,這會令代碼變得臃腫。通過觀察可以看出使用 next 方法的部分存在很大的重復(fù)性,所以可以使用遞歸(也就是函數(shù)內(nèi)部調(diào)用自身)對其進行改造。

var  runGen = Gen();

function next(err, data) {
  var runNext = runGen.next(data);
  if (runNext.done) return;
  runNext.value(next);          
}
next();

5、修改 promise 的鏈式調(diào)用

function Test(num){
  return new Promise( function(resolve, reject){
    resolve(num);
  } );
}

function* Gen(){
  yield Test(1);
  yield Test(2);
}

var runGen = Gen();
function next(){
  var genObj = runGen.next();
                
  if(genObj.done) return;
  genObj.value.then(function(num){
    console.log(num);
    next();
  });
}
next();

console.log(3);
// 得到的結(jié)果為 3、1、2

總結(jié):

關(guān)于es6的研究到此就告一段落了。個人覺得“類”和“箭頭函數(shù)”是一定要掌握的,因為這兩點能簡化代碼結(jié)構(gòu)。至于異步,從例子的長短也能看出,generator 沒必要了解很深,還是交給 promise 和 ansyc/await 吧。

三、參考:

1、http://www.cnblogs.com/webeye/p/5383785.html (js同步的缺點)
2、http://blog.csdn.net/tywinstark/article/details/48447135 (15樓回復(fù))
3、http://stackoverflow.com/questions/9516900/how-can-i-create-an-asynchronous-function-in-javascript (js實現(xiàn)異步的方法)
4、http://www.ruanyifeng.com/blog/2014/10/event-loop.html (js運行機制)
5、http://www.nowamagic.net/librarys/veda/detail/787 (瀏覽器假死原因)
6、https://segmentfault.com/a/1190000003096984 (異步回調(diào)的缺點)
7、https://www.oschina.net/translate/event-based-programming-what-async-has-over-sync (回調(diào)函數(shù)嵌套的缺點)
8、https://segmentfault.com/q/1010000002577322 (回調(diào)函數(shù)如何實現(xiàn)異步)
9、http://es6.ruanyifeng.com/#docs/promise (promise知識點)
10、http://www.cnblogs.com/lvdabao/p/es6-promise-1.html (promise需要放到另一個函數(shù)里)
11、https://segmentfault.com/a/1190000007535316(await知識點)
12、http://blog.rangle.io/javascript-asynchronous-options-2016/(generator不是異步)
13、http://es6.ruanyifeng.com/#docs/generator (generator知識點)
14、http://www.liaoxuefeng.com/wiki/ (generator函數(shù)的調(diào)用)
15、http://www.itdecent.cn/p/87183851756f (promise使用例子)
16、https://segmentfault.com/q/1010000011014844 (使用return返回ajax獲取的數(shù)值)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 異步編程對JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的,如果沒有異步編程,根本...
    呼呼哥閱讀 7,399評論 5 22
  • 簡單介紹下這幾個的關(guān)系為方便起見 用以下代碼為例簡單介紹下這幾個東西的關(guān)系, async 在函數(shù)聲明前使用asyn...
    _我和你一樣閱讀 21,476評論 1 24
  • 相關(guān)github草稿代碼在此處相關(guān)es6教程在此處 大綱 JS與python的同步和異步 generator的執(zhí)行...
    王俊宇閱讀 2,768評論 0 3
  • 弄懂js異步 講異步之前,我們必須掌握一個基礎(chǔ)知識-event-loop。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,867評論 0 5
  • 根據(jù)筆者的項目經(jīng)驗,本文講解了從函數(shù)回調(diào),到 es7 規(guī)范的異常處理方式。異常處理的優(yōu)雅性隨著規(guī)范的進步越來越高,...
    黃子毅閱讀 8,679評論 7 37

友情鏈接更多精彩內(nèi)容