簡單的進行JS異步編程

2018-05-29

簡單記錄開發中的異步問題,把這幾天開發中踩的坑全部敲出來!

為什麼使用異步?

JS是單線程的,她會以單線程的方式運行,但是我們需要同一時刻做多件事情,搶佔式多任務處理

JS單線程的性質其實沒有必要感覺我們被限制住了什麼的…實際上她避開了很多開發人員,為多線程編程中可能出現的更加棘手的問題。

異步的階段

從早期開始,JS的異步就有一套自己的執行方式,但隨著業務要求的提高,傳統的回調異步越發的麻煩所以異步編程的支持也分為幾個不同的階段。

回調(Callback)Promise(承諾)生成器(Generator),發展是這樣的,並不是說後者完全比前者好。

主要的應用場景

  • 網絡請求(Ajax)
  • 文件系統操作(FS讀寫)
  • 刻意的時間延遲功能(警告等…)

回調

一個簡單的回調…

1
2
3
4
5
6
7
8
setTimeout(function(){
console.log("test");
},60*1000);
//或者
function f(){
console.log("test");
}
setTimeout(f,60*1000);

一些支持回調的異步函數舉例:

  • setTimeout :JS內建函數,推遲函數的執行在(第二參數)n毫秒之後;
  • setInterval:每間隔一段時間運行回調函數
  • clearInterval:傳入建立的setInterval,停止該函數的循環執行

關於 Scope和異步執行

我們可以這樣理解

先了解一下閉包,每當一個函數被調用時,都創建了一個閉包:所有在函數內部創建的變量(包括形參)只有在被訪問的時候才會存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
var v1;
function test(){
for(var i=5;i>=0;i--){
setTimeout(
function(){
console.log(i);
v1 = i;
},8*100
);
}
}
test()
console.log(v1)

以上情況常常會出現意想不到的結果,i並不會老老實實的輸出 5,4,3,2,1…而是-1 -1 -1 -1 -1 -1,而且 v1會被第一個輸出undefined

正如上文所述“只有在被訪問的時候才存在”

用白話理解一下,就是setTimeout的回調函數執行之前for的循環就已經執行完了,讓i變成了-1 ;

而且我們會首先看到程序輸出undefined,這是左後一行輸出v1是輸出出來的;

我們很容易把這段程序理解成是線性執行的,然而並不是這樣,回調函數作為普通的函數數據類型傳入到諸如setTimeout這樣的函數中,等到這個函數異步執行這個回調函數,互不幹預。真正的異步是傳到setTimeout中的函數。

錯誤優先回調

這是約定,在Node確定其主導地位中產生。

e.g

1
2
3
4
5
6
fs.readFile("a.txt",function(err,data){
if(err){
return console.error(`error reading ${fname} : ${err.message}`)
}
console.log(`...`)
})

其實就是在回調函數第一個值是用來確認這個操作是否出現錯誤!

回調地獄

寫異步操作的童鞋多少都有過因為這個而苦惱,我也不外乎…尤其是回調函數中的內容想要傳出來用!

