驯服定时器和线程

1,439 阅读8分钟

前言

在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最后到应用,让我们一次又一次地被定时器这个神器的东西所叹服,其实吧定时器是个神奇的东西,有很多意想不到的功能等着我们去探索

(本文完)