馴服定時器和執行緒

我叫呂胖胖發表於2018-08-08

前言

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

(本文完)

相關文章