淺聊異步--回調(diào)函數(shù)

為什么會(huì)想寫一個(gè)關(guān)于JS的異步的一個(gè)系列呢?
在不斷的學(xué)習(xí)之中,越發(fā)的讓我覺(jué)得,異步以及原型,可以說(shuō)是JS最為重要的兩個(gè)部分,或者說(shuō)是,JS最與其他的編程語(yǔ)言不一樣的特性(當(dāng)然JS還有很多其他的特性,雖然說(shuō)很多特性本質(zhì)是都是設(shè)計(jì)缺陷。。。)
這兩個(gè)特性也導(dǎo)致了JS很多令人難以理解的部分,比如原型鏈中一直在扯的繼承。。。
很多人都會(huì)說(shuō)js是一個(gè)十多天設(shè)計(jì)出來(lái)的語(yǔ)言,很多東西都沒(méi)有考慮到,所以js中存在很多難以理解的東西也就不足為奇了(或者說(shuō)js這么爛是很有理由的),可是經(jīng)過(guò)這么久的發(fā)展,其實(shí)js現(xiàn)在也在彌補(bǔ)一些以前的錯(cuò)誤,特別是在ES6發(fā)展以后,很多缺陷都已經(jīng)被補(bǔ)足,比如塊作用域,我們很多時(shí)候已經(jīng)不需要再去使用那些難以理解的東西了
可是,因?yàn)镴S這門語(yǔ)言的特殊性質(zhì)(或者說(shuō)瀏覽器的特殊性質(zhì)),很多東西其實(shí)是在做增量,比如class,本質(zhì)也就是一個(gè)語(yǔ)法糖而已,如果我們不去理解一些底層的東西,不去理解那11天里為什么會(huì)做出這樣的設(shè)計(jì),那么我們依舊會(huì)很難駕馭JS,即使現(xiàn)在的異步已經(jīng)有了Promise以及async這樣的優(yōu)秀的解決方案,我們依舊可以看到很多這樣的代碼

const getPromise1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, 1000);
  })
}

const getPromise2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, 1000);
  })
}

const test = () => {
  getPromise1().then((res) => {
    console.log(res);
    getPromise2().then(res2 => {
      console.log(res2);
    })
  })
}

依舊是是回調(diào)地獄的寫法,并不知道為什么非要使用Promise
或者這樣

const test2 = async () => {
  await getPromise1().then(res => {
    console.log(res);
  })
}

這段代碼當(dāng)時(shí)看的我很驚訝。。。。

或者是

const sleep = () => {
  setTimeout(() => {
    console.log(1)
  }, 1000);
}
const test3 = async () => {
  console.log(0)
  await sleep()
  console.log(2)
}
test3()

期望輸出0 1 2 且認(rèn)為1會(huì)在0輸出后1000ms后才進(jìn)行輸出的

ES6還可以稱為ES2015,也就是說(shuō)promise正式成為標(biāo)準(zhǔn)到我寫這篇文章的時(shí)候已經(jīng)過(guò)去了4年多了,而被稱為異步的終極解決方案的async跟awiat也是在es2017中就被加入了進(jìn)來(lái),可是依舊有蠻蠻多的學(xué)習(xí)者對(duì)他們的使用方式是錯(cuò)誤的,這里面不乏已經(jīng)在工作了的人,這也是我為什么突然想寫這樣一個(gè)系列的文章的原因。
不過(guò)這篇文章對(duì)于那些已經(jīng)能夠很好的處理異步流程的同學(xué)來(lái)說(shuō)可能沒(méi)有什么幫助了,畢竟語(yǔ)言是在不斷的發(fā)展的,如果只是要用得話,其實(shí)在條件允許的情況下,async跟awiat,以及promise,已經(jīng)基本上成為了毫無(wú)爭(zhēng)議的選擇了,特別是async跟awiat,用起來(lái)真的很簡(jiǎn)單,而promise,我們更多的是使用它來(lái)生成可以給async使用的函數(shù),或者用來(lái)做性能優(yōu)化。
而這個(gè)系列的文章,主要是在探尋,為什么JavaScript的異步會(huì)發(fā)展成現(xiàn)在這樣子,以及那些藏在現(xiàn)代異步手段下的東西,這樣我們才不至于迷迷糊糊的犯下錯(cuò)誤。

在講解異步的處理的之前,我們需要先問(wèn)自己一個(gè)問(wèn)題

什么是異步?

