前言
在javascript中,定時器是一個經常被誤用且不被眾人所知的特性,但如果在複雜應用程式中正確應用定時器的話,就會給開發人員帶來非常多的好處。
1 概念
1.1 執行緒概述
1.js運作在瀏覽器中,是單執行緒的,即js程式碼始終在一個執行緒上執行,這個執行緒稱為js引擎執行緒。
2.瀏覽器是多執行緒的,除了js引擎執行緒,它還有
UI渲染執行緒瀏覽器事件觸發執行緒http請求執行緒EventLoop輪詢的處理執行緒複製程式碼
這些執行緒的作用:
UI執行緒用於渲染頁面js執行緒用於執行js任務瀏覽器事件觸發執行緒用於控制互動,響應使用者http執行緒用於處理請求,ajax是委託給瀏覽器新開一個http執行緒EventLoop處理執行緒用於輪詢訊息佇列複製程式碼
單執行緒的含義是js只能在一個執行緒上執行,也就說,js同時只能執行一個js任務,其它的任務則會排隊等待執行。
js是單執行緒的,並不代表js引擎執行緒只有一個。js引擎有多個執行緒,一個主執行緒,其它的後臺配合主執行緒。
多執行緒之間會共享執行資源,瀏覽器端的js會操作dom,多個執行緒必然會帶來同步的問題,所有js核心選擇了單執行緒來避免處理這個麻煩。js可以操作dom,影響渲染,所以js引擎執行緒和UI執行緒是互斥的。這也就解釋了js執行時會阻塞頁面的渲染。
JavaScript執行時,除了一個執行執行緒,引擎還提供一個訊息佇列,裡面是各種需要當前程式處理的訊息。新的訊息進入佇列的時候,會自動排在佇列的尾端。
1.2 定時器概述
我們說的定時器可以在JavaScript中使用,但我們沒說它是JavaScript自身的一個功能—定時器不是JavaScript的一項功能,定時器作為物件和方法的一部分,才能在瀏覽器中使用。
2 原理
下面我們通過一張圖片來執行緒中的定時器工作原理
上面這張圖出自《js忍者祕籍1》,本胖這裡借用一下哈
這張圖有很多資訊需要消化,但完全理解以後就會對js的非同步執行工作有一個更加深入的理解。
這張圖的X軸是以毫秒為單位的時間軸矩形快的大小意味著js程式碼的執行部分以及執行時間。下面本胖就以時刻為單位來簡單明瞭地說清楚這張圖的內涵哈。
在0ms時刻
啟動一個10ms的延遲的定時器(代號呂肥肥)啟動一個10ms的間隔定時器(代號呂胖胖一代)啟動一個大約18ms執行時間的主線js程式碼塊(代號王大熊)複製程式碼
在6ms時刻
一個滑鼠單擊事件(代號呂小花)複製程式碼
在18ms時刻
王大熊執行完畢但是在0-18ms這18ms時間內傳送了很多事情在10ms的時候呂肥肥和呂胖胖一代都想執行。但是呢,主執行緒裡面王大熊還站著坑呢,於是呂肥肥和呂胖胖只好乖乖地排隊,對了還有一個6ms想要執行的呂小花就會在第18ms後才能執行複製程式碼
在20ms時刻
這時候主執行緒裡面佔坑的是呂小花,這時候呂胖胖二代又誕生了但是呢,呂胖胖還在排著隊呢,所以這個呂胖胖二代會被廢棄也就是說瀏覽器不會對特定(比如呂胖胖)間隔定時器的多個例項進行排隊複製程式碼
第28ms時刻
呂小花已經執行完畢,這時候排著隊的有呂肥肥以及呂胖胖一代於是就會執行呂肥肥複製程式碼
第30ms時刻
呂胖胖三代又誕生了,但是呢這時候呂胖胖一代還在排隊(好苦逼的呂胖胖)所以這個呂胖胖三代也是要被廢棄的(瀏覽器就是這麼聰明)複製程式碼
第35ms時刻
呂肥肥執行完畢,這時候主執行緒完全空了,要開始執行呂胖胖一號了複製程式碼
第40ms時刻
呂胖胖四代又誕生了,這時候呢沒有其他呂胖胖在排隊了,那麼這個呂胖胖四號就會排隊等待被執行複製程式碼
第42ms時刻
呂胖胖一代執行完畢,這時候排隊的是有呂胖胖四代,所以就會執行呂胖胖四代(呂胖胖二代,呂胖胖三代都被廢棄)複製程式碼
上面分析了0ms-42ms這42ms間發生的事情,可以得出如下的結論
1.js引擎是單執行緒的,非同步事件要排隊才能執行2.無法保證設定的定時器在什麼時候執行3.某一時刻,相同setInterval例項只會有一個在排隊複製程式碼
3 API
上面這張圖是定時器的api集合,這裡需要強調一點
無論是window.setTimeout還是window.setInterval,在使用函式名作為呼叫控制程式碼時都不能帶引數,而在許多場合必須要帶引數,這就需要想方法解決。
3.1 使用字串傳參
function say(name) {
console.log(name);
}setTimeout('say("我是放在字串裡面的傳進來的呂肥肥")', 1000);
複製程式碼
3.2 返回新函式
function say(name) {
console.log(name);
}function _say(name) {
return function() {
say(name);
}
}複製程式碼
3.3 修改setTimeout
var _setTimeout = setTimeout;
window.setTimeout = function(cb, param, time) {
var args = Array.prototype.slice.call(arguments, 1);
var _cb = function() {
cb.apply(null, args);
};
_setTimeout(_cb, time);
}window.setTimeout(say, '我是改造過setTimeout才被傳進來的王大熊', 2000);
複製程式碼
其實吧,上面的方法都是可以不用的,因為setTimeout預設就是執行第三個引數的(這一點是本胖做分享的時候同事提出來的,非常感謝),直接想下面這樣就可以傳入引數
setTimeout((name) =>
{
console.log(name)
}, 1000, '呂胖胖');
複製程式碼
4 應用
任何知識只有在用實際開發中才有存在的意義,定時器也一樣。下面我們來看看定時器有哪些用處。
4.1 動畫
上圖是之前做活動的一個彈幕效果,當時用的就是定時器。
function Barrage(box) {
this.box = box;
}Barrage.prototype = {
// 氣泡動效 randomPop: function (val) {
var item = document.createElement('span'), box = this.box, randomLeft = this.random(0, (box.clientWidth / 2)), randomTop = this.random(0, box.clientHeight - 15);
item.style.left = randomLeft + 'px';
item.style.top = randomTop + 'px';
item.innerText = val;
box.appendChild(item);
item.addEventListener('animationend', function() {
item.remove();
});
}, // 在min,max之間的隨機數 random: function (min, max) {
return (min + Math.random() * (max - min)).toFixed(2);
}
};
var box = document.querySelector('.barrage-box');
var zimu = new Barrage(box);
var time = 0, inter = null, isRun = true, assistList = [ {
nickName: '呂肥肥', num: 100
}, {
nickName: '呂胖胖', num: 1200
}, {
nickName: '王大熊', num: 200
}, {
nickName: '王大虎', num: 1000
}, {
nickName: '呂肥肥', num: 100
}, {
nickName: '呂胖胖', num: 1200
}, {
nickName: '王大熊', num: 200
}, {
nickName: '王大虎', num: 1000
}, {
nickName: '呂肥肥', num: 100
}, {
nickName: '呂胖胖', num: 1200
}, {
nickName: '王大熊', num: 200
}, {
nickName: '王大虎', num: 1000
} ];
function go() {
clearTimeout(inter);
assistList.forEach(function (item) {
time++;
inter = setTimeout(function () {
if (isRun) {
zimu.randomPop(item.nickName + '注入' + item.num + '銅板');
} time++;
if (time === assistList.length * 2) {
time = 0;
go();
}
}, time * 2000);
});
}document.addEventListener('visibilitychange', function () {
if (document.hidden) {
isRun = false;
} else {
isRun = true;
}
});
go();
複製程式碼
之所以採用這段程式碼來說明定時器做動效的例子,是因為當你用定時器做動效的時候,有一點需要特別注意那就是當app被切換到後臺或者瀏覽器tab切換後再次到動效頁面,這時候間隔時間內所有定時器的例項都將同時執行,會造成下面這樣的情況(這裡資料少,不是很明顯)
所以這裡面用了visibilitychange事件,來做一個判斷,誰讓瀏覽器太機智了哈。
4.2 節流+防抖
節流和防抖這對好兄弟很容易被人混淆,這裡做一個說明哈。
節流
一定時間內js方法只跑一次,多數在監聽頁面元素滾動事件的時候會用到多數在監聽頁面元素滾動事件的時候會用到複製程式碼
防抖
頻繁觸發的情況下,只有足夠的空閒時間,才執行程式碼一次最常見的就是使用者註冊時候的手機號碼驗證和郵箱驗證了複製程式碼
下面用定時器來分別實現簡單的節流和防抖。
節流
var canRun = true;
document.body.onscroll = function () {
if (!canRun) {
// 判斷是否已空閒,如果在執行中,則直接return return;
} canRun = false;
setTimeout(function () {
console.log("函式節流");
canRun = true;
}, 300);
};
複製程式碼
防抖
var timer = false;
document.body.onscroll = function () {
clearTimeout(timer);
// 清除未執行的程式碼,重置回初始化狀態 timer = setTimeout(function () {
console.log("函式防抖");
}, 300);
};
複製程式碼
4.3 處理昂貴的計算
在處理一些資料量很多的操作時候(尤其是大量dom操作的時候),會發現瀏覽器會變的很慢,比如下面的這段程式碼,目的就是想頁面動態插入500000個tr節點。
var tbody = document.querySelector('#table');
for (var i = 0;
i <
500000;
i++) {
var tr = document.createElement('tr');
tr.innerText = i;
tbody.appendChild(tr);
}複製程式碼
其實我們可以巧用定時器的作用
var num = 500000, divideInto = 10, chunkSize = num / divideInto, flag = 0;
var tbody = document.querySelector('#table');
setTimeout( function add() {
var base = chunkSize * flag;
for (var i = 0;
i <
chunkSize;
i++) {
var tr = document.createElement('tr');
tr.innerText = flag * chunkSize + i;
tbody.appendChild(tr);
} flag++;
if (flag <
divideInto) {
setTimeout(add, 0);
}
}, 0);
複製程式碼
5 總結
上面說了這麼多,從概念到原理到api最後到應用,讓我們一次又一次地被定時器這個神器的東西所歎服,其實吧定時器是個神奇的東西,有很多意想不到的功能等著我們去探索
(本文完)