前端非同步(async)解決方案
注:篇幅詳細,請逐步消化
javascript是一門單執行緒語言,即一次只能完成一個任務,若有多個任務要執行,則必須排隊按照佇列來執行(前一個任務完成,再執行下一個任務)。
這種模式執行簡單,但隨著日後的需求,事務,請求增多,這種單執行緒模式執行效率必定低下。只要有一個任務執行消耗了很長時間,在這個時間裡後面的任務無法執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。(
弊端
)
為了解決這個問題,javascript語言將任務執行模式分成同步和非同步:
同步模式:
就是上面所說的一種執行模式
,
後一個任務等待前一個任務結束,然後再執行,程式的執行順序與任務的排列順序是一致的、同步的。
非同步模式
:就是每一個任務有一個或多個回撥函式(callback),前一個任務結束後,不是執行後一個任務,而是執行回撥函式,後一個任務則是不等前一個任務結束就執行,所以程式的執行順序與任務的排列順序是不一致的、非同步的。
“非同步模式”非常重要。在瀏覽器端,耗時很長的操作都應該非同步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在伺服器端,”非同步模式”甚至是唯一的模式,因為執行環境是單執行緒的,如果允許同步執行所有http請求,伺服器效能會急劇下降,很快就會失去響應。(
非同步模式的重要性
)
下面就帶來幾種前端非同步解決方案:
一。傳統方案
1.回撥函式(callback):
非同步程式設計的基本方法。
首先需要宣告,回撥函式只是一種實現,並不是非同步模式特有的實現。回撥函式同樣可以運用到同步(阻塞)的場景下以及其他一些場景。
回撥函式的定義:
函式A作為引數(函式引用)傳遞到另一個函式B中,並且這個函式B執行函式A。我們就說函式A叫做回撥函式。如果沒有名稱(函式表示式),就叫做匿名回撥函式。
生活舉例:約會結束後你送你女朋友回家,離別時,你肯定會說:“到家了給我發條資訊,我很擔心你。” 然後你女朋友回家以後還真給你發了條資訊。其實這就是一個回撥的過程。你留了個引數函式(要求女朋友給你發條資訊)給你女朋友,然後你女朋友回家,回家的動作是主函式。她必須先回到家以後,主函式執行完了,再執行傳進去的函式,然後你就收到一條資訊了。
案例:
//定義主函式,回撥函式作為引數
function
A
(
callback
)
{
callback
();
console
。
log
(
‘我是主函式’
);
}
//定義回撥函式
function
B
(){
setTimeout
(
“console。log(‘我是回撥函式’)”
,
3000
);
//模仿耗時操作
}
//呼叫主函式,將函式B傳進去
A
(
B
);
//輸出結果
我是主函式
我是回撥函式
上面的程式碼中,我們先定義了主函式和回撥函式,然後再去呼叫主函式,將回調函式傳進去。
定義主函式的時候,我們讓程式碼先去執行callback()回撥函式,但輸出結果卻是後輸出回撥函式的內容。這就說明了主函式不用等待回撥函式執行完,可以接著執行自己的程式碼。所以一般回撥函式都用在耗時操作上面。比如ajax請求,比如處理檔案等。
優點:
簡單,容易理解和 部署。
缺點:
不利於程式碼的閱讀,和維護,各部分之間高度耦合,流程會很混亂,而且每一個任務只能指定一個回撥函式。
2.事件監聽
採用事件驅動模式。
任務的執行不取決程式碼的順序,而取決於某一個事件是否發生。
監聽函式有:on,bind,listen,addEventListener,observe
以f1和f2為例。首先,為f1繫結一個事件(採用jquery寫法)。
f1
。
on
(
‘done’
,
f2
);
上面程式碼意思是,當f1發生done事件,就執行f2。
然後對f1進行改寫:
function
f1
(){
settimeout
(
function
(){
//f1的任務程式碼
f1
。
trigger
(
‘done’
);
},
1000
);
}
f1。trigger(‘done’)表示,執行完成後,立即觸發done事件,從而開始執行f2。
優點:
比較容易理解,可以繫結多個事件,每一個事件可以指定多個回撥函式,而且可以去耦合,有利於實現模組化。
缺點:
整個程式都要變成事件驅動型,執行流程會變得不清晰。
事件鑑定方法:
(1).onclick方法:
element
。
onclick
=
function
(){
//處理函式
}
優點
:寫法相容到主流瀏覽器。
缺點
:當同一個
element元素
繫結多個事件時,只有最後一個事件會被新增。
例如:
element
。
onclick
=
handler1
;
element
。
onclick
=
handler2
;
element
。
onclick
=
handler3
;
上訴只有handler3會被新增執行,所以我們使用另外一種方法新增事件。(2)attachEvent和addEvenListener方法
(2).attachEvent和addEvenListener方法:
//IE:attachEvent(IE下的事件監聽)
elment
。
attachEvent
(
“onclick”
,
handler1
);
elment
。
attachEvent
(
“onclick”
,
handler2
);
elment
。
attachEvent
(
“onclick”
,
handler3
);
上述三個方法執行順序:3-2-1;
//標準addEventListener(標準下的監聽)
elment
。
addEvenListener
(
“click”
,
handler1
,
false
);
elment
。
addEvenListener
(
“click”
,
handler2
,
false
);
elment
。
addEvenListener
(
“click”
,
handler3
,
false
);
>
執行順序:1-2-3;
PS:該方法的第三個引數是冒泡獲取(useCapture),是一個布林值:當為false時表示由裡向外(事件冒泡),true表示由外向裡(事件捕獲)。
<
div
id
=
“id1”
>
<
div
id
=
“id2”
><
/div>
<
/div>
document
。
getElementById
(
“id1”
)。
addEventListener
(
“click”
,
function
(){
console
。
log
(
‘id1’
);
},
false
);
document
。
getElementById
(
“id2”
)。
addEventListener
(
“click”
,
function
({
console
。
log
(
‘id2’
);
},
false
);
//點選id=id2的div,先在console中輸出,先輸出id2,在輸出id1
document
。
getElementById
(
“id1”
)。
addEventListener
(
“click”
,
function
({
console
。
log
(
‘id1’
);
},
false
);
document
。
getElementById
(
“id2”
)。
addEventListener
(
“click”
,
function
({
console
。
log
(
‘id2’
);
},
true
);
//點選id=id2的div,先在console中輸出,先輸出id1,在輸出id2
(3).DOM方法addEventListener()和removeListenner():
addEventListenner()和removeListenner()表示用來分配和刪除事件的函式。這兩種方法都需要三種引數,分別為:string(事件名稱),要觸發事件的函式function,指定事件的處理函式的時期或者階段(boolean)。
例子見(2)
(4).通用的時間新增方法:
on
:
function
(
elment
,
type
,
handler
){
//新增事件
return
element
。
attachEvent
?
elment
。
attachEvent
(
“on”
+
type
,
handler
)
:
elment
。
addEventListener
(
type
,
handler
,
false
);
}
事件冒泡和事件捕獲的區別,可以參考:
二。工具方案
工具方案大致分為以下5個:
Promise
gengerator函式
async await
node。js中 nextTick setImmidate
第三方庫 async。js
下面針對每一個做詳細說明應用:
1.Promise(重點)
(1).Promise的含義和發展:
含義:Promise 物件用於一個非同步操作的最終完成(或失敗)及其結果值的表示。簡單點說,它就是用於處理非同步操作的,非同步處理成功了就執行成功的操作,非同步處理失敗了就捕獲錯誤或者停止後續操作。
發展:Promise 是非同步程式設計的一種解決方案,比傳統的解決方案–回撥函式和事件--更合理和更強大。它由社群最早提出和實現,ES6將其寫進了語言標準,統一了語法,原生提供了Promise
(2).它的一般形式:
new Promise(
/* executor */
function(resolve, reject) {
if (/* success */) {
// 。。。執行程式碼
resolve();
} else { /* fail */
// 。。。執行程式碼
reject();
}
}
);
其中,Promise中的引數executor是一個
執行器函式
,它有兩個引數
resolve
和
reject
。它內部通常有一些非同步操作,如果非同步操作成功,則可以呼叫resolve()來將該例項的狀態置為
fulfilled
,即已完成的,如果一旦失敗,可以呼叫reject()來將該例項的狀態置為
rejected
,即失敗的。
我們可以把Promise物件看成是一條工廠的流水線,對於流水線來說,從它的工作職能上看,它只有三種狀態,一個是初始狀態(剛開機的時候),一個是加工產品成功,一個是加工產品失敗(出現了某些故障)。同樣對於Promise物件來說,它也有三種狀態:
pending:
初始狀態,也稱為未定狀態,就是初始化Promise時,呼叫executor執行器函式後的狀態。
fulfilled:
完成狀態,意味著非同步操作成功。
pending:
初始狀態,也稱為未定狀態,就是初始化Promise時,呼叫executor執行器函式後的狀態。
fulfilled:
完成狀態,意味著非同步操作成功。
rejected:
失敗狀態,意味著非同步操作失敗。
它只有兩種狀態可以轉化,即
操作成功:
pending -> fulfilled
操作失敗:
pending -> rejected
注意:並且這個狀態轉化是單向的,不可逆轉,已經確定的狀態(fulfilled/rejected)無法轉回初始狀態(pending)。
(3).Promise物件的方法(api):
1):Promise.prototype.then(callback)
Promise物件含有
then方法
,then()呼叫後返回一個Promise物件,意味著例項化後的Promise物件可以進行鏈式呼叫,而且這個then()方法可以接收兩個函式,一個是處理成功後的函式,一個是處理錯誤結果的函式。
如下:
var
promise1
=
new
Promise
(
function
(
resolve
,
reject
)
{
// 2秒後置為接收狀態
setTimeout
(
function
()
{
resolve
(
‘success’
);
},
2000
);
});
promise1
。
then
(
function
(
data
)
{
console
。
log
(
data
);
// success
},
function
(
err
)
{
console
。
log
(
err
);
// 不執行
})。
then
(
function
(
data
)
{
// 上一步的then()方法沒有返回值
console
。
log
(
‘鏈式呼叫:’
+
data
);
// 鏈式呼叫:undefined
})。
then
(
function
(
data
)
{
// 。。。。
});
在這裡我們主要關注promise1。then()方法呼叫後返回的Promise物件的狀態,是pending還是fulfilled,或者是rejected?
返回的這個Promise物件的狀態主要是根據promise1。then()方法返回的值,大致分為以下幾種情況:
1。如果then()方法中返回了一個引數值,那麼返回的Promise將會變成接收狀態。
2。如果then()方法中丟擲了一個異常,那麼返回的Promise將會變成拒絕狀態。
3。 如果then()方法呼叫resolve()方法,那麼返回的Promise將會變成接收狀態。
4。 如果then()方法呼叫reject()方法,那麼返回的Promise將會變成拒絕狀態。
5。如果then()方法返回了一個未知狀態(pending)的Promise新例項,那麼返回的新Promise就是未知 狀態。
6。如果then()方法沒有明確指定的resolve(data)/reject(data)/return data時,那麼返回的新Promise就是接收狀態,可以一層一層地往下傳遞。
2):Promise.prototype.catch(callback)
catch()方法和then()方法一樣,都會返回一個新的Promise物件,它主要用於捕獲非同步操作時出現的異常。因此,我們通常省略then()方法的第二個引數,把錯誤處理控制權轉交給其後面的catch()函式,如下:
var
promise3
=
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
function
()
{
reject
(
‘reject’
);
},
2000
);
});
promise3
。
then
(
function
(
data
)
{
console
。
log
(
‘這裡是fulfilled狀態’
);
// 這裡不會觸發
// 。。。
})。
catch
(
function
(
err
)
{
// 最後的catch()方法可以捕獲在這一條Promise鏈上的異常
console
。
log
(
‘出錯:’
+
err
);
// 出錯:reject
});
3):Promise.all()
Promise。all()接收一個引數,它必須是可以迭代的,比如陣列。
它通常用來處理一些併發的非同步操作,即它們的結果互不干擾,但是又需要非同步執行。它最終只有兩種狀態:成功或者失敗。
指的是將陣列中所有的任務執行完成之後, 才執行。then 中的任務
它的狀態受引數內各個值的狀態影響,即裡面狀態全部為fulfilled時,它才會變成fulfilled,否則變成rejected。
成功呼叫後返回一個數組,陣列的值是有序的,即按照傳入引數的陣列的值操作後返回的結果。
如下:
const
p1
=
new
Promise
((
resolve
,
reject
)=>{
setTimeout
(()=>{
resolve
(
console
。
log
(
‘p1 任務1’
))
},
1000
)
})
。
then
(
data
=>
{
console
。
log
(
‘p1 任務2’
)
})
。
then
(
res
=>
{
console
。
log
(
‘p1 任務3’
)
})
。
catch
(
err
=>{
throw
err
}
)
const
p2
=
new
Promise
((
resolve
,
reject
)=>{
resolve
(
console
。
log
(
‘p2 任務1’
))
})。
then
(
data
=>
{
console
。
log
(
‘p2 任務2’
)
}
)。
catch
(
err
=>
{
throw
err
}
)
//只有在p1,p2都執行完後才會執行then裡的內容
Promise
。
all
([
p1
,
p2
])
。
then
(()=>
console
。
log
(
‘done’
))
4):Promise.race()
Promise。race()和Promise。all()類似,都接收一個可以迭代的引數,但是不同之處是Promise。race()的狀態變化不是全部受引數內的狀態影響,
一旦引數內有一個值的狀態發生的改變,那麼該Promise的狀態就是改變的狀態。就跟race單詞的字面意思一樣,誰跑的快誰贏
。如下:
var
p1
=
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
resolve
,
300
,
‘p1 doned’
);
});
var
p2
=
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
resolve
,
50
,
‘p2 doned’
);
});
var
p3
=
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
reject
,
100
,
‘p3 rejected’
);
});
Promise
。
race
([
p1
,
p2
,
p3
])。
then
(
function
(
data
)
{
// 顯然p2更快,所以狀態變成了fulfilled
// 如果p3更快,那麼狀態就會變成rejected
console
。
log
(
data
);
// p2 doned
})。
catch
(
function
(
err
)
{
console
。
log
(
err
);
// 不執行
});
5):Promise.resolve()
Promise。resolve()接受一個引數值,可以是普通的值,具有then()方法的物件和Promise例項。正常情況下,它返回一個Promise物件,狀態為fulfilled。但是,當解析時發生錯誤時,返回的Promise物件將會置為rejected態。如下:
// 引數為普通值
var
p4
=
Promise
。
resolve
(
5
);
p4
。
then
(
function
(
data
)
{
console
。
log
(
data
);
// 5
});
// 引數為含有then()方法的物件
var
obj
=
{
then
:
function
()
{
console
。
log
(
‘obj 裡面的then()方法’
);
}
};
var
p5
=
Promise
。
resolve
(
obj
);
p5
。
then
(
function
(
data
)
{
// 這裡的值時obj方法裡面返回的值
console
。
log
(
data
);
// obj 裡面的then()方法
});
// 引數為Promise例項
var
p6
=
Promise
。
resolve
(
7
);
var
p7
=
Promise
。
resolve
(
p6
);
p7
。
then
(
function
(
data
)
{
// 這裡的值時Promise例項返回的值
console
。
log
(
data
);
// 7
});
// 引數為Promise例項,但引數是rejected態
var
p8
=
Promise
。
reject
(
8
);
var
p9
=
Promise
。
resolve
(
p8
);
p9
。
then
(
function
(
data
)
{
// 這裡的值時Promise例項返回的值
console
。
log
(
‘fulfilled:’
+
data
);
// 不執行
})。
catch
(
function
(
err
)
{
console
。
log
(
‘rejected:’
+
err
);
// rejected: 8
});
6):Promise.reject()
Promise。reject()和Promise。resolve()正好相反,它接收一個引數值reason,即發生異常的原因。此時返回的Promise物件將會置為rejected態。如下:
var
p10
=
Promise
。
reject
(
‘手動拒絕’
);
p10
。
then
(
function
(
data
)
{
console
。
log
(
data
);
// 這裡不會執行,因為是rejected態
})。
catch
(
function
(
err
)
{
console
。
log
(
err
);
// 手動拒絕
})。
then
(
function
(
data
)
{
// 不受上一級影響
console
。
log
(
‘狀態:fulfilled’
);
// 狀態:fulfilled
});
總之,除非Promise。then()方法內部丟擲異常或者是明確置為rejected態,否則它返回的Promise的狀態都是fulfilled態,即完成態,並且它的狀態不受它的上一級的狀態的影響。
2.gengerator函式
在非同步程式設計中,還有一種常用的解決方案,它就是Generator
生成器函式
。顧名思義,它是一個生成器,它也是一個狀態機,內部擁有值及相關的狀態,生成器返回一個迭代器Iterator物件,我們可以透過這個迭代器,手動地遍歷相關的值、狀態,保證正確的執行順序。
es6 提供的
generator函式
總得來說就三點:
*在function關鍵字後加一個* , 那麼這個函式就稱之為generator函式
*函式體有關鍵字 yield , 後面跟每一個任務 , 也可以有return關鍵字, 保留一個數據
*透過
next函式
呼叫, 幾個呼叫, 就是幾個人任務執行
(1).簡單使用
Generator的宣告方式類似一般的函式宣告,只是多了個*號,並且一般可以在函式內看到yield關鍵字
function
*
showWords
()
{
yield
‘one’
;
yield
‘two’
;
return
‘three’
;
}
var
show
=
showWords
();
show
。
next
()
// {done: false, value: “one”}
show
。
next
()
// {done: false, value: “two”}
show
。
next
()
// {done: true, value: “three”}
show
。
next
()
// {value: underfined, done: true}
如上程式碼,定義了一個showWords的生成器函式,呼叫之後返回了一個迭代器物件(即show)
呼叫next方法後,函式內執行第一條yield語句,輸出當前的狀態done(迭代器是否遍歷完成)以及相應值(一般為yield關鍵字後面的運算結果)
每呼叫一次next,則執行一次yield語句,並在該處暫停,return完成之後,就退出了生成器函式,後續如果還有yield操作就不再執行了
當然還有以下情況:(next()數量小於yield)
function
*
g1
(){
yield
‘任務1’
yield
‘任務2’
yield
‘任務3’
return
‘任務4’
}
const
g1done
=
g1
()
console
。
log
(
g1done
。
next
())
//{ value: ‘任務1’, done: false }
console
。
log
(
g1done
。
next
())
//{ value: ‘任務2’, done: false }
(2).yield和yield*
有時候,我們會看到yield之後跟了一個*號,它是什麼,有什麼用呢?
類似於生成器前面的*號,yield後面的星號也跟生成器有關,舉個大栗子:
function
*
showWords
()
{
yield
‘one’
;
yield
showNumbers
();
return
‘three’
;
}
function
*
showNumbers
()
{
yield
10
+
1
;
yield
12
;
}
var
show
=
showWords
();
show
。
next
()
// {done: false, value: “one”}
show
。
next
()
// {done: false, value: showNumbers}
show
。
next
()
// {done: true, value: “three”}
show
。
next
()
// {done: true, value: undefined}
增添了一個生成器函式,我們想在showWords中呼叫一次,簡單的 yield showNumbers()之後發現並沒有執行函數里面的yield 10+1
因為yield只能原封不動地返回右邊運算後值,但現在的showNumbers()不是一般的函式呼叫,返回的是迭代器物件
所以換個yield* 讓它自動遍歷進該物件
function
*
showWords
()
{
yield
‘one’
;
yield
*
showNumbers
();
return
‘three’
;
}
function
*
showNumbers
()
{
yield
10
+
1
;
yield
12
;
}
var
show
=
showWords
();
show
。
next
()
// {done: false, value: “one”}
show
。
next
()
// {done: false, value: 11}
show
。
next
()
// {done: false, value: 12}
show
。
next
()
// {done: true, value: “three”}
要注意的是,這yield和yield* 只能在generator函式內部使用,一般的函式內使用會報錯
function
showWords
()
{
yield
‘one’
;
// Uncaught SyntaxError: Unexpected string
}
雖然換成yield*不會直接報錯,但使用的時候還是會有問題,因為’one‘字串中沒有Iterator介面,沒有yield提供遍歷
function
showWords
()
{
yield
*
’one‘
;
}
var
show
=
showWords
();
show
。
next
()
// Uncaught ReferenceError: yield is not defined
在爬蟲開發中,我們常常需要請求多個地址,為了保證順序,引入Promise物件和Generator生成器函式,看這個簡單的栗子:
var
urls
=
[
’url1‘
,
’url2‘
,
’url3‘
];
function
*
request
(
urls
)
{
urls
。
forEach
(
function
(
url
)
{
yield
req
(
url
);
});
// for (var i = 0, j = urls。length; i <; j; ++i) {
// yield req(urls[i]);
// }
}
var
r
=
request
(
urls
);
r
。
next
();
function
req
(
url
)
{
var
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
$
。
get
(
url
,
function
(
rs
)
{
resolve
(
rs
);
});
});
p
。
then
(
function
()
{
r
。
next
();
})。
catch
(
function
()
{
});
}
上述程式碼中forEach遍歷url陣列,匿名函式內部不能使用yield關鍵字,改換成註釋中的for迴圈就行了
(3).next()呼叫中的傳參
引數值有注入的功能,可改變上一個yield的返回值,如
function
*
showNumbers
()
{
var
one
=
yield
1
;
var
two
=
yield
2
*
one
;
yield
3
*
two
;
}
var
show
=
showNumbers
();
show
。
next
()。
value
// 1
show
。
next
()。
value
// NaN
show
。
next
(
2
)。
value
// 6
第一次呼叫next之後返回值one為1,但在第二次呼叫next的時候one其實是undefined的,因為generator不會自動儲存相應變數值,我們需要手動的指定,這時two值為NaN,在第三次呼叫next的時候執行到yield 3 * two,透過傳參將上次yield返回值two設為2,得到結果
另一個栗子:
由於ajax請求涉及到網路,不好處理,這裡用了setTimeout模擬ajax的請求返回,按順序進行,並傳遞每次返回的資料
var
urls
=
[
’url1‘
,
’url2‘
,
’url3‘
];
function
*
request
(
urls
)
{
var
data
;
for
(
var
i
=
0
,
j
=
urls
。
length
;
i
&
lt
;
j
;
++
i
)
{
data
=
yield
req
(
urls
[
i
],
data
);
}
}
var
r
=
request
(
urls
);
r
。
next
();
function
log
(
url
,
data
,
cb
)
{
setTimeout
(
function
()
{
cb
(
url
);
},
1000
);
}
function
req
(
url
,
data
)
{
var
p
=
new
Promise
(
function
(
resolve
,
reject
)
{
log
(
url
,
data
,
function
(
rs
)
{
if
(
!
rs
)
{
reject
();
}
else
{
resolve
(
rs
);
}
});
});
p
。
then
(
function
(
data
)
{
console
。
log
(
data
);
r
。
next
(
data
);
})。
catch
(
function
()
{
});
}
達到了按順序請求三個地址的效果,初始直接r。next()無引數,後續透過r。next(data)將data資料傳入
注意程式碼的第16行,這裡引數用了url變數,是為了和data資料做對比
因為初始next()沒有引數,若是直接將url換成data的話,就會因為promise物件的資料判斷 !rs == undefined 而reject
所以將第16行換成 cb(data || url);
透過模擬的ajax輸出,可瞭解到next的傳參值,第一次在log輸出的是 url = ’url1‘值,後續將data = ’url1‘傳入req請求,在log中輸出 data = ’url1‘值
(4).for...of迴圈代替.next()
除了使用。next()方法遍歷迭代器物件外,透過ES6提供的新迴圈方式for…of也可遍歷,但與next不同的是,它會忽略return返回的值,如
function
*
showNumbers
()
{
yield
1
;
yield
2
;
return
3
;
}
var
show
=
showNumbers
();
for
(
var
n
of
show
)
{
console
。
log
(
n
)
// 1 2
}
此外,處理for…of迴圈,具有呼叫迭代器介面的方法方式也可遍歷生成器函式,如擴充套件運算子…的使用
function
*
showNumbers
()
{
yield
1
;
yield
2
;
return
3
;
}
var
show
=
showNumbers
();
[。。。
show
]
// [1, 2, length: 2]
更多使用可以參考:MDN - Generator
3.async await (重點)
es7新增的
async函式
可以更舒適地與promise協同工作,它叫做async/await,它是非常的容易理解和使用。
(1)。格式
async
function
aa
(){
await
’任務1‘
await
’任務2‘
}
async:
讓我們先從
async關鍵字
說起,它被放置在一個函式前面。就像下面這樣:
async
function
timeout
()
{
return
’hello world‘
;
}
函式前面的async一詞意味著一個簡單的事情:這個函式總是返回一個promise,如果程式碼中有return <非promise>語句,JavaScript會自動把返回的這個value值包裝成promise的resolved值。
例如,上面的程式碼返回resolved值為1的promise,我們可以測試一下:
async
function
f
()
{
return
1
}
f
()。
then
(
alert
)
// 彈出1
我們也可以顯式的返回一個promise,這個將會是同樣的結果
async
function
f
()
{
return
Promise
。
resolve
(
1
)
}
f
()。
then
(
alert
)
// 彈出1
所以,async確保了函式返回一個promise,即使其中包含非promise,這樣都不需要你來書寫繁雜的Promise,夠簡單了吧?但是不僅僅只是如此,還有另一個關鍵詞
await
,只能在async函數里使用,同樣,它也很cool。
await:
// 只能在async函式內部使用
let
value
=
await
promise
關鍵詞await可以讓JavaScript進行等待,直到一個promise執行並返回它的結果,JavaScript才會繼續往下執行。
以下是一個promise在1s之後resolve的例子:
async
function
f
()
{
let
promise
=
new
Promise
((
resolve
,
reject
)
=>
{
setTimeout
(()
=>
resolve
(
’done!‘
),
1000
)
})
let
result
=
await
promise
// 直到promise返回一個resolve值(*)
alert
(
result
)
// ’done!‘
}
f
()
函式執行到(await)行會‘暫停’,不再往下執行,
當promise處理完成後重新恢復執行, resolve的值成了最終的result,所以上面的程式碼會在1s後輸出’done!‘
我們強調一下:await字面上使得JavaScript等待,直到promise處理完成,
然後將結果繼續下去。這並不會花費任何的cpu資源,因為引擎能夠同時做其他工作:執行其他指令碼,處理事件等等。
這只是一個更優雅的得到promise值的語句,它比promise更加容易閱讀和書寫。
注意不:能在常規函數里使用await
如果我們試圖在非async函數里使用await,就會出現一個語法錯誤:
function
f
()
{
let
promise
=
Promise
。
resolve
(
1
)
let
result
=
await
promise
// syntax error
}
//Uncaught SyntaxError: await is only valid in async function
如果我們忘記了在函式之前放置async,我們就會得到這樣一個錯誤。如上所述,await只能在async函式中工作。
就以前面幾個案例可能還看不出async/await 的作用,如果我們要計算3個數的值,然後把得到的值進行輸出呢?
async
function
testResult
()
{
let
first
=
await
doubleAfter2seconds
(
30
);
let
second
=
await
doubleAfter2seconds
(
50
);
let
third
=
await
doubleAfter2seconds
(
30
);
console
。
log
(
first
+
second
+
third
);
}
6秒後,控制檯輸出220, 我們可以看到,寫非同步程式碼就像寫同步程式碼一樣了,再也沒有回撥地域了。
再來一個看看:先來個問題
readFile(’。/01-Promise。js‘) 執行結果是Promise, 但是我們使用 async await之後, 它的結果是具體的資料了?
用到了Node。js裡的fs模組,fs模組是檔案模組,可以操作檔案,readFile()是讀一個檔案,不瞭解的樂意看Node。js官方文件
const
fs
=
require
(
’fs‘
)
//匯入fs模組
const
readFile
=
(
filename
)
=>{
return
new
Promise
((
resolve
,
reject
)=>{
fs
。
readFile
(
filename
,(
err
,
data
)=>{
resolve
(
data
。
toString
())
})
})
}
const
asyncFn
=
async
()
=>
{
//const f0 = eadFile(’。/01-Promise。js‘) //類似{value: ’檔案內容‘, done: false}
const
f1
=
await
readFile
(
’。/01-Promise。js‘
)
//檔案內容
//const f1 = readFile(’。/01-Promise。js‘)。then(data=>data)
const
f2
=
await
readFile
(
’。/02-generator。js‘
)
//檔案內容
console
。
log
(
f1
)
console
。
log
(
f2
)
}
asyncFn
()
readFile()定義了一個Promise方法讀取檔案,這裡有個坑,我們現在是在裡面返回出資料了的,要知道這裡面有3層函式,如果不用new Promise這個方法,大家可以試試用常規方法能不能返回資料,先透個底拿不到,大家可以試試。
asyncFn()輸出了檔案內容,在
const f1 = eadFile('./01-Promise.js')
這一句這一句會打印出出一個Promise{’檔案內容‘},有點類似前面的generator函式輸出的{value: ’‘, done: false},只不過省略了done,大家知道,我們讀檔案,肯定是要裡面的內容的,如果輸出 Promise{’檔案內容‘} ,我們是不好取出內容的,但是await很好的幫我們解決了這個問題,前面加上await直接輸出了檔案內容。
所以:這個問題可以有個小總結
1。async函式使用了generator函式的語法糖 , 它直接生成物件 {value: ’‘,done:false} await 直接將value提取出來了
2。 透過Promise + async,我們可以把多層函式巢狀(非同步執行)的裡層函式得到的資料 返回出來
關於async/await總結
放在一個函式前的
async
有兩個作用:
使函式總是返回一個promise
允許在這其中使用await
promise前面的
await
關鍵字能夠使JavaScript等待,直到promise處理結束。然後:
如果它是一個錯誤,異常就產生了,就像在那個地方呼叫了throw error一樣。
否則,它會返回一個結果,我們可以將它分配給一個值
他們一起提供了一個很好的框架來編寫易於讀寫的非同步程式碼。
有了async/await,我們很少需要寫promise。then/catch,但是我們仍然不應該忘記它們是基於promise的,因為有些時候(例如在最外面的範圍內)我們不得不使用這些方法。Promise。all也是一個非常棒的東西,它能夠同時等待很多工。
4.node.js nextTick setImmidate
nextTick vs setImmediate
輪詢:
nodejs中是事件驅動的,有一個迴圈執行緒一直從
事件佇列
中取任務執行或者
I/O的操作轉給後臺執行緒池來操作,把這個迴圈執行緒的每次執行的過程算是一次輪詢。
2。setImmediate()的使用
即時計時器立即執行工作,它是在事件輪詢之後執行,為了防止輪詢阻塞,每次只會呼叫一個。
3。Process。nextTick()的使用
它和setImmediate()執行的順序不一樣,它是在事件輪詢之前執行,為了防止I/O飢餓,所以有一個預設process。maxTickDepth=1000來限制事件佇列的每次迴圈可執行的nextTick()事件的數目。
總結:
nextTick()的回撥函式執行的優先順序要高於setImmediate();
process。nextTick()屬於idle觀察者,setImmediate()屬於check觀察者。在每一輪迴圈檢查中,idle觀察者先於I/O觀察者,I/O觀察者先於check觀察者。
在具體實現上,process。nextTick()的回撥函式儲存在一個數組中,
setImmediate()的結果則是儲存在連結串列中。
在行為上,process。nextTick()在每輪迴圈中會將陣列中的回撥函式全部執行完。
而setImmediate()在每輪迴圈中執行連結串列中的一個回撥函式。
5.第三方庫 async.js
async.js
是一個第三方庫,帶有很多api
暴露了一個async物件,這個物件身上有很多的api(多工執行),例如parallel,series
async
。
parallel
(
&
#
91
;
function
(
callback
){
callback
(
null
,
’任務1‘
)
},
function
(
callback
){
callback
(
null
,
’任務2‘
)
},
],(
err
,
data
)=>{
console
。
log
(
’data‘
,
data
)
})