一個(gè)比較通俗易懂的解釋是
當(dāng)JS引擎執(zhí)行到同步代碼的時(shí)候,同步代碼會(huì)立即執(zhí)行
當(dāng)JS引擎執(zhí)行到異步代碼的時(shí)候,異步代碼不會(huì)立即執(zhí)行

比如

let a=1
const b=()=>console.log(a)
const run=(fnc)=>fnc()
run(b)

當(dāng)JS引擎執(zhí)行這句代碼的時(shí)候,會(huì)立即執(zhí)行里面的代碼,立刻打印出來(lái)1
而對(duì)于如下的異步代碼

setTimeout(()=>{
    console.log(1)
},1000)
console.log(1)

這句代碼并不會(huì)立即執(zhí)行,而是等待1000ms后才執(zhí)行
并且

在同步代碼后面的代碼必須等到同步代碼執(zhí)行完畢以后才會(huì)執(zhí)行,而異步代碼后面的代碼則不會(huì)

常見(jiàn)的異步情況有

  1. 定時(shí)器
  2. 網(wǎng)絡(luò)請(qǐng)求
  3. 事件

寫在這些情況里面的回調(diào)函數(shù)代碼,都不會(huì)立即執(zhí)行

接下來(lái)我們來(lái)看下異步處理的最基本的方式,回調(diào)函數(shù)

在看回調(diào)函數(shù)之前
我們要先有的概念
1、JS在一開(kāi)始的時(shí)候開(kāi)發(fā)目的是運(yùn)行在瀏覽器上面的
2、JS是單線程的(不應(yīng)該說(shuō)js是單線程的,這里所屬的js是單線程的其實(shí)是說(shuō)的是js引擎是單線程的)
3、JS支持函數(shù)式,或者說(shuō)函數(shù)在JS中是一等公民,可以進(jìn)行傳遞

這幾點(diǎn)使得js與我們平時(shí)的時(shí)候接觸的一些語(yǔ)言會(huì)有所不同,比如在java中不管我們做什么都要先聲明一個(gè)類,而且函數(shù)也不能作為值進(jìn)行傳遞
而在js中這三個(gè)內(nèi)容是相輔相成的,因?yàn)閖s是運(yùn)行在瀏覽器里面的,所以js必須是單線程的,所以js也就必須要使用異步的方式,因此必須使用回調(diào)函數(shù)
為什么呢,因?yàn)閖s創(chuàng)建出來(lái)的一個(gè)目的就是為了操作DOM,如果js引擎允許多線程得話,那么有可能出現(xiàn)線程a在獲取某個(gè)元素的高度后,使用此高度進(jìn)行計(jì)算,而在這兩個(gè)步驟之間,線程b修改該元素的高度,因此計(jì)算結(jié)果將會(huì)不正確,也就無(wú)法進(jìn)行正確的渲染,即使是server work這些加入多線程機(jī)制的內(nèi)容里面,也是不允許對(duì)dom進(jìn)行操作的,對(duì)dom的操作永遠(yuǎn)會(huì)是單線程,因此js也就是必須采用回調(diào)函數(shù)的方來(lái)對(duì)異步進(jìn)行處理(這里可能存在一些誤差,因?yàn)槲掖_實(shí)不是很了解太多的語(yǔ)言),為什么這樣說(shuō)呢?假如是初學(xué)者,可能對(duì)異步以及回調(diào)函數(shù)沒(méi)有太多的理解,所以接下來(lái)我們來(lái)聊聊異步。
我相信大部分的人學(xué)習(xí)js的時(shí)候應(yīng)該很早的時(shí)候就接觸到異步或者說(shuō)回調(diào)函數(shù)了。比如settimeout,比如事件綁定,當(dāng)然這個(gè)時(shí)候,很多人可能還是迷迷糊糊的,只知道我在這里(settimeout的參數(shù))傳入一個(gè)函數(shù),那么過(guò)了一段時(shí)間以后,就會(huì)執(zhí)行這段代碼,我給dom事件綁定一個(gè)函數(shù),那么當(dāng)我點(diǎn)擊按鈕的時(shí)候(或者其他)的時(shí)候這個(gè)函數(shù)就會(huì)執(zhí)行。
而當(dāng)我們開(kāi)始對(duì)異步以及回調(diào)函數(shù)產(chǎn)生疑惑的時(shí)候,我覺(jué)得可能大部分的人都是在第一次使用ajax或者說(shuō)請(qǐng)求數(shù)據(jù)的時(shí)候,我們?cè)趺床拍塬@取到數(shù)據(jù)呢?
是這樣嗎?

