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同時操作而造成渲染沖突,需要
- JS代碼執(zhí)行的時候,瀏覽器的渲染會暫停
- 兩端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程序一樣阻塞程序,這時候就需要異步了,即:
所有“等待的情況”都需要異步
故需要“等待”的情況也是通常前端使用的異步的場景:
- 定時任務(wù):
setTimeout,setInverval和process.nextTick,setImmediate(node特有的定時器)- 網(wǎng)絡(luò)請求:ajax請求,動態(tài)<img>加載
- 事件綁定(比如點擊事件)
// 有定時器任務(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 機制的核心:
- 代碼分為同步代碼和異步代碼(異步任務(wù)會有對應(yīng)的回調(diào)函數(shù))
- 同步代碼放在主執(zhí)行棧里,直接執(zhí)行
- 異步函數(shù)先放在異步任務(wù)隊列里,暫時先不執(zhí)行
- 待同步函數(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ù)
微隊列:
- process.nextTick(Node獨有)
- Promise的then方法
- Object.observe
- MutationObserver
宏隊列:
- 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)致可讀性變差
- 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問題