在開始接觸JavaScript的時候,書上有一句話我記憶深刻,JavaScript是一門單執行緒語言,不管從什麼途徑去獲取這個訊息,前端開發者都會記住,哦~~,JavaScript是一門單執行緒語言,所以alert()會卡住
為什麼JavaScript是一門單執行緒語言?因為什麼原因讓JavaScript出生就是單執行緒語言?
提出問題,必然解決問題,從JavaScript出生說起,最開始JavaScript是配合html去完成對dom的控制,優化使用者互動,於是問題來了
- js可以修改Dom結構
- 瀏覽器渲染Dom結構
假如同時執行,會發生什麼,假如同時操作同一個Dom怎麼辦,
同理 兩段js都修改Dom結構,假如同時操作Dom會發生什麼
js沒有執行的時候,瀏覽器Dom渲染,js執行的時候,瀏覽器Dom停止渲染,html結構樹本來是就是從上往下渲染,也就是說,瀏覽器渲染Dom本身就是單執行緒,js為了避免發生Dom衝突,沒有辦法只能成為單執行緒語言
但是單執行緒的硬傷出現了,單執行緒意味著頁面載入會卡頓,執行緒被佔用瀏覽器卡死
var sum = 0;
console.log("start");
for (let i = 0; i < 1000000000; i++) { //單執行緒佔用
sum++
}
console.log(sum);
alert("100") //單執行緒佔用
console.log("我終於執行啦");
例如上面的程式碼,瀏覽器資源類佔用,就會發生可怕的事情,瀏覽器卡死,也許這裡for迴圈不形式,但是現實開發裡面同步ajax請求慢的時候頁面卡死是正常事
同步解決方案 – 非同步
說到非同步,不得不談大名鼎鼎的event-loop(事件輪詢)
相信開發者一定看過阮一峰大神對Event loop的解釋,除了圖有點看不懂之外,其他都講的很請求
這裡要說一道比較老的面試題
setTimeout(() => {
console.log("我是定製器");
});
console.log("我是主程式");
很簡單 先執行console.log("我是主程式");
,為什麼呢?即使因為事件輪詢
當瀏覽器從上往下解析,遇到setTimeout,setTimeout是非同步任務,瀏覽器就會將他放入非同步任務裡面,然後執行主執行緒的程式碼,當主執行緒的程式碼執行完畢,事件輪詢就是去檢視非同步任務,於是執行了setTimeout裡面的函式
這就是事件輪詢
這裡借閱阮一峰大神的一段部落格
單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。
如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒著的,因為IO裝置(輸入輸出裝置)很慢(比如Ajax操作從網路讀取資料),不得不等著結果出來,再往下執行。
JavaScript語言的設計者意識到,這時主執行緒完全可以不管IO裝置,掛起處於等待中的任務,先執行排在後面的任務。等到IO裝置返回了結果,再回過頭,把掛起的任務繼續執行下去。
於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。
具體來說,非同步執行的執行機制如下。(同步執行也是如此,因為它可以被視為沒有非同步任務的非同步執行。)
(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
(2)主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主執行緒不斷重複上面的第三步。
event-loop就像哨兵一樣,監視著非同步佇列,一旦有非同步程式碼,就會在主執行緒執行完畢將非同步方法,讀取到主執行緒,在回去監視非同步佇列,所以event-loop也叫事件輪詢
jQuery.deferred()
無法改變 JS 非同步和單執行緒的本質
只能從寫法上杜絕 callback 這種形式
他是語法糖,但是解構了程式碼
體現了程式設計思想 : 開放封閉原則
對擴充開放,對修改封閉, 越是深入模組化,越是體現這樣的實現
$.ajax({
url: "test.html",
success: function(){
alert("哈哈,成功了!");
},
error:function(){
alert("出錯啦!");
}
});
老版本的使用的是回撥函式形式,那麼假如我們想修改程式碼,勢必要對$.ajax({})裡面的所有程式碼進行變成 這就成了
對擴充封閉,對修改封閉,這是對在開發上非常不友好的變成方式,牽一髮而動全身
當deferred出現後,ajax變成了鏈式呼叫
$.ajax("test.html")
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出錯啦!"); });
這裡明顯可以看到對修改封閉,對擴充開放
假如我們想擴充
.done(function(){ alert("第二個回撥函式!");} );
完全不影響之前的程式碼,程式碼耦合性低
在我最開始接觸jquery的時候,ajax有三種寫法
$.ajax({
url,
success: function (){
//成功回撥
},
error: function (){
//失敗回撥
}
})
$.ajax({url})
.done(()=> {
//成功回撥
})
.fail((0=> {
//失敗回撥
})
$.ajax({url})
.then(()=> {
//成功回撥
},
()=> {
//失敗回撥
})
剛開始不解,為什麼第一種是淘汰的寫法.再後來漸漸深入才發現回撥的侷限性,
- 耦合性太高,
- 不支援鏈式呼叫,
- 不利於維護,回撥裡程式碼過於複雜會產生風險
這讓我想探究一下,如何實現.done.fail,所以就不得不說jQuery的deferred
在使用jq我們可以使用deferred幫助我們控制非同步
function waitHandle() {
var dtd = $.Deferred()
var wait = function (dtd) {
var task = function () {
console.log("執行完成");
dtd.resolve()
}
setTimeout(task, 2000);
return dtd.promise() //返回pomise物件 防止在回撥裡面執行控制回撥的函式 例如reject()
}
return wait(dtd) //返回wait的執行結果
}
waitHandle()
.then(function () {
console.log(1);
}, () => {
console.log("hahah");
})
.then(() => {
console.log(2);
}, () => {
console.log("hahah");
})
這裡我就不贅述了,前人已經全面的總結了;這裡明顯看到,這是已經初步提出了promise的概念,對日後的標準建立的基礎
Promise
對於低版本瀏覽器,想要使用第三方庫,可以使用bluebird.js,對promise進行實現
老生長談了,開發必須要會
這是jq的deferred的進階版啊,,開發者卸磨殺驢,竟然說jq淘汰了,一葉障目
function loadImg(src){
return new Promise((resolve,reject)=> {
var img = document.createElement(`img`)
img.onload = ()=> {
resolve(img)
}
img.onerror = ()=> {
reject("圖片載入失敗")
}
img.src = src
})
}
var load1 = loadImg("https://secure.gravatar.com/avatar/7337a05ac8210b3f1b522818cd31b90d?s=86")
var load2 = loadImg("https://avatars2.githubusercontent.com/u/33681955?s=460&v=4")
load1.then(res=> {
console.log(res.width);
return load2 //後面的是load2的回撥 對於請求串聯 可以這麼寫
}).then(res=> {
console.log(res.width);
}).catch(res=> {
console.log(res);
})
Promise.all
接受一個Promise物件的陣列,待全部完成後,統一執行then,失敗一個catch
Promise.all([load1,load2])
.then(res=> {
console.log(res);
})
.catch (res=> {
console.log(res); //假如有一個失敗就會返回catch
})
Pormise.race
接收一個包含多個Promise物件的陣列,主要完成一個就執行then,失敗一個catch
Promise.race([load1, load2])
.then(res => {
console.log(res); //成功一個立馬返回
})
.catch(res => {
console.log(res); //假如有一個失敗就會返回catch
})
在Peomise上還要說一點就是狀態變化
三種狀態: pending(待定的) fulfilled(滿足的) rejected(拒絕)
規則: 狀態變化不可逆
在初始狀態下,Promise是pending 只有兩種情況
- Promise執行成功 pending 成功狀態變成fulfilled
- Promise執行成功 pending 失敗狀態變成rejected
瞭解一下
async/await
then是將callback進行拆分了
關於async/await 其實網上資料也挺多的,將非同步程式碼同步進行,取消了.then這樣的寫法,迴歸本源
let load = async function() {
let result1 = await loadImg(load1)
console.log(result1);
let result2 = await loadImg(load2)
console.log(result2);
}
總結一下JavaScript非同步的前世今生
- JavaScript是單執行緒,兩段js不可以同時進行
- 原因是為了避免 DOM的渲染衝突
- 非同步就是”無奈的解決方案”,但是初期有很多問題
- 非同步是event loop實現的,事件輪詢 非同步方法 進入的非同步佇列,主執行緒執行完畢才會去執行非同步佇列裡面的程式碼
- jQuery Deferred,改變了非同步只能由callback操作這個問題
- deferred和Promise 有區別的 Promise對deferred進行優化 ,讓Promise物件只能被動監聽結果,避免衝突
- Promise的出現成為了對非同步的最佳解決方案相對於callback,極大的降低了耦合性
- async/await 配合Promise 成為終極解決方案,但是ES7為成為正式版,未來仍需參考
非同步的解決方案
- 回撥函式 – callback
- 初代鏈式 – deferred (我覺得這個有必要提出來,改變js非同步程式設計的關鍵)
- 成熟方案 – promise
- 終極方案 – async/await + promise
- 迭代器 Generator(同樣實現非同步,但是被async/await替代)