- 異步的誕生
- ajax年代
- Promise年代
- promise和生成器
- 什么是生成器
- 怎么和promise配合
- Co
- Async/Await
[TOC]
## 異步的誕生
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+實現](https://fancierpj0.github.io/iPromise/)
這個年代,我們在書寫異步代碼的形式上取得了一定程度的進步,我們寫起代碼來是像這個樣子滴
```
$('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年發展的慢慢長路,鼓掌!!!
---
參考資料:
- [Co](https://github.com/tj/co)