Javascript學(xué)習(xí)筆記-異步和回調(diào)

Javascript異步和回調(diào).png

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ù)f1f2執(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(中篇)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容