馴服定時器和線程

前言

在javascript中,定時器是一個常常被誤用且不被衆人所知的特性,但若是在複雜應用程序中正確應用定時器的話,就會給開發人員帶來很是多的好處。javascript

1 概念

1.1 線程概述

1.js運做在瀏覽器中,是單線程的,即js代碼始終在一個線程上執行,這個線程稱爲js引擎線程。java

2.瀏覽器是多線程的,除了js引擎線程,它還有ajax

UI渲染線程
瀏覽器事件觸發線程
http請求線程
EventLoop輪詢的處理線程
複製代碼

這些線程的做用:api

UI線程用於渲染頁面
js線程用於執行js任務
瀏覽器事件觸發線程用於控制交互,響應用戶
http線程用於處理請求,ajax是委託給瀏覽器新開一個http線程
EventLoop處理線程用於輪詢消息隊列
複製代碼

單線程的含義是js只能在一個線程上運行,也就說,js同時只能執行一個js任務,其它的任務則會排隊等待執行。瀏覽器

js是單線程的,並不表明js引擎線程只有一個。js引擎有多個線程,一個主線程,其它的後臺配合主線程。bash

多線程之間會共享運行資源,瀏覽器端的js會操做dom,多個線程必然會帶來同步的問題,全部js核心選擇了單線程來避免處理這個麻煩。js能夠操做dom,影響渲染,因此js引擎線程和UI線程是互斥的。這也就解釋了js執行時會阻塞頁面的渲染。多線程

JavaScript運行時,除了一個運行線程,引擎還提供一個消息隊列,裏面是各類須要當前程序處理的消息。新的消息進入隊列的時候,會自動排在隊列的尾端。app

1.2 定時器概述

咱們說的定時器能夠在JavaScript中使用,但咱們沒說它是JavaScript自身的一個功能—定時器不是JavaScript的一項功能,定時器做爲對象和方法的一部分,才能在瀏覽器中使用。dom

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

(本文完)

相關文章
相關標籤/搜索