異步發展簡明北

  • 異步的誕生
  • ajax年代
  • Promise年代
  • promise和生成器
    • 什麼是生成器
  • 怎麼和promise配合
    • Co
  • Async/Await

異步的誕生

javascript因爲設計之初被設計成了單線程,So,這會致使一個問題,若是有一個任務的量過重很耗費時間,那這個任務後面的代碼就會由於它被阻塞好久才能執行。javascript

有些man以爲這段等待的時間蠻浪費的,因而冒出了一個想法,有木有辦法不讓咱們這麼幹等着,它操做它的,咱們剩下的程序繼續執行本身的,當它最終拿到結果了再通知咱們。css

嗯……異步解決的就是這麼個東東。。。java

但這樣推理異步的誕生私覺得是錯誤的。git

事實上,javascript被設計出來的主要做用就是用來處理DOM的,它天生就是異步的,若是說有什麼是爲了適應這種設計而生,那麼單線程這種設計纔是故意而爲之的。github

爲何這麼說呢?ajax

想象一下,咱們要一個元素在0.5秒的時間向左移動100px,接着再讓它在0.5秒的時間往右移動100px,若是是多線程,這兩個任務幾乎會同時下單,也就意味着這個元素幾乎不會動,這顯然和咱們預期的結果不一樣。編程

而若是是單線程若是是異步操做,那麼這個元素會先向右運動,而後在0.5秒的時間完成運動後,將向左的運動做爲宏任務加入到callbacks queque中,做爲一輪單獨在執行棧中再執行,縱然它又會被當作異步任務分發出去,但卻確保了向左的操做是在向右操做完成之後的某個時機纔開始執行的。json

雖說多線程不是不能作到(相似於鎖這樣的操做),但實現起來確定不如一個線程簡單(一我的幹事不存在多我的幹事須要協調的問題),而javascript最初只用了10天的時間就被創造了出來!promise

ajax年代

在這個年代,咱們的網站再也不是一灘死水,咱們開始能經過異步的HTTP請求來更新咱們網頁的部分信息,bash

咱們的代碼中開始出現這樣的書寫結構

$.ajax({
     type: "GET",
     url: "地址!!",
     data: {param1:xxx, param2:xxx},
     dataType: "json",
     success: function(data){
        
      }
 });
複製代碼

上一段落咱們說過,異步任務幫咱們解決了阻塞問題,js的回調機制(事件環)幫咱們解決了異步任務的執行順序問題,但成也蕭何敗蕭何,有些場景咱們的異步任務是須要嵌套的,一層套一層,那麼咱們的代碼就會長成這樣

$.ajax({
     type: "GET",
     url: "地址!!",
     data: {param1:xxx, param2:xxx},
     dataType: "json",
     success: function(data){
        $.ajax({
            type: "GET",
            url: "地址!!",
            data: {param1:xxx, param2:xxx},
            dataType: "json",
            success: function(data){
                $.ajax({
                    type: "GET",
                    url: "地址!!",
                    data: {param1:xxx, param2:xxx},
                    dataType: "json",
                    success: function(data){

                    }
                });
            }
        });
      }
 });
複製代碼

這就是所謂的回調地獄了

嗯....這維護起來同志們確定、鐵定、必定呀!以爲至關不方便! 因而就開始折騰。。。想去改變這種傳統異步方法的書寫形式,想辦法讓代碼更易讀易維護

Promise年代

Promise 的原理與用法詳見個人這篇白菜大文 Promise深度學習—我のPromise/A+實現

這個年代,咱們在書寫異步代碼的形式上取得了必定程度的進步,咱們寫起代碼來是像這個樣子滴

$('div').find().css()...
複製代碼

嗯,開了個玩笑別介意。。。其實大致想法就是這樣的,像jQ同樣鏈式書寫異步代碼