let res=request.get('http://localhost:8888/',{})
console.log(res);
xxxx

這個(gè)其實(shí)真的蠻形象的,因?yàn)樗芊衔覀兊恼J(rèn)知,那就是我請(qǐng)求別人給我東西,別人給了我東西,然后我就可以用這個(gè)東西了,并且在python之類的代碼中,這樣寫是完全沒(méi)有問(wèn)題的,可是這是JS。

在前面的論述中,我們已經(jīng)根據(jù)1得出了2,JS必須是單線程的,那么我們來(lái)看下,假如JS允許這樣的情況發(fā)生,那么會(huì)是怎么樣的呢?
單線程的意思是在一段時(shí)間內(nèi),只能做一個(gè)事情,那么假如以上的的做法可以達(dá)到獲取數(shù)據(jù)的辦法,那么就會(huì)發(fā)生這樣的事情,在我們點(diǎn)擊一個(gè)按鈕發(fā)送一個(gè)請(qǐng)求之后,這個(gè)時(shí)候請(qǐng)求正在返回,他很慢,很慢,我們等得無(wú)聊了,這個(gè)時(shí)候我想點(diǎn)一下旁邊的按鈕,剛剛我點(diǎn)了,他會(huì)給我一個(gè)反饋比如發(fā)射一發(fā)煙火,可是現(xiàn)在不管我怎么點(diǎn)都沒(méi)有用,因?yàn)镴S是單線程的,他現(xiàn)在在做其他的事情--等待。我們可以認(rèn)為有一個(gè)人,這個(gè)人一次只能做一個(gè)事情,他剛剛送了一封信,為了獲取回信,他現(xiàn)在在一直等著,等著,他沒(méi)有辦法來(lái)做,其他的事情,哈你說(shuō)為什么他不能等下再去看看信有沒(méi)有回復(fù),現(xiàn)在怎么不去做其他的事情偏偏要等著?拜托,看下你寫的代碼,假如他不等拿到內(nèi)容以后再去做其他的事情,他怎么能做其他的時(shí)候,或者說(shuō),他怎么知道xxxx中,那部分的內(nèi)容是需要信里面的內(nèi)容才能做得呢,哪些是不需要信里面的內(nèi)容才能去做的?此外,假如他現(xiàn)在不等了,那么他什么時(shí)候才會(huì)知道信會(huì)到呢?
這種方案在python可信,是因?yàn)閜ython有多線程啊,他完全可以讓另外一個(gè)人去做這個(gè)事情,而可憐的js
只有一個(gè)人,那么我們肯定就要思考一下,有沒(méi)有其他的辦法來(lái)處理這種情況,畢竟傻傻的等著,實(shí)在是太過(guò)于愚蠢了。

那么應(yīng)該怎么進(jìn)行設(shè)計(jì)呢?
其實(shí)在前面的時(shí)候我們已經(jīng)說(shuō)過(guò)了怎么進(jìn)行設(shè)計(jì)了,在等待的時(shí)間去做其他的事情了。那么我們可以這樣做

  1. 發(fā)出信件(不管怎么說(shuō)發(fā)信這個(gè)操作總是在我們當(dāng)前的流程里面的)
  2. 提前安排好信到了以后要做什么(所以我們需要寫一段代碼,對(duì)這段代碼進(jìn)行特殊的處理,以跟我們正常的流程進(jìn)行區(qū)分,為什么說(shuō)不是等到信件到來(lái)再進(jìn)行處理,這個(gè)我覺(jué)得你可以看一下自己的代碼就明白為什么我要這樣沒(méi)說(shuō)了,代碼畢竟不是人)
  3. 繼續(xù)做接下來(lái)安排好的事情
  4. 當(dāng)把當(dāng)前做完的事情做完后去看看是否有回信,如果有回信得話就開(kāi)始做前面安排好的事情

這里安排好的事情就是回調(diào)函數(shù)
當(dāng)然這里的描寫其實(shí)還是有部分的偏差,不過(guò)我們已經(jīng)知道了我們大致上要的效果,而實(shí)際上,JS的這套機(jī)制被稱為事件輪詢(event loop)

那么回歸正題,瀏覽器對(duì)異步的處理機(jī)制到底是什么樣子的呢?
首先我們要了解的是,js是單線程的,可是瀏覽器并不是單線程的,在這里我們說(shuō)的js也可以說(shuō)是js引擎。
不然下面的話說(shuō)起來(lái)可能就會(huì)有一些歧義了。

