看了《你不知道的javascript》上卷以及中卷以後,本身的一些總結。javascript
JavaScript 引擎並非獨立運行的,它運行在宿主環境中,對多數開發者來講一般就是Web 瀏覽器。處理程序中多個塊的執行,且執行每塊時調用JavaScript 引擎,這種機制被稱爲事件循環。html
先經過一段僞代碼瞭解一下這個概念:html5
// eventLoop是一個用做隊列的數組
// (先進,先出)
var eventLoop = [ ];
var event;
// 「永遠」執行
while (true) {
// 一次tick
if (eventLoop.length > 0) {
// 拿到隊列中的下一個事件
event = eventLoop.shift();
// 如今,執行下一個事件
try {
event();
}
catch (err) {
reportError(err);
}
}
}
複製代碼
你能夠看到,有一個用while 循環實現的持續運行的循環,循環的每一輪稱爲一個tick。 對每一個tick 而言,若是在隊列中有等待事件,那麼就會從隊列中摘下一個事件並執行。這 些事件就是你的回調函數。java
必定要清楚,setTimeout(..) 並無把你的回調函數掛在事件循環隊列中。它所作的是設定一個定時器。當定時器到時後,環境會把你的回調函數放在事件循環中,這樣,在將來某個時刻的tick 會摘下並執行這個回調。jquery
若是這時候事件循環中已經有20 個項目了會怎樣呢?你的回調就會等待。它得排在其餘項目後面——一般沒有搶佔式的方式支持直接將其排到隊首。這也解釋了爲何setTimeout(..) 定時器的精度可能不高。大致說來,只能確保你的回調函數不會在指定的 時間間隔以前運行,但可能會在那個時刻運行,也可能在那以後運行,要根據事件隊列的狀態而定。ajax
listen("click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
} );
}, 500) ;
} );
複製代碼
你極可能很是熟悉這樣的代碼。這裏咱們獲得了三個函數嵌套在一塊兒構成的鏈,其中每一個函數表明異步序列(任務,「進程」)中的一個步驟。這種代碼經常被稱爲回調地獄(callback hell),有時也被稱爲毀滅金字塔(pyramid of doom,得名於嵌套縮進產生的橫向三角形狀)。數據庫
doA( function(){
doB();
doC( function(){
doD();
} )
doE();
} );
doF();
複製代碼
執行順序是?數組
A、F、B、C、E、Dpromise
在線性(順序)地追蹤這段代碼的過程當中,咱們不得不從一個函數跳到下一個,再跳到下一個,在整個代碼中跳來跳去以「查看」流程。並且別忘了,這仍是簡化的形式,只考慮了最優狀況。咱們都知道,真實的異步JavaScript程序代碼要混亂得多,這使得這種追蹤的難度會成倍增長。瀏覽器
咱們的順序阻塞式的大腦計劃行爲沒法很好地映射到面向回調的異步代碼。這就是回調方式最主要的缺陷:對於它們在代碼中表達異步的方式,咱們的大腦須要努力才能同步得上。
這是回調驅動設計最嚴重(也是最微妙)的問題。它以這樣一個思路爲中心:有時候ajax(..)(也就是你交付回調continuation 的第三方)不是你編寫的代碼,也不在你的直接控制下。多數狀況下,它是某個第三方提供的工具。
// A
ajax( "..", function(..){
// C
} );
// B
複製代碼
咱們把這稱爲控制反轉(inversion of control),也就是把本身程序一部分的執行控制交給某個第三方。在你的代碼和第三方工具(一組你但願有人維護的東西)之間有一份並無明確表達的契約。
爲了更優雅地處理錯誤,有些API 設計提供了分離回調(一個用於成功通知,一個用於出錯通知):
function success(data) {
console.log( data );
}
function failure(err) {
console.error( err );
}
ajax( "http://some.url.1", success, failure );
複製代碼
還有一種常見的回調模式叫做「error-first 風格」(有時候也稱爲「Node風格」,由於幾乎全部Node.jsAPI都採用這種風格),其中回調的第一個參數保留用做錯誤對象(若是有的話)。若是成功的話,這個參數就會被清空/置假(後續的參數就是成功數據)。不過,若是產生了錯誤結果,那麼第一個參數就會被置起/ 置真(一般就不會再傳遞其餘結果):
function response(err,data) {
// 出錯?
if (err) {
console.error( err );
}
// 不然認爲成功
else {
console.log( data );
}
}
ajax( "http://some.url.1", response );
複製代碼
教科書裏面的setTimeout 定義很簡單。
setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。普遍應用場景:定時器,輪播圖,動畫效果,自動滾動等等。可是setTimeout真的有那麼簡單嗎?
測試題
for (var i = 1;i <= 5;i ++) {
setTimeout(function timer() {
console.log(i)
},i * 1000)
}
複製代碼
答案:以一秒的頻率連續輸出五個6。
解答
這裏我引用《你不知道的javascript》中的一個比喻,能夠把做用域鏈想象成一座高樓,第一層表明當前執行做用域,樓的頂層表明全局做用域。咱們在查找變量時會先在當前樓層進行查找,若是沒有找到,就會坐電梯前往上一層樓,若是仍是沒有找到就繼續向上找,以此類推。到達頂層後(全局做用域),可能找到了你所需的變量,也可能沒找到,但不管如何查找過程都將中止。
事件循環只有一個,但任務隊列可能有多個,任務隊列可分爲宏任務(macro-task)和微任務(micro-task)。XHR回調、事件回調(鼠標鍵盤事件)、setImmediate、setTimeout、setInterval、indexedDB數據庫操做等I/O以及UI rendering都屬於宏任務(也有文章說UI render不屬於宏任務,目前尚未定論),process.nextTick、Promise.then、Object.observer(已經被廢棄)、MutationObserver(html5新特性)屬於微任務。注意進入到任務隊列的是具體的執行任務的函數。好比上述例子setTimeout()中的timer函數。另外不一樣類型的任務會分別進入到他們所屬類型的任務隊列,好比全部setTimeout()的回調都會進入到setTimeout任務隊列,全部then()回調都會進入到then隊列。當前的總體代碼咱們能夠認爲是宏任務。事件循環從當前總體代碼開始第一次事件循環,而後再執行隊列中全部的微任務,當微任務執行完畢以後,事件循環再找到其中一個宏任務隊列並執行其中的全部任務,而後再找到一個微任務隊列並執行裏面的全部任務,就這樣一直循環下去。
測試題2
console.log('global');
setTimeout(function () {
new Promise(function (resolve) {
console.log('timeout1_promise')
resolve()
}).then(function () {
console.log('timeout1_then')
});
console.log('timeout1');
},2000);
for (var i = 1;i <= 5;i ++) {
setTimeout(function() {
console.log(i)
},i*1000)
console.log(i)
}
setTimeout(function () {
console.log('timeout2')
}, 1000);
複製代碼
咱們來一步一步分析以上代碼:
首先執行總體代碼,「global」會被第一個打印出來。這是第一個輸出。
執行到第一個setTimeout時,發現它是宏任務,此時會新建一個setTimeout類型的宏任務隊列並派發當前這個setTimeout的回調函數到剛建好的這個宏任務隊列中去,而且輪到它執行時要延遲2秒後再執行。
代碼繼續執行走到for循環,發現是循環5次setTimeout(),那就把這5個setTimeout中的回調函數依次派發到上面新建的setTimeout類型的宏任務隊列中去,注意,這5個setTimeout的延遲分別是1到5秒。此時這個setTimeout類型的宏任務隊列中應該有6個任務了。再執行for循環裏的console.log(i),很簡單,直接輸出1,2,3,4,5,這是第二個輸出。
再繼續走,執行到第二個setTimeout,發現是宏任務,派發它的回調到上面setTimeout類型的宏任務隊列中去。
第一輪事件循環的宏任務執行完成(總體代碼能夠看作宏任務)。
開始第二輪事件循環:執行setTimeout類型隊列(宏任務隊列)中的全部任務。發現都有延時,但延時最短的是for循環中第一次循環push進來的那個setTimeout和第二個setTimeout,它們都只延時1s。它們會被同時執行,但前者先被push進來,因此先執行它!它的做用就是打印變量i,在當前做用域找變量i,木有!去它上層做用域(這裏是全局做用域)找,找到了,但此時的i早已經是6了。(爲啥不是5,那你得去補補for循環的執行流程了~)因此這裏第三個輸出是延時1s後打印出6。緊接着執行第二個setTimeout,它會打印出"timeout2",這是第四個輸出。
延遲一秒後,宏隊列當中前後有第一個setTimeout和for循環當中的setTimeout,上面有說到Promise.then是微任務,那麼這裏會生成一個Promise.then類型的微任務隊列,這裏的then回調會被push進這個隊列中。第五個和第六個輸出爲「timeout1_promise」,「timeout1」,以後執行微任務隊列,第七個輸出爲「timeout1_then」。以後執行宏隊列,第八個輸出」6」;
後續就每隔一秒輸出」6」,執行三次,所有代碼執行完畢。
執行結果以下:
測試題3
改動一下代碼,要它以一秒的頻率分別輸出1,2,3,4,5。
利用setTimeout第三個參數
for (var i=1; i<=5; i++) {
setTimeout( function timer(i) {
console.log(i);
}, i*1000,i);
}
複製代碼
測試題4
setTimeout(function () {
func1();
}, 0);
func2();
複製代碼
setTimeout,setInterval都存在一個最小延遲的問題,雖然你給的delay值爲0,可是瀏覽器執行的是本身的最小值。HTML5標準是4ms,但並不意味着全部瀏覽器都會遵循這個標準,包括手機瀏覽器在內,這個最小值既有可能小於4ms也有可能大於4ms。在標準中,若是在setTimeout中嵌套一個setTimeout, 那麼嵌套的setTimeout的最小延遲爲10ms。
setInterval有一個很重要的應用是javascript中的動畫。
舉個例子,假設咱們有一個正方形div,寬度爲100px, 如今想讓它的寬度在1000毫秒內增長到300px——很簡單,算出每毫秒內應該增長的像素,再按每毫秒爲週期調用setInterval實現增加。
var div = $('div')[0];
var width = parseInt(div.style.width, 10);
var MAX = 300, duration = 1000;
var inc = parseFloat( (MAX - width) / duration );
function animate (id) {
width += inc;
if (width >= MAX) {
clearInterval(id);
console.timeEnd("animate");
}
div.style.width = width + "px";
}
console.time("animate");
var timer = setInterval(function () {
animate(timer);
}, 0)
複製代碼
執行結果以下:
代碼中利用console.time來計算時間所花費的時間——實際上花的時間是明顯大於1000毫秒的,爲何?由於上面說到最小週期至少應該是4ms,因此每一個週期的增加量應該是每毫秒再乘以四。var inc = parseFloat( (MAX - width) / duration ) * 4;
複製代碼
執行結果以下:
若是你有心查看jquery的動畫源碼的話,你能發現源碼的時間週期是13ms,13ms 大概是一個在各瀏覽器上使動畫表現接近一致的值。若是最求流暢的動畫效果來講,每秒(1000毫秒)應該是60幀,這樣算下來每幀的時間應該是16.7毫秒,在這裏我把每幀定義爲完成一個像素增量所花的時間,也就是16毫秒(毫秒不容許存在小數)是讓動畫流暢的最佳值。不管你如何優化setInterval,偏差是始終存在的。但其實在HTML5中,有一個實踐動畫的最佳途徑requestAnimationFrame。這個函數能保證能以每幀來執行動畫函數。好比上面的例子就能夠改寫爲:
//init some values
var div = $('div')[0].style;
var height = parseInt(div.height, 10);
var seconds = 1;
//calc distance we need to move per frame over a time
var max = 300;
var steps = (max- height) / seconds / 16.7;
//16.7ms is approx one frame (1000/60)
//loop
function animate (id) {
height += steps; //use calculated steps
div.height = height + "px";
if (height < max) {
requestAnimationFrame(animate);
}
}
animate();
複製代碼
這種狀況下一般會有多個計時器同時運行,若是同時大量計時器同時運行的話,會引發一些個問題,好比如何回收這些計時器?jquery的做者John Resig建議創建一個管理中心,它給出的一個很是簡單的代碼以下:
var timers = {
timerID: 0,
timers: [],
add: function(fn) {
this.timers.push(fn);
},
start: function() {
if (this.timerID) return;
(function runNext() {
if (timers.timers.length > 0) {
for (var i = 0; i < timers.timers.length; i++) {
if (timers.timers[i]() === false) {
timers.timers.splice(i,1);
i--;
}
}
timers.timerID = setTimeout(runNext, 0);
}
})();
},
stop: function() {
clearTimeout(this.timerID);
this.timerID = 0;
}
};
複製代碼
注意看中間的start方法:他把全部的定時器都存在一個timers隊列(數組)中,只要隊列長度不爲0,就輪詢執行隊列中的每個子計時器,若是某個子計時器執行完畢(這裏的標誌是返回值是false),那就把這個計時器踢出隊列。繼續輪詢後面的計時器。
上面描述的整個一輪輪詢就是runNext,而且遞歸輪詢,一遍一遍的執行下去timers.timerID = setTimeout(runNext, 0)直到數組爲空。
感謝閱讀至此,後面會更新promise的總結,更優的異步解決方案。