read(url,encode){
    return new Promise((resolve,reject)=>{
        readFile(url,encode,(err,data)=>{
            if(err)?reject(err):resolve(data); 
        })
    })
}
read('1.txt','utf8').then(value=>{
	return readFile(value,'utf8'); //根據1.txt的內容來查找讀取2.txt
}).then(value=>{
	return readFile(value,'utf8');  //根據2.txt的內容來查找讀取3.txt
}).then((value)=>{
	console.log(value); //輸出3.txt的內容
}).catch((err)=>{
	//deal with error
})
//下一次then接收的參數爲上一次return的結果,若是這個return的結果爲promise則爲promise的結果
複製代碼

嗯。。。好想好上很多?

emmm....好上很多才有鬼咧!

雖然經過promise的then方法讓咱們實現了鏈式調用,但咱們還須要手動將本來的異步API進行一次封裝,而且還要每次在then中將這個封裝的函數return執行,這。。。。

[imortant] promise就像是一個異步API的包裝器,它能將傳統的異步API的本體回調部分進行分離,讓咱們更好的專一於異步回調的處理。

promise和生成器

我的以爲單單是promise的話,其實至關的。。。雞肋!真正使promise發揚光大的是在人們認識到不論怎樣異步終究是異步終究是一種反人類的操做,咱們理應豎起大義的旗幟開始反擊的時候。

什麼不反人類?固然是同步代碼啊!書寫簡單又易於閱讀~

那怎麼作到呢?其實藉由生成器這麼個東東咱們就可以實現啦。

什麼是生成器

那麼,咱們須要先了解一下生成器是什麼

生成生成,就是要生點什麼,那麼生成器生了點什麼呢?生成器實際上生成了迭代器

emmm...那迭代器又是個什麼鬼呢?迭代器其實就是有next方法的對象,每次調用next方法都會返回一個data和一個標識符(用來標識是否已經迭代完畢)。

嗯,可能這麼解釋仍是不怎麼清楚。其實生成器它自己是一個函數,或則說是一個集成的函數,它用*來標識它本身,像這樣function *gen(){},而後咱們每次調用迭代器的next方法的時候,生成器方法就會被執行一部分,只有咱們經過不斷調用next,這個生成器方法纔會被完全執行完成,並在最後一次next調用時返回done:false的標識。

咱們來看一個示例

function *r(){
  let content1 = yield read('./1.txt','utf8');
  let content2 = yield read(content1,'utf8');
  return content2;
}
let it  = r();
複製代碼

其中*r就是一個生成器函數,而it就是這個生成器函數生成的迭代器。每一次it.next(),生成函數都會執行一部分

其中青色的線框住的部分就是第一次調用 it.next時執行的代碼,橘色的是第二次,紅色的是第三次。

也就是說每次調用時以yield爲分界的,yield表明產出,它會以yield後面的部分做爲next調用時返回的value值。

另外還有點須要注意的是生成器裏的yield左邊的=並不表明賦值運算,而表明調用next時會接受一個參數傳入做爲輸入,而content一、content2其實是做爲參數傳入的形參。

[warning] 注意: 第一次迭代是沒法傳入參數的,但生成器生成迭代器時能夠接收參數做爲輸入。

最後生成器方法的return的值就是最後一次next調用時返回的value值,而且此時的done爲true。另外不是說今後以後不能再調next了,只是獲得的對象永遠都會是{value:undefined,done:true}

怎麼和promise配合

咱們的目的是爲了使異步代碼書寫起來看起來像是同步代碼同樣

咱們知道生成器函數是分段執行的,且每次迭代都會接受一個參數做爲輸入,而後每次都會yield產出。So咱們能利用它這種機制

function *r(p1){
  console.log(p1)
  let content1 = yield read('./1.txt','utf8');
  let content2 = yield read(content1,'utf8');
  return content2;
}
let it  = r('生成迭代器時傳入的參數');
//第一次迭代
it.next().value.then(function(data){ // 2.txt
//第二次迭代
  it.next(data).value.then(function(data){
  //第三次迭代,迭代完畢
    console.log(it.next(data).value);
  });
});
複製代碼