在現(xiàn)代瀏覽器中一般是多進(jìn)程的。主要有

  1. 渲染進(jìn)程
  2. 網(wǎng)絡(luò)進(jìn)程
  3. 瀏覽器進(jìn)程
  4. 插件進(jìn)程
    而js引擎線程程是屬于渲染進(jìn)程的一部分

我們可以認(rèn)為JS單線程是說(shuō)JS引擎是單線程的。而瀏覽器則是可以負(fù)責(zé)多個(gè)人去做事情的那個(gè)人
因?yàn)槿绻^測(cè)我們前面做的設(shè)計(jì)以及JS是單線程的,很容易就讓人想到一些問(wèn)題,那就是
1、假如沒(méi)有信得話怎么辦?
2、假如在我做事情的時(shí)候來(lái)了多封信,我應(yīng)該先做那封信里面的事情,這里我們可以想到給信進(jìn)行排順序,那么問(wèn)題就來(lái)了,誰(shuí)來(lái)負(fù)責(zé)這個(gè)信的排序?
這個(gè)人就是瀏覽器啦

對(duì)于渲染進(jìn)程至少有以下幾個(gè)線程
1、GUI渲染線程
2、JS引擎線程
3、事件觸發(fā)線程
4、定時(shí)器觸發(fā)線程
5、異步http請(qǐng)求線程

而后面這三個(gè),我們就可以很明顯的發(fā)現(xiàn),他們都是可以產(chǎn)生異步的操作的線程
那么我們來(lái)走一下JS執(zhí)行的流程

1、先從整個(gè)JS腳本添加到任務(wù)隊(duì)列中開(kāi)始順序執(zhí)行、此時(shí)是跟GUI渲染線程是互斥的,會(huì)阻塞GUI的繪制
2、如果遇到了3、4、5這些情況,則使用異步線程,例如在當(dāng)JS引擎線程執(zhí)行到setTimeOut\setTimeInterval 關(guān)鍵詞時(shí),會(huì)把定時(shí)器任務(wù)添加到定時(shí)觸發(fā)器線程中,定時(shí)觸發(fā)器線程開(kāi)始執(zhí)行倒數(shù),當(dāng)?shù)箶?shù)之間到了后,將回調(diào)任務(wù)添加到task queue任務(wù)隊(duì)列中,等待JS引擎線程來(lái)執(zhí)行
3、當(dāng)當(dāng)前任務(wù)隊(duì)列中的內(nèi)容執(zhí)行完畢后,檢查異步任務(wù)隊(duì)列中是否存在任務(wù),如果有,則依次執(zhí)行異步任務(wù)隊(duì)列中的內(nèi)容
4、依次類推

這里又有了一個(gè)新的概念為事件輪詢,在看這個(gè)概念之前我們可以先看一段代碼

這段代碼來(lái)自<<你不知道的js>>

// eventLoop是一個(gè)消息隊(duì)列
// (先進(jìn),先出)
var eventLoop = [ ];
var event;

