驯服定时器和线程

驯服定时器和线程_第1张图片
定时器.jpg

定时器并不属于JavaScript

虽然我们一直在JavaScript中使用定时器,但是它并不是javascript的一项功能。定时器作为对象和方法的一部分,才能在浏览器中使用。也就是说,在非浏览器环境中使用JavaScript,可能定时器并不存在。比如Rhino中的定时器功能需要特定实现。

定时器和线程是如何工作的

2.1设置和清除定时器(setTimeout)

setTimeout 语法

  var timeoutID = scope.setTimeout(function[,delay,param1,param2,...])
  var timeoutID = scope.setTimeout(function[,delay])
  var timeoutID = scope.setTimeout(code[,delay])

需要注意的是,IE9及更早的IE浏览器不支持第一语法中向函数传递额外参数的功能。

返回值
返回值timeoutID是一个正整数,表示定时器的编号。这个值可以传递给clearTimeout()来取消该定时器。

注意 setTimeout()和setInterval()共用一个编号池。同一个对象上(一个window或worker),setTimeout()或setInterval()返回的定时器编号不会重复。但是不同的对象使用独立的编号池。
看下demo。

如何让低版本浏览器能够使用符合HTML5标准的定时器?

(function() {
setTimeout(function(arg1) {
    if(arg1 === 'test') {
        return;
    }
    var __nativeST__ = window.setTimeout;
    window.setTimeout = function(vCallback, nDelay) {
        var aArgs = Array.prototype.slice.call(arguments, 2);
        return __nativeST__(vCallback instanceof Function ? function() {
            vCallback.apply(null, aArgs);
        } : vCallback, nDelay);
    };
}, 0, 'test');

var interval = setInterval(function(arg1) {
    clearInterval(interval);
    if(arg1 === 'test') {
        return;
    }
    var __nativeSI__ = window.setInterval;
    window.setInterval = function(vCallback, nDelay) {
        var aArgs = Array.prototype.slice.call(arguments, 2);
        return __nativeSI__(vCallback instanceof Function ? function() {
            vCallback.apply(null, aArgs);
        } : vCallback, nDelay);
    };
}, 0, 'test');
}())

setTimeout(fn,0)真的是零延迟吗?
不是。至少4ms延迟。

证据代码如下:

  

定时器的延迟能否得到保证?
不能。下面会讲。

如何写出清理所有定时器的方法?

function clearAllTimeouts() {
    var id = setTimeout(function() {}, 0);
    while (id > 0) {
      if (id !== gid) {
        clearTimeout(id);
      }
      id--;
    }
}

能实现零延迟的定时器吗?
能。

代码如下:

  (function() {
    var timeouts = [];
    var messageName = "zero-timeout-message";
    function setZeroTimeout(fn) {
        timeouts.push(fn);
        window.postMessage(messageName, "*");
    }
    function handleMessage(event) {
        if(event.source == window && event.data == messageName) {
            event.stopPropagation();
            if(timeouts.length > 0) {
                var fn = timeouts.shift();
                fn();
            }
        }
    }
    window.addEventListener("message", handleMessage, true);
    window.setZeroTimeout = setZeroTimeout;
   })();

setZeroTimeout的实现主要依靠HTML5中狂拽酷炫吊炸天的API:跨文档消息传输Cross Document Messaging,这个功能实现非常简单主要包括接受信息的”message”事件和发送消息的”postMessage”方法。

postMessage语法:

otherWindow.postMessage(message, targetOrigin, [transfer]);

otherWindow
其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
message
将要发送到其他 window的数据。
targetOrigin通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI

监听派遣的message:

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event){
}

event 的属性有:

data-从其他 window 中传递过来的对象。
origin-调用 postMessage 时消息发送方窗口的 origin . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。
source-对发送消息的窗口对象的引用; 你可以使用此来在具有不同origin的两个窗口之间建立双向通信。

2.2 timeout与interval之间的区别

先看一个例子,这样更好说明setTimeout()和setInterval()之间的差异:

  setTimeout(function repeatMe() {
    /*假设这里有一段很长很长的代码块*/
    setTimeout(repeatMe, 10);
}, 10);
setInterval(function() {
    /*假设这里有一段很长很长的代码块*/
}, 10);

2.3 执行线程中的定时器执行

在web worker 出现之前,浏览器中所有的JavaScript都在单线程中执行的。因此,异步事件的处理程序,如用户界面事件和定时器在线程中没有代码执行的时候才进行执行。这就是说,处理程序在执行时必须进行排队执行,并且一个处理程序并不能中断另一个处理程序的执行。

下面先看一个例子:

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

大家不妨先思考一下上面代码执行的结果是什么。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

