以小見大——從setTimeout引伸JS的幾大特性

前言

最近在複習JS基礎,回新手村整理下筆記
回想當初看書的過程,有兩位朋友曝光度極高
那就是setTimeoutsetIntervalhtml

他們的一些迷惑行爲,初看實在讓人摸不着頭腦,但其實背後暴露出了JS的幾大特性
若是能全盤理解,也就能基本掌握JS的一些原理了node

先介紹一下

setInterval,是每隔一段時間執行一次函數,而setTimeout則是一段時間後進行,他們的用法基本同樣,因此下面也就只講解setTimeoutchrome

setTimeout(function (a,b) { console.log(a+b) }, 2000, 1, 2)promise

  • 第一個參數: 推遲執行的回調函數,也能夠直接寫函數名
  • 第二個參數: 推遲的毫秒數

若是不設置瀏覽器會自動配置時間,在IE,FireFox中,第一次配可能給個很大的數字,100ms上下,日後會縮小到最小時間間隔,Safari,chrome,opera則多爲10ms上下。瀏覽器

  • 從第三個參數開始,是給回調函數傳的參數
  • 返回值:定時器的id

能夠經過clearTimeout(id)來清除這個定時器
或者也可使用setInterval的清除方法clearInterval()
只不過從語義上來講不推薦bash

迷惑一:執行順序

雖然setTimeout能夠定時執行函數,但實際上它的執行時間不是精確的 這就要說到它的原理了
咱們先看一個極端的例子,把第二個參數設置爲0閉包

setTimeout(function () {
    console.log('1')
}, 0)
console.log('2')
//2 1
複製代碼

雖然設置爲0了,但也不是當即執行的
這個API是瀏覽器提供的,因此瀏覽器處理後會將setTimeout要執行的匿名函數添加到異步隊列
須要等待到函數調用棧清空以後,即全部可執行代碼執行完畢以後,纔會開始執行執行這個異步隊列,而且是先進先出
而setTimeout設定的延遲時間,並不是相對於setTimeout執行這一刻,而是相對於其餘代碼執行完畢這一刻。app

大體過程如上,理解了異步的過程大概也就明白執行順序了
可是,其實這不夠全面
出了新手村以後碰見了各式各樣的新的朋友 好比 promise

迷惑二:比promise的優先性差?

setTimeoutpromise都是異步的,按照隊列先進先出的順序來講
若是給setTimeout設置爲0,同時放置在promise以前,那應該執行完同步代碼以後就執行setTimeout的函數異步

setTimeout(function () {
    console.log('setTimeout1');
}, 0);
new Promise(function (resolve) {
    resolve();
}).then(function () {
    console.log('then1')
})
console.log('script end')
複製代碼

但實際上的輸出結果是async

// script end
// then1
// setTimeout1
複製代碼

.then()setTimeout優先執行了
那再有個async函數的話,執行順序又是什麼呢
若是不能堅決地回答,那說明咱們以前的理解必定還差了點東西

Event Loop

完整的事件循環如上圖

異步隊列還分爲Task(宏任務)隊列MicroTask(微任務)隊列
在最新標準中,它們被分別稱爲task與jobs。
MicroTask會優先於Task執行。

  • 宏任務:script(全局任務), setTimeout, setInterval, setImmediate, I/O(包括各類鍵鼠事件), UI rendering(不肯定).
  • 微任務:process.nextTick, Promise, Object.observer,MutationObserver,callback

同時,Javascript引擎在執行Microtask隊列的時候,若是期間又加入了新的Microtask,則該Microtask會加入到以前的Microtask隊列的尾部,保證Microtask先於Task隊列執行。

  1. 先在執行棧中執行整個script。
  2. 遇到微任務和宏任務,分別添加到微任務隊列和宏任務隊列中去。
  3. 當前宏任務執行完畢,當即執行微任務隊列中的任務
  4. 當前微任務隊列中的任務執行完畢,檢查渲染,GUI線程接管渲染。
  5. 繼續執行下一個宏任務從事件隊列中取。

因此在咱們寫下setTimeout(fn,0)的時候他並非在當時當即執行,是從下一個Event loop開始執行,便是等當前全部腳本執行完再運行,就是"儘量早"。

引用自https://juejin.im/post/5b93829de51d450e7579b171

若是想挑戰一下,能夠看一下這道題目

輸出結果

