非同步發展簡明北

Cris_冷崢子發表於2018-01-19
  • 非同步的誕生
  • ajax年代
  • Promise年代
  • promise和生成器
    • 什麼是生成器
  • 怎麼和promise配合
    • Co
  • Async/Await

非同步的誕生

javascript由於設計之初被設計成了單執行緒,So,這會導致一個問題,如果有一個任務的量太重很耗費時間,那這個任務後面的程式碼就會因為它被阻塞很久才能執行。

有些man覺得這段等待的時間蠻浪費的,於是冒出了一個想法,有木有辦法不讓我們這麼幹等著,它操作它的,我們剩下的程式繼續執行自己的,當它最終拿到結果了再通知我們。

嗯……非同步解決的就是這麼個東東。。。

但這樣推理非同步的誕生私以為是錯誤的。

事實上,javascript被設計出來的主要作用就是用來處理DOM的,它天生就是非同步的,如果說有什麼是為了適應這種設計而生,那麼單執行緒這種設計才是故意而為之的。

為什麼這麼說呢?

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

而如果是單執行緒如果是非同步操作,那麼這個元素會先向右運動,然後在0.5秒的時間完成運動後,將向左的運動作為巨集任務加入到callbacks queque中,作為一輪單獨在執行棧中再執行,縱然它又會被當做非同步任務分發出去,但卻確保了向左的操作是在向右操作完成以後的某個時機才開始執行的。

雖然說多執行緒不是不能做到(類似於鎖這樣的操作),但實現起來肯定不如一個執行緒簡單(一個人幹事不存在多個人幹事需要協調的問題),而javascript最初只用了10天的時間就被創造了出來!

ajax年代

在這個年代,我們的網站不再是一灘死水,我們開始能通過非同步的HTTP請求來更新我們網頁的部分資訊,

我們的程式碼中開始出現這樣的書寫結構

 $.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時會接受一個引數傳入作為輸入,而content1、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年發展的慢慢長路,鼓掌!!!


參考資料:

相關文章