前言
在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最後到應用,讓我們一次又一次地被定時器這個神器的東西所歎服,其實吧定時器是個神奇的東西,有很多意想不到的功能等著我們去探索
(本文完)