// “永遠(yuǎn)”執(zhí)行
while (true) {
    // 一次tick
    if (eventLoop.length > 0) {
        // 拿到隊(duì)列中的下一個(gè)事件
        event = eventLoop.shift();

        // 現(xiàn)在,執(zhí)行下一個(gè)事件
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

eventLoop這個(gè)數(shù)組里面有什么?我們可以認(rèn)為有一些函數(shù),而這些函數(shù)里面的代碼,就是我們?cè)谑录喸冎刑砑舆M(jìn)去的。
所以說(shuō),事件輪詢其實(shí)就是一個(gè)永遠(yuǎn)不會(huì)終止的循環(huán),這個(gè)循環(huán)會(huì)檢查一個(gè)數(shù)組(或者說(shuō)函數(shù)調(diào)用棧)里面是否存在可執(zhí)行的代碼,如果有則進(jìn)行執(zhí)行
而什么時(shí)候這個(gè)數(shù)組里面才會(huì)被添加代碼進(jìn)去呢?

最開(kāi)始的一次添加是在整個(gè)JS代碼第一次執(zhí)行的時(shí)候,整個(gè)JS代碼被添加到了eventLoop隊(duì)列中去了,而后當(dāng)檢測(cè)到異步操作的時(shí)候,瀏覽器就會(huì)存儲(chǔ)好這些異步操作對(duì)應(yīng)的回調(diào)函數(shù),當(dāng)異步操作完成的時(shí)候,則會(huì)被添加到event loop中去,等到當(dāng)前的event loop執(zhí)行完成了以后,就會(huì)進(jìn)行下一步的執(zhí)行。

具體流程可以參考圖片(極客時(shí)間--瀏覽器原理與實(shí)踐)

圖源:極客時(shí)間--瀏覽器原理與實(shí)踐

以上的部分是如何實(shí)現(xiàn)的呢?
下面我們以setTimeout的實(shí)現(xiàn)為例子來(lái)看具體的實(shí)現(xiàn)方案.

如果要實(shí)現(xiàn)settimeout,我們不能僅需要一個(gè)消息隊(duì)列,還需要一個(gè)隊(duì)列,這個(gè)隊(duì)列維護(hù)了需要延遲執(zhí)行的任務(wù)列表。可想而知,在每次循環(huán)執(zhí)行完畢以后,我們需要檢查一下這個(gè)延遲隊(duì)列中是否有任務(wù)達(dá)到了執(zhí)行的條件,如果達(dá)到了,那么就應(yīng)該把該任務(wù)放到任務(wù)隊(duì)列中去,那么在以后任務(wù)隊(duì)列中前面的任務(wù)都執(zhí)行完畢了以后,那么這個(gè)任務(wù)就會(huì)執(zhí)行。
一般來(lái)說(shuō),這個(gè)延遲隊(duì)列中的任務(wù)應(yīng)該包括

  1. 回調(diào)函數(shù)
  2. 當(dāng)前發(fā)起時(shí)間
  3. 延遲執(zhí)行時(shí)間
  4. id 用于取消執(zhí)行等

參考代碼

// eventLoop是一個(gè)消息隊(duì)列
// (先進(jìn),先出)
var eventLoop = [ ];
var delayLoop=[];

var event;

// “永遠(yuǎn)”執(zhí)行
while (true) {
    // 一次tick
    if (eventLoop.length > 0) {
        // 拿到隊(duì)列中的下一個(gè)事件
        event = eventLoop.shift();

        // 現(xiàn)在,執(zhí)行下一個(gè)事件
        try {
            event();
                    
            if delayLoop中存在任務(wù)可以執(zhí)行了
                for
                    event.loop.push(task)
        
        }
        catch (err) {
            reportError(err);
        }
    }
}

從這里我們也可以看出來(lái),即使你設(shè)置了settimeout的時(shí)間是1000ms,實(shí)際上最后的執(zhí)行時(shí)間很可能會(huì)大于1000ms,一是因?yàn)榧词馆喌竭@個(gè)任務(wù)了但是因?yàn)榍懊孢€有任務(wù)在執(zhí)行,需要等到前面的任務(wù)執(zhí)行完畢了才會(huì)添加進(jìn)任務(wù)隊(duì)列,二是在添加到任務(wù)隊(duì)列以后,瀏覽器也會(huì)執(zhí)行一些自己的操作,導(dǎo)致最后的世界控制并不會(huì)很準(zhǔn)確。

最后我們?cè)賮?lái)回顧一下這篇文章的內(nèi)容

  1. 一些場(chǎng)景的異步的錯(cuò)誤
  2. 為什么JS引擎是單線程了
  3. 事件輪詢是怎么回事,是怎么實(shí)現(xiàn)的

文章參考
極客時(shí)間--瀏覽器工作原理與實(shí)踐
「前端進(jìn)階」從多線程到Event Loop全面梳理
《你不知道的JS》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一、單線程 主線程:JavaScript是單線程的,所謂單線程,是指在JS引擎中負(fù)責(zé)解釋和執(zhí)行JavaScript...
    puxiaotaoc閱讀 21,100評(píng)論 7 32
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,136評(píng)論 1 32
  • 弄懂js異步 講異步之前,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,748評(píng)論 0 5
  • 最近本人對(duì)于js的運(yùn)行機(jī)制,特別是異步,還有回調(diào)函數(shù)感覺(jué)很亂,于是參考了很多有用的博客(博客原文地址會(huì)在文末給出)...
    一包閱讀 1,080評(píng)論 0 2
  • 大學(xué)是個(gè)音樂(lè)劇 我在大一大二的時(shí)候?qū)λ麕缀鯖](méi)有印象,他是屬于很靦腆很害羞的那種男孩子,幾乎是不和其他人說(shuō)話聊天,除...
    黑色ET尾戒閱讀 298評(píng)論 0 0