徹底理清JavaScript的單線程,異步,Event Loop,Promise的關(guān)系

JavaScript的三座大山:單線程與異步,原型與原型鏈(繼承),作用域和閉包。
接下來就其中的單線程與異步,和延擴涉及的事件輪詢,Promise寫下我個人的理解,算是做下總結(jié),順便給對這幾個概念的關(guān)系有些模糊的朋友提供一些思路。
若有錯誤的,敬請指正。

JS的單線程

其他面向?qū)ο笳Z言JAVA,C++,都是多線程的,即一個進程中可以并發(fā)多個線程,而每條線程并行執(zhí)行不同的任務(wù),也就是說在同一時刻可以同時進行多個任務(wù)
而單線程:沒有多個線程可供主程序來調(diào)用,簡單來說,就是同一時刻只能做一件事情
JS正是單線程的語言

console.log("abc")
alert("小魚你好")
console.log("e")

運行結(jié)果只打印出了“abc”,對話框點確定之前,“e”是不會被打印的,說明JS是順序執(zhí)行的,并且前面的代碼執(zhí)行完才能繼續(xù)執(zhí)行后面的代碼,沒有其它的多余線程可以在執(zhí)行第二行代碼的時候同時執(zhí)行第3行

為什么JS是單線程的

JS語言的應(yīng)用場景注定了它只能是單線程的語言,可以說,單線程是JavaScript的本質(zhì)
原因就在于:

避免dom渲染沖突

JavaScript是一種屬于網(wǎng)絡(luò)的腳本語言,已經(jīng)被廣泛用于Web應(yīng)用開發(fā),最早就是在HTML網(wǎng)頁上使用,進行網(wǎng)頁的交互功能

我們都知道瀏覽器利用HTML文件內(nèi)容可以渲染dom結(jié)構(gòu),JS也可以操作dom結(jié)構(gòu),則兩者都可以對dom結(jié)構(gòu)產(chǎn)生影響。
所以為了避免出現(xiàn)二者都對同一dom同時操作而造成渲染沖突,需要

  1. JS代碼執(zhí)行的時候,瀏覽器的渲染會暫停
  2. 兩端JS不能同時執(zhí)行(否則有多個源頭修改dom,會產(chǎn)生沖突)

所以:
JS本身必須是單線程的,且必須和瀏覽器渲染共用一個線程

JS的異步:單線程的解決方案

為什么要使用異步

上面說了由于JavaScript用于為網(wǎng)頁添加各式各樣的動態(tài)功能,能夠操作dom,為了避免dom渲染沖突,所以JavaScript必須是單線程的。
但是單線程又會帶來一系列的問題,比如卡頓,即前面的代碼沒有執(zhí)行完,后面的代碼又只能一直等待
比如定時器任務(wù)setTimeout/serInteral

var i,sum = 0;
for(i=0;i<1000000000;i++){
    sum+=i
}
//循環(huán)執(zhí)行期間,JS執(zhí)行和dom渲染暫時卡頓
console.log("abc") 
//由于前面的代碼沒有執(zhí)行完,后面的代碼也只能是一直等待著,即沒有“abc”被打印出

為了解決單線程帶來的問題,有了異步這個解決方案。
在可能需要等待的情況下,為了不讓這些等待的過程像alert程序一樣阻塞程序,這時候就需要異步了,即:
所有“等待的情況”都需要異步
故需要“等待”的情況也是通常前端使用的異步的場景:

  1. 定時任務(wù): setTimeout , setInvervalprocess.nextTick ,setImmediate(node特有的定時器)
  2. 網(wǎng)絡(luò)請求:ajax請求,動態(tài)<img>加載
  3. 事件綁定(比如點擊事件)
// 有定時器任務(wù)
console.log("aaa")
setTimeout(function(){   //反正1s后才執(zhí)行,先不管它,先讓其他的代碼執(zhí)行
    console.log("bbb") 
},1000)
console.log("ccc")
console.log("ddd")

運行結(jié)果:


在這里插入圖片描述
console.log("aaa")
$.ajax({        
    url:'xxxxxx',
    success:function(result){  //ajax加載完才執(zhí)行
        console.log(result)    //先不管它,先讓其他JS代碼執(zhí)行
    }
})
console.log("ccc")
console.log("ddd")

運行結(jié)果是aaa,ccc,ddd,之后才是ajax請求返回的內(nèi)容

異步的實現(xiàn)機制---Event Loop事件輪詢

JavaScript是怎么實現(xiàn)可以不依照代碼順序執(zhí)行,實現(xiàn)部分代碼(也就是異步任務(wù))的異步執(zhí)行的呢?
就是通過eventLoop即事件輪詢的機制。
換句話說,event loop是JavaScript實現(xiàn)異步的具體方案
event loop 機制的核心:

  1. 代碼分為同步代碼異步代碼(異步任務(wù)會有對應(yīng)的回調(diào)函數(shù))
  2. 同步代碼放在主執(zhí)行棧里,直接執(zhí)行
  3. 異步函數(shù)先放在異步任務(wù)隊列里,暫時先不執(zhí)行
  4. 待同步函數(shù)執(zhí)行完畢,輪詢執(zhí)行異步隊列里的函數(shù)(執(zhí)行的就是相關(guān)異步任務(wù)對應(yīng)的回調(diào)函數(shù))