Mircotasks
Mircotasks 通常用于安排一些事,它们应该在正在执行的代码之后立即发生,例如响应操作,或者让操作异步执行,以免付出一个全新 task 的代价。mircotask 队列在回调之后处理,只要没有其它执行当中的(mid-execution)代码;或者在每个 task 的末尾处理。在处理 microtasks 队列期间,新添加的 microtasks 添加到队列的末尾并且也被执行。 microtasks 包括process.nextTick,Promise, MutationObserver,Object.observe。

看下面的例子:

有如下的 Javascript 代码,假如我点击 div.inner 会发生什么 log 呢?

var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

  new MutationObserver(function() {
    console.log('mutate');
  }).observe(outer, {
    attributes: true
  });

  function onClick() {
    console.log('click');
    setTimeout(function() {
      console.log('timeout');
    }, 0);
    Promise.resolve().then(function() {
      console.log('promise');
    });
    outer.setAttribute('data-random', Math.random());
  }

  inner.addEventListener('click', onClick);
  outer.addEventListener('click', onClick);

看下vue.js的nextTick的实现

看一下setImmediate.js异步的实现

再看下es6-promise.js中,异步的实现。

定时器的应用

3.1. 可以调整事件的发生顺序

比如: 网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,我们先让父元素的事件回调函数先发生,就要用到setTimeout(f, 0)。

  var input = document.getElementsByTagName('input[type=button]')[0];
  input.onclick = function A() {
  setTimeout(function B() {
        input.value +=' input';
      }, 0)
  };
  document.body.onclick = function C() {
      input.value += ' body'
  };
3.2 可以实现debounce方法

debounce(防抖动)方法,用来返回一个新函数。只有当两次触发之间的时间间隔大于事先设定的值,这个新函数才会运行实际的任务

该方法用于防止某个函数在短时间内被密集调用。具体来说,debounce方法返回一个新版的该函数,这个新版函数调用后,只有在指定时间内没有新的调用,才会执行,否则就重新计时。

  function debounce(fn, delay){
    var timer = null; // 声明计时器
    return function(){
      var context = this;
      var args = arguments;
      clearTimeout(timer);
      timer = setTimeout(function(){
        fn.apply(context, args);
      }, delay);
    };
 }
// 用法示例
$('textarea').on('keydown', debounce(ajaxAction, 2500))
3.3 处理昂贵的计算过程

当我们在操作成千上万个DOM元素的时候,会产生不响应的用户界面。
先来看看没有优化过的代码:

   

这个例子,我们创建了600000个DOM节点,并使用大量的单元格来填充一个表格,这个操作非常昂贵,页面会阻塞很久。

使用定时器来优化上面的代码:

  

页面渲染的时间明显快了不少。
使用定时器解决了浏览器环境的单线程限制是多么容易的事情,而且还提供了很好的用户体验。

3.4 中央定时器控制

使用定时器可能出现的问题是对大批量定时器的管理。这在处理动画时尤其重要,因为在试图操纵大量属性的同时,我们还需要一种方式来管理它们。
同时创建大量的定时器,将会在浏览器中增加垃圾回收任务的可能性。
在多个定时器中使用中央定时器控制,可以带来很大的威力和灵活性。
什么是中央定时器控制:

  • 每个页面在同一时间只需要运行一个定时器。
  • 可以根据需要暂停和恢复定时器。
  • 删除回调函数的过程变得很简单。

实现代码如下:

  var timers = {  //声明了一个定时器控制对象
    timerID: 0, //记录状态
    timers: [], //记录状态
    add: function(fn) { //创建添加处理程序的函数
        this.timers.push(fn);  
    },
    start: function() {//创建开启定时器的函数
        if(this.timerID) {
            return;
        }
        (function runNext() {
            if(timers.timers.length > 0) {
                for(var i = 0; i < timers.timers.length; i++) {
                    if(timers.timers[i]() === false) {
                        timers.timers.splice(i, 1);
                        I--;
                    }
                }
                timers.timerID = setTimeout(runNext, 0);
            }
        })();
    },
    stop: function() {//创建停止定时器的函数
        clearTimeout(this.timerID);
        this.timerID = 0;
    }
}

看看jquery中的中央定时器控制fx.tick

好了,讲完了。如果有收获的话,双击666。

参考文档如下:

Tasks, microtasks, queues and schedules
Concurrency model and Event Loop
setTimeout with a shorter delay
JS中的异步以及事件轮询机制
这是个视频
JavaScript 运行机制详解:再谈Event Loop
JavaScript参考标准教程--定时器
setImmediate.js

参考书籍:
《JavaScript Ninja》

你可能感兴趣的:(驯服定时器和线程)