上面的示例中,若是咱們只看*r裏面的內容,那麼這樣書寫的形式幾乎是和同步木有區別的。

那麼,有沒有一種方法可以讓*r下面那一團子代碼在咱們在生成其中寫完代碼後就本身產生呢?

Co

嗯,Co的出現就是爲了解決這個問題的,Co是TJ大姥姥寫的一個庫,能幫咱們自動生成迭代代碼

function *read() {
  console.log('開始');
  let a = yield readFile('1.txt'); 
  console.log(a);
  let b = yield readFile('2.txt'); //執行這裏時必然有一個le a的輸入,就像是上一句代碼當即獲得了返回值同樣
  console.log(b);
  let c = yield readFile('3.txt');
  console.log(c);
  return c;
}

//咱們只需在生成器裏寫完代碼後再加上這麼一句
co(read).then(function(data){
	console.log(data); //data爲成器函數中c的值
})

//---
function readFile(filename) {
  return new Promise(function (resolve, reject) {
    fs.readFile(filename, 'utf8', function (err, data) {
      err ? reject(err) : resolve(data);
    });
  })
}

複製代碼

那麼這是怎麼實現的呢?從代碼量上來講其實很簡單,就幾行代碼,

function co(gen){ //傳入一個生成器
    let it = gen(); //生成一個迭代器
    return new Promise((resolve,reject)=>{
    	!function next(lastVal){
        //這裏的next的lastVal參數即爲上一次迭代出的promise的結果,也是a的值,而後依次類推...
            let{value,done} = it.next(lastVal);
            if(done) {
                resolve(value); //若是生成器函數執行完成就讓co的promise成功
            }else{ //若是尚未迭代完,在這次返回的promise中綁定回調,當狀態改變時調用下一次迭代
                value.then(next,reject); 
            }
    	}()
    })
}

// 效果等同於前文所說的
//第一次迭代
it.next().value.then(function(data){ // 2.txt
//第二次迭代
  it.next(data).value.then(function(data){
  //第三次迭代,迭代完畢
    console.log(it.next(data).value);
  });
});
複製代碼

思路分析: yield readFile('1.txt')執行完畢,會等待下一次迭代和let a的輸入,而等到何時呢?會等到readFile這個異步函數獲得結果後纔會繼續走。這時let a對於yield readFile('2.txt')是有效的,就像同步代碼中當即獲得了返回值同樣。


通過上面一遭咱們終於可以像寫同步代碼同樣寫異步了,但美中不足的是每次在咱們在生成器中寫完異步代碼,都須要在最後用Co來生成對應的迭代代碼,那有沒有更簡單的方法呢?嗯。。。有的!

Async/Await

Async/Await 其實是 promise+迭代器實現的語法糖,常和bluebird promise實現庫 結合起來使用,號稱異步的終極解決方案。

let Promise = require('bluebird');
let readFile = Promise.promisify(require('fs').readFile);
async function read() {
  //await後面必須跟一個promise,
  let a = await readFile('./1.txt','utf8');
  console.log(a);
  let b = await readFile('./2.txt','utf8');
  console.log(b);
  let c = await readFile('./3.txt','utf8');
  console.log(c);
  return 'ok';
}

read().then(data => {
  console.log(data);
});
複製代碼

拋去語法糖的糖衣,其實就是對Co進行了一層封裝

//co實現
 function read(){
     return co(function *(){
         let a = yield readFile('./1.txt');
         console.log(a);
         let b = yield readFile('./2.txt');
         console.log(b);
         let c = yield readFile('./3.txt');
         console.log(c);
         return 'ok';
     });
 }
複製代碼

到此爲止,咱們終於走完了異步編程10年發展的慢慢長路,鼓掌!!!


參考資料:

相關文章
相關標籤/搜索