第3點 將異步任務(wù)放入任務(wù)隊列時分為三種情況:

1.若異步任務(wù)沒有延時,則直接將其放入異步隊列

2.若有延時,則等延時時間到了才會放入異步隊列

3.若有ajax請求,則等ajax加載完成才放入異步隊列

第4點 “輪詢”過程理解:
JS搜索引擎會輪詢監(jiān)聽異步隊列里的函數(shù),主要主線程里空了(即主執(zhí)行棧里的同步代碼都執(zhí)行完了),就會去讀取任務(wù)隊列里的事件,調(diào)到主線程里執(zhí)行,這個過程是循環(huán)重復(fù)

// 代碼演示
$.ajax({
   url:'xxxxx',
   success:function(result) {
        console.log('a')
   }
})
setTimeout(function () {
     console.log('b')
},100)
setTimeout(function () {
    console.log('c')
)
console.log('d')

分析:


在這里插入圖片描述

運行結(jié)果是dcba或dcab
(若該ajax加載完成時間小于100ms,則ajax的回調(diào)函數(shù)的執(zhí)行先于延時100ms的定時器的回調(diào)函數(shù),則'a'會先于'b'打印)

微任務(wù)和宏任務(wù)

異步任務(wù)又分為“微隊列”和“宏隊列”里的任務(wù),微隊列里的都執(zhí)行完才會去執(zhí)行宏隊列里的異步任務(wù)
微隊列:

宏隊列:

  • setTimeout
  • setInterval
  • setImmediate(Node獨有)
  • requestAnimationFarme(瀏覽器獨有)
  • I/O
  • UI rendering(瀏覽器獨有)

常用的異步任務(wù)和執(zhí)行順序整理在下圖(圖略丑...)

在這里插入圖片描述

其中要特別注意的是promise對象一旦建立就執(zhí)行,只不過promise對象的then方法是異步的

//直接簡單粗暴的用下面簡單代碼演示
console.log('a'))
setImmediate(() => console.log('b'));
new Promise((resolve, reject) => {console.log('c');resolve()}).then(() => 
Promise.resolve().then(() => console.log('d'));
--------------------- 
// 結(jié)果: a c d b 

JS的promise:異步的解決方案

由于JS單線程的本質(zhì),需要通過異步來解決單線程帶來的問題。
而異步也會帶來問題:
1.代碼沒按照書寫形式執(zhí)行,導(dǎo)致可讀性變差

  1. callback(回調(diào)函數(shù))中不容易模塊化

回調(diào)嵌套是解決異步最直接的方法,即將后一個的操作放在前一個操作的異步回調(diào)里,但回調(diào)的多層嵌套會導(dǎo)致使代碼很冗雜,而且導(dǎo)致檢查代碼會費勁。
Promise可以用來優(yōu)雅避免callback hell問題

promise的基本語法

通過new Promise()生成一個promise實例(promise對象),傳入一個函數(shù),函數(shù)有兩個參數(shù),
第一個為resolve,第二個參數(shù)為reject,這兩個參數(shù)都是函數(shù)形式,分別是成功和失敗時執(zhí)行的函數(shù)
promise對象可調(diào)用then方法,then方法可傳入兩個參數(shù),
第一個參數(shù):成功時的回調(diào),第二個參數(shù):失敗時的回調(diào)

注意:
當(dāng)then()沒有return時則默認(rèn)返回的仍是調(diào)該then方法的promise對象
當(dāng)then()里有return則返回的是指定的promise對象

  function loadImg(src) {
        return new Promise(function (resolve, reject) {
            var img = document.createElement('img')
            img.onload = function () {
                resolve(img)
            }
            img.onerror = function () {
                reject()

            }
            img.src = src
        })
    }
    var src = "xxxxx"
    var result = loadImg(src)  //result是調(diào)用loadImg函數(shù)后返回的promise對象
    result.then(function (img) {  //promise調(diào)用then方法,傳入兩個參數(shù)
        console.log(img.width)
    },function () {
        console.log('failed')
    }).then(function (img) {  //可多次調(diào)用then方法
       console.log(img.height)
    })

promise捕獲異常

這里順便也附上如何用promise捕獲異常
想要捕獲異常時:

1. then方法只傳入一個參數(shù):成功時的回調(diào)函數(shù)(不再傳入失敗時的回調(diào))
2. 最后統(tǒng)一用catch方法捕獲異常:catch方法傳入一個函數(shù),函數(shù)的參數(shù)就是想要捕獲的產(chǎn)生異常的對象

 var src='xxxxx'
    var result=loadImg(src)
    result.then(function (img) {
        console.log(1,img.width) 
    }).then(function (img) {
        console.log(2.img.height)
    }).catch(function (ex) { //catch傳入的函數(shù)的參數(shù)就是產(chǎn)生異常的那個對象
        // 統(tǒng)一捕獲異常
        console.log(ex)
    })

總結(jié)

  • 為了避免dom渲染沖突,要求JavaScript的本質(zhì)就是單線程

  • 為了解決單線程帶來的可能造成卡頓和等待的問題,需要JavaScript的異步

  • 為了實現(xiàn)JavaScript的異步,利用的是Event Loop 事件輪詢的機制

  • 為了解決異步里回調(diào)函數(shù)嵌套帶來的問題,利用Promise 優(yōu)雅避免callback hell問題

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

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

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