async/await其實就是 promise的語法糖
async function 聲明將定義一個返回 AsyncFunction對象的異步函數。
當調用一個 async 函數時,會返回一個 Promise 對象。
await以後的函數語句至關於被包裹在 .then()裏面,因此被推動了微任務隊列

注意:
上面的測試結果是谷歌瀏覽器73版本以後輸出的結果
在谷歌瀏覽器73版本之前,以及node中,promise的優先級都要大於這個await給出的回調函數,因此即使在任務隊列中await的回調是先進入的,也要在promise.then()以後執行
也就是說async1 endthen3的順序會顛倒

不過在73版本以後,爲了不await的執行須要至少3次tick,性能比較慢,因此 使用對PromiseResolve的調用來更改await的語義,以減小在公共awaitPromise狀況下的轉換次數。
若是傳遞給await的值已是一個Promise,那麼這種優化避免了再次建立Promise包裝器,在這種狀況下,咱們從最少三個microtick到只有一個microtick
因此上圖async1 end 會在then3前面

迷惑三:this指針

var x=1
function hhh () {
var x=2
setInterval(function() {
console.log(x)
console.log(this.x)},1000)}
// 2 1
複製代碼

this指針的指向是最使人頭疼的,四種綁定很是反直覺
函數中的this指向的是執行上下文,而這個例子中匿名函數最終執行的環境就是瀏覽器,因此this.x就是window.x

關於執行上下文

當瀏覽器加載script的時候,默認直接進入Global Execution >Context(全局上下文),將全局上下文入棧。若是在代碼中調用了函數,則會建立Function Execution Context(函數上下文)並壓入調用棧內,變成當前的執行環境上下文。當執行完該函數,該函數的執行上下文便從調用棧彈出返回到上一個執行上下文。 能夠看着這個圖感覺一下

但不少時候,咱們看不清函數究竟是在什麼上下文執行的,因此 ES6的箭頭函數必定程度上解決了這個問題, this指向的是聲明時的上下文

還有相似這樣的例子

function User(login) {
this.login = login;
this.sayHi = function() {
console.log(this.login);
}
}
var user = new User('John');
setTimeout(user.sayHi, 1000);
// undefined
複製代碼

能夠這樣調用來解決問題

setTimeout(function() {
	user.sayHi();
}, 1000);
複製代碼

或者利用bind進行綁定
也能夠用call或者apply方法,可是會致使函數當即執行,失去延時效果

setTimeout(user.sayHi.bind(user), 1000);
複製代碼

迷惑四:閉包

var x=1
function hhh () {
var x=2
setTimeout(function() {
    console.log(x)
1000)}
// 2
複製代碼

由於在調用setTimeout時發生了閉包
而匿名函數在執行時雖然已經不在hhh函數環境裏了,但被定義的時候被告知:執行的時候你去調用hhh函數的x,已經綁定給你了
注意,在定義時只是進行綁定,並無真正傳參\

因此下面會發生下面這個老生常談的問題

for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
//輸出5個5,且每隔一秒一次
複製代碼

上面這段話,咱們能夠把它翻譯一下

var i = 5;
function timer() {
console.log(i);
}
setTimeout( timer, 1 * 1000 );
setTimeout( timer, 2 * 1000 );
setTimeout( timer, 3 * 1000 );
setTimeout( timer, 4 * 1000 );
setTimeout( timer, 5 * 1000 );
複製代碼

因此一秒一次,以及輸出都是5
由於在定義匿名函數的時候,使用了i值來設置時間
可是參數只是進行了綁定,真正執行的時候纔會取到那個值,而此時i已經變成了5

解決辦法有三種:

  • 把var改爲let
  • 用當即函數包裹匿名函數
  • 利用setTimeout的第三個參數立即傳參

方法蠻多,你們應該都會用,就不贅述了
但其實原理都同樣,就是不把匿名函數的參數綁定到公用的i值上去,而是每次循環時,將i值保存在一個閉包中,當匿名函數執行時,則訪問對應閉包保存的i值便可

總結

setTimeout和setInterval其實並不推薦被大量使用
尤爲是setInterval,可能會出現間隔被跳過的問題,這個能夠參考這篇文章 www.cnblogs.com/xiaohuochai…

但經過對他們進行研究,能夠以小見大地理解JS運行機制 原理部分若是有寫的不對的,歡迎指正

相關文章
相關標籤/搜索