最重要的是它不是很好拋異常!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fs.readFile('a.txt',function(err,dataA){
if(err)return console.error
//...一些操作
fs.readFile('b.txt',function(err,dataB){
if(err)return console.error
//...一些操作
fs.readFile('c.txt',function(err,dataC){
if(err)return console.error
//...一些操作
fs.readFile('d.txt',function(err,dataD){
if(err)return console.error
//...一些操作
fs.readFile('e.txt',function(err,dataE){

}
}
}
}
})

Promise

因為使用普通的回調Callback會產生一系列問題:

  • 回調地獄
  • 回調函數的數據難於取出

一個彌補回調操作比較不錯的方案,使用它可以寫出更加安全且易於維護的代碼。她不是排斥回調,更需要配合回調一起使用~

Promise 本質上是一個對象,從她可以獲取異步操作的消息,她提供統一的API,各種異步操作都可以用這個方法來處理。

初衷

在調用基於Promise的異步函數時,他會返回一個Promise實例。其中只可能發生2件事情:

  • 被滿足 success
  • 被拒絕 failure

Promise會保證只有這兩種其中一件發生,而且只會發生一次。一旦被滿足和被拒絕就會被認為處理(settled)了。

Promise只是對象,可以到處飛來飛去的被傳遞,結果可以被其他地方處理。

創建&使用

創建一個帶有函數的Promise實例,包含 resolvereject作為回調函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//創建
function funcA(filenameA){
return new Promise(function(resolve,reject){
fs.readFile(filenameA,function(err,dataA){
if(err){
reject(err)
}else{
resolve(dataA)
}
}
})
}
//使用
funcA("a.txt").then(
function(filedata){
console.log(filedata)
},
function(err){
console.error(err)
}
)

鏈式調用

一個Promise強大的地方,可以解決可啪的回調地獄。

先來理解一下,Promise可以作為函數的返回值,所以說在 .then() 中的“回調函數”可以再次返回一個Promise!

1
2
3
4
5
6
7
8
9
10
11
//摘自MDN的🌰
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

catch()

前面說過回調地獄也不能被try…catch正常的補獲,於是可以和.then()類比接在返回的Promise后的.catch()

其實,.catch()相當於.then(null, failureCallback)

如第一個例子所示,.then()參數1是resolve(回調函數)、參數2是reject回調函數(出現問題)

如果想要在回调中获取上个promise中的结果,上个promise中必须要返回结果。

一个promise链式遇到异常就会停止,查看链式的底端,寻找catch处理程序来代替当前执行

BTW在Promies若是想調用多個回調函數可以使用Node的內建支持EventEmitter

假裝自己是同步的操作(推薦)

async/await在ECMAScript 2017标准的一個語法糖

1
2
3
4
5
6
7
8
9
10
async function foo() {
try {
let result = await doSomething();
let newResult = await doSomethingElse(result);
let finalResult = await doThirdThing(newResult);
console.log(`Got the final result: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}

使用她吧~

Promise在項目中是會常常使用,而且是推薦使用的!

  • 有些常用的包自己有自己的Promise支持版本;
  • 在舊API中直接在Promise中用也是可以的;

請大膽的使用!

生成器

說到生成器,需要了解一下這個ES6中引入的非常重要的概念迭代器生成器

####迭代器

for...of是很容易被理解的,她對任何提供迭代器的結構都試用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const book = [
"蕾咪坐在門口",
"吃著手裡的漢堡",
"咲夜希望蕾咪少吃漢堡",
"多喝可樂"
];

const it = book.values();
//通過數組的values方法獲取迭代器

it.next(); //{value:"蕾咪坐在門口",done:false}
it.next(); //{value:"吃著手裡的漢堡",done:false}
it.next(); //{value:"咲夜希望蕾咪少吃漢堡",done:false}
it.next(); //{value:"多喝可樂",done:false}
it.next();//{value:undefined,done:true}

創建的每一個迭代器都是不同的,迭代器更多的操作玩法,諸如迭代器協議什麼的可以前去查資料(略跑題)。

生成器

生成器是使用迭代器來控制其運行的函數。

一般情況下,函數接到調用的參數懟回return的結果,但其調用者無法控制函數內部的運行,調用函數時實際上是放棄了對該函數的控制,直到函數返回。有了生成器,她可以在函數執行時對其進行控制。(聽起來很累!)

說回生成器這邊,它提供了兩種能力:

  • 控制函數執行的能力;
  • 使函數能夠分步驟運行;

同時她也應用於不同的地方:

  • 函數可以通過域yield,在其運行的任意時刻將控制權交還給調用方。
  • 調用生成器的時候,它不是立即執行,而先返回迭代器中!函數會在調用迭代器的next方法時執行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//一個生成器
function* koumakan(){
yield "Remilia Scarlet";
yield "Hong Meirin";
yield "Patchouli Knowledge";
yield "Sakuya Izayoi";
yield "Flandre Scarlet";
yield "Koakuma";
let for_return_value = yield "What's your favorite role?";
return "${for_return_value} is my favorite role";
}
//在迭代器中使用
const emmmmm = koumakan();
emmmm.next(); //{value:"Remilia Scarlet",done:false}
emmmm.next(); //{value:"Hong Meirin",done:false}
emmmm.next(); //{value:"Patchouli Knowledge",done:false}
emmmm.next(); //{value:"Sakuya Izayoi",done:false}
emmmm.next(); //{value:"Flandre Scarlet",done:false}
emmmm.next(); //{value:"Koakuma",done:false}
emmmm.next(); //{value:"What's your favorite role?",done:false}
emmmm.next("Remilia Scarlet"); //{value:"Remilia Scarlet is my favorite role",done:false}
emmmm.next(); //{value:undefined,done:true}
1
2
3
for(let role of koumakan()){
console.log(role)
}

生成器不可以使用箭頭函數創建,只可以用function*

生成器本質上是是將計算工作延遲了,只在需要的時候進行!

在異步中的使用生成器

先將Node中錯誤優化優先轉換成Promis的方法

因為生成器的逐步執行是需要在 yield後面跟已經成型的Promies。回調函數?不可能的!

1
2
3
4
5
6
7
8
function nfcall(f,...args){
return new Promise(function(resolve,reject){
f.call(null,...args,function(err,...args){
if(err) return reject(err);
resolve(args.length<2 ? args[0] : args);
});
});
}

其實更推薦使用Q中的nfcall,這個只是便於理解

創建一個執行生成器的函數

首先我們需要一個生成器的運行器,因為是生成器本質天生並不是異步的。

e.g:

1
2
3
4
5
6
7
8
9
10
11
12
13
function grun(g){
const it = g();
(function iterate(val){
const x = it.next(var);
if(!x.done){
if(x.value instanceof Promise){
x.value.then(iterate).catch(err=>it.throw(err));
}else{
setTimeout(iterate,0,x.value);
}
}
})
}

可以向runGenerator()傳入一個生成器函數,然後會返回該函數。

1
2
3
4
5
6
7
8
9
function* handleFiles(){
const dataA = yield nfcall(fs.readFile,'a.txt');
const dataB = yield nfcall(fs.readFile,'b.txt');
const dataC = yield nfcall(fs.readFile,'c.txt');
yield ptimeout(60*100);
yield nfcall(fs.writeFile,'all.txt',dataA+dataB+dataC);
}
//使用
grun(handleFiles);

生成器的異常處理

在function* 中直接建立即可~

你需要知道的

  • JS中異步是通過回調來管理的;
  • Promise不能替代回調函數,她需要then和catch回調函數;
  • Promise可以接解決一回調掉被多次調用的問題;
  • Promise可以被鏈式調用;

需要注意的

  • 不要自己作死寫這文章所講的一切基礎設施(包括但不止 生成器運行器),可以使用co或者Koa。也不要怕自己將Node格式回調轉化Promies,用一下Q;

寫在最後,希望能幫助你理解JS異步的相關操作。

文檔信息

版權聲明:自由轉載-非商用-非衍生-保持署名

2018年5月29日


Comments: