
1. 異步
Javascript中程序是分塊執(zhí)行的,塊的最常見單位是函數(shù),在Javascript引擎執(zhí)行的時(shí)候,通常最少存在一個(gè)現(xiàn)在正在執(zhí)行的塊和一個(gè)將要執(zhí)行的塊,對(duì)于異步的分塊執(zhí)行,最簡(jiǎn)單的方式就是回調(diào)。
function f1() {
console.log(1);
}
function f2() {
console.log(2);
}
// 當(dāng)開始執(zhí)行f1的時(shí)候,f1就是當(dāng)前執(zhí)行塊,setTimeout(f1,0)和f2()就是將來要執(zhí)行的塊
f1();
// 使用了setTimeout并將f1作為了回調(diào)函數(shù),創(chuàng)建了一個(gè)異步的塊
setTimeout(f1, 0);
f2();
1.1 事件循環(huán)
事件循環(huán)是Javascript引擎用于處理多塊程序執(zhí)行的機(jī)制。主要實(shí)現(xiàn)是在Javascript中存在一個(gè)事件循環(huán)隊(duì)列,然后會(huì)將需要運(yùn)行的塊加入到該隊(duì)列中,之后等待觸發(fā)運(yùn)行,同時(shí)存在一個(gè)無限循環(huán)監(jiān)聽該隊(duì)列的變化,并觸發(fā)方法執(zhí)行。
// 先進(jìn)先出的隊(duì)列
var eventLoop = [];
var event;
while(true) {
if (eventLoop.length > 0) {
// 獲取隊(duì)列中的某個(gè)事件
event = eventLoop.shift();
// 事件執(zhí)行
try {
event();
} catch(e) {
doError(e);
}
}
}
1.2 并行
并行是時(shí)間點(diǎn)的概念,是指某個(gè)時(shí)間點(diǎn)多個(gè)事情同時(shí)執(zhí)行,通常多線程才存在并行的能力,事情的運(yùn)行結(jié)果存在不確定性,即兩個(gè)或者多個(gè)線程同時(shí)操作同一份數(shù)據(jù),那么數(shù)據(jù)結(jié)果就會(huì)不確定。
var a = 1;
function f1() {
a = a + 1;
}
function f2() {
a = a + 2;
}
// ajax是某個(gè)異步函數(shù)
ajax(f1);
ajax(f2);
// 如果f1和f2是兩個(gè)線程同時(shí)并行
線程1(X和Y是臨時(shí)內(nèi)存地址)
f1:
a. 把a(bǔ)的值保存在X
b. 把1的值保存在Y
c. 執(zhí)行X+Y
d. 把結(jié)果保存到a
線程2(X和Y是臨時(shí)內(nèi)存地址)
f1:
a. 把a(bǔ)的值保存在X
b. 把2的值保存在Y
c. 執(zhí)行X+Y
d. 把結(jié)果保存到a
/* 由于多線程并行,所以線程1中和線程2中的步驟是可以按照任意方式組合
于是對(duì)于操作同一份數(shù)據(jù)a就存在了不確定性
*/
// 例如順序1:
1a -> 1b -> 1c-> 1d -> 2a -> 2b -> 2c -> 2d 那么最后a的結(jié)果是4
// 如果假設(shè)順序2:
1a -> 2a -> 1b -> 1c -> 1d -> 2b -> 2c -> 2d 那么最后a的結(jié)果就是3
// 當(dāng)然還有其他很多順序組合
由于Javascript是單線程的,所以在函數(shù)運(yùn)行的時(shí)候具有原子性和完整性,也就是說在回調(diào)函數(shù)f1和f2執(zhí)行的時(shí)候,如果f1開始執(zhí)行,那么在f1執(zhí)行完成之前,f2不會(huì)進(jìn)行執(zhí)行,所以不存在多線程導(dǎo)致的不確定性。
但是Javascript同樣存在不確定性,其不確定性來自異步的回調(diào)函數(shù)執(zhí)行時(shí)間,這種不確定性也稱為競(jìng)態(tài)條件,也就是說對(duì)于上面的例子,這里只存在先執(zhí)行f1,還是先執(zhí)行f2導(dǎo)致的結(jié)果不確定性。
1.3 并發(fā)
并發(fā)是時(shí)間間隔的概念,是指某個(gè)時(shí)間間隔內(nèi)可以處理多件事情的能力。
知乎上有一個(gè)很通俗的例子關(guān)于并行和并發(fā)的區(qū)別:
非并發(fā):吃飯的時(shí)候接到電話,需要先把飯吃完,才能接電話
并發(fā):吃飯的時(shí)候接到電話,中斷吃飯接電話,接完電話吃飯
并行:吃飯的時(shí)候接到電話,一邊吃飯一邊接電話(快速切換上下文,其實(shí)并不能算是很嚴(yán)格的并行,看似吃飯和打電話同時(shí)進(jìn)行)
并發(fā)有三種常見的情況
1.3.1 非交互
兩個(gè)運(yùn)行函數(shù)之間沒有任何關(guān)聯(lián),獨(dú)自運(yùn)行不會(huì)對(duì)結(jié)果產(chǎn)生影響
var a,b;
function f1() {
a = 1;
}
function f2() {
b = 1;
}
ajax(f1)
ajax(f2)
1.3.2 交互
兩個(gè)運(yùn)行函數(shù)之間存在關(guān)聯(lián),運(yùn)行順序?qū)Y(jié)果會(huì)產(chǎn)生影響
// 由于回調(diào)的不確定性,所以最后a的值可能是1,可能是2
var a ;
function f1() {
a = 1;
}
function f2() {
a = 2;
}
ajax(f1);
ajax(f2);
通常為了控制結(jié)果,會(huì)設(shè)置競(jìng)態(tài)條件
var a,b ;
function f1() {
a = 1;
foo(); // 如果直接輸出,可能此時(shí)b未被賦值,所以會(huì)返回NaN
}
function f2() {
b = 2;
foo(); // 如果直接輸出,可能此時(shí)a未被賦值,所以會(huì)返回NaN
}
function foo() {
console.log(a + b);
}
ajax(f1);
ajax(f2);
// 使用競(jìng)態(tài)條件確保輸出結(jié)果
var a,b ;
function f1() {
a = 1;
// 添加競(jìng)態(tài)條件
if(a&&b) {
foo();
}
}
function f2() {
b = 2;
// 添加競(jìng)態(tài)條件
if(a&&b) {
foo();
}
}
function foo() {
console.log(a + b);
}
ajax(f1);
ajax(f2);
1.3.3 協(xié)作
Javascript的單線程操作具有原子性,那么當(dāng)某個(gè)方法執(zhí)行可能會(huì)持續(xù)占用引擎,于是我們通常會(huì)考慮執(zhí)行一部分以后釋放資源,使事件循環(huán)隊(duì)列中的其他內(nèi)容可以先執(zhí)行,不斷切換執(zhí)行上下文。利用setTimeout函數(shù),可以讓我們實(shí)現(xiàn)這樣的效果
function res(datas) {
for(let i = 0; i< datas.length; i++) {
if (i === 1000) {
// 釋放當(dāng)前資源,嘗試將回調(diào)重新加入事件循環(huán)隊(duì)列尾部
setTimeout(_ => {res(datas.slice(0,1000))} , 0);
break;
}
}
}
1.4 任務(wù)隊(duì)列
任務(wù)隊(duì)列是建立在事件循環(huán)隊(duì)列基礎(chǔ)上,區(qū)別在于事件循環(huán)隊(duì)列每次只能將事件添加到隊(duì)列的尾部;任務(wù)隊(duì)列則是在一個(gè)事件循環(huán)隊(duì)列觸發(fā)下一次tick前執(zhí)行,可以不斷在插入內(nèi)容,從而使得事件循環(huán)的下一次tick延遲。
2. 回調(diào)
2.1 回調(diào)的執(zhí)行
Javascript中實(shí)現(xiàn)異步分塊執(zhí)行的最簡(jiǎn)單的方式就是回調(diào)。當(dāng)異步操作結(jié)束的時(shí)候,回調(diào)函數(shù)會(huì)被放到事件循環(huán)隊(duì)列中,注意,這里不是異步操作結(jié)束的時(shí)候執(zhí)行回調(diào)函數(shù),而是將其放到事件循環(huán)隊(duì)列中等待執(zhí)行。
也就是說,當(dāng)異步結(jié)束的時(shí)候,回調(diào)函數(shù)并非是立即執(zhí)行,而是根據(jù)Javascript事件循環(huán)機(jī)制來進(jìn)行執(zhí)行,其具體運(yùn)行時(shí)間不可預(yù)知。
function f1() {
console.log(1);
}
// 這里設(shè)置1000ms是指在1000ms后將f1方法放到事件隊(duì)列中,并不是1000ms后就立即執(zhí)行回調(diào)方法
setTimeout(f1, 1000);
2.2 缺陷
回調(diào)很簡(jiǎn)單,但是回調(diào)在處理Javascript操作的時(shí)候存在一些缺陷
2.2.1 回調(diào)地獄
由于異步的不確定性,當(dāng)我們需要依次使用回調(diào)結(jié)果的時(shí)候,不可避免的就必須要使用嵌套的方式
ajax(url1, function(url2){
ajax(url2, function(url3)){
ajax(url3, function(data){
// 做某些內(nèi)容
})
})
});
這種嵌套一方面帶來的問題是代碼上閱讀的困難,另一方面沒辦法對(duì)某些操作進(jìn)行統(tǒng)一的處理,例如:異常處理,日志操作等
2.2.2 信任問題
在使用第三方異步方法的時(shí)候,由于只能進(jìn)行回調(diào)函數(shù)的傳遞,那么我們不能確保第三方異步方法如何對(duì)回調(diào)進(jìn)行調(diào)用,可能存在多次調(diào)用回調(diào)的情況,從而導(dǎo)致結(jié)果和預(yù)期不相符
function f() {
console.log(1);
}
ajaxF(f);
// 第三方提供的ajaxF,可能我們并不知道第三方調(diào)用回嘗試重連
// 所以可能導(dǎo)致我們的回調(diào)調(diào)用多次,當(dāng)然這個(gè)問題可以通過溝通解決
var i = 0;
function ajaxF(fn) {
ajax(data=>{
// 失敗時(shí)且請(qǐng)求次數(shù)小于3嘗試重新請(qǐng)求
if (data.fail && i<3){
i++;
// 多次調(diào)用回調(diào)
fn(data.fail);
ajaxF(fn);
} else {
fn(data.success);
}
})
}
2.3 優(yōu)化
回調(diào)自身存在一些缺陷,我們通過一些處理手段可以提升回調(diào)的可讀性,但是這種優(yōu)化并不能從根本上解決目前回調(diào)帶來的困境,我們需要使用新的異步方案來替代回調(diào),當(dāng)然回調(diào)仍然是最簡(jiǎn)單的Javascript異步處理方法
優(yōu)化一:將異常和成功回調(diào)分離
function fail() {
console.log('fail')
}
function succ() {
console.log('succ');
}
ajax(succ, fail);
優(yōu)化二:first-error風(fēng)格回調(diào)
function foo(err, data) {
if(err) {
// 處理錯(cuò)誤邏輯
} else {
// 處理正常邏輯
}
}
ajax(foo);
3. 小結(jié)
Javascript異步最基礎(chǔ)的內(nèi)容是事件循環(huán),所以最基本的是了解事件循環(huán)機(jī)制,在了解的基礎(chǔ)上再去處理所有和異步相關(guān)的問題都會(huì)變的簡(jiǎn)單。
回調(diào)是最簡(jiǎn)單的Javascript異步解決方案,但是其自身存在一些不方便使用的地方,在較復(fù)雜的時(shí)候,我們需要更好的異步方案來替代。
4. 參考
《你不知道的Javascript(中篇)》