驯服定时器和线程

定时器.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延迟。

证据代码如下:

  <script>
        var start = Date.now();
        var i = 0;

        function test() {
            if(++i == 1000) {
                console.log(Date.now() - start);
            } else {
                setTimeout(test, 0);
            }
        }
        test();
    </script>

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

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

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。

看下面的例子:

<div class="outer">
  <div class="inner"></div>
</div>

有如下的 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元素的时候,会产生不响应的用户界面。
先来看看没有优化过的代码:

   <table>
        <tbody></tbody>
   </table>
    <script>
        var tbody = document.getElementsByTagName("tbody")[0];
        for(var i = 0;i<100000;i++){
            var tr = document.createElement('tr');
            for(var t=0;t<6;t++){
                var td = document.createElement("td");
                td.appendChild(document.createTextNode(i+","+t));
                tr.appendChild(td);
            }
            tbody.appendChild(tr);
        }
    </script>

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

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

  <table>
        <tbody></tbody>
    </table>
    <script>
        var rowCount = 100000;
        var divideInto = 4;
        var chunkSize = rowCount / divideInto;
        var iteration = 0;
        
        var tbody = document.getElementsByTagName("tbody")[0];
        
        setTimeout(function generateRows(){
            var base = (chunkSize)*iteration;
            for(var i=0;i<chunkSize;i++){
                var tr = document.createElement("tr");
                for(var t=0;t<6;t++){
                    var td = document.createElement("td");
                    td.appendChild(document.createTextNode((i+base)+","+t+","+iteration));
                    tr.appendChild(td);
                }
                tbody.appendChild(tr);
            }
            iteration++;
            if(iteration < divideInto){
                setTimeout(generateRows,0);
            }
        },0);
    </script>

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

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》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,714评论 1 17
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,686评论 0 5
  • 1、 单线程、任务队列的概念 单线程: JavaScript是一个单线程语言,浏览器只会分配一个javascrip...
    海山城阅读 1,012评论 0 1
  • 在谈js定时器以前,我觉得有必要了解下javascript的事件运行机制,简称(javascript event ...
    JohnsonChe阅读 882评论 0 2
  • 9.26-9.30 第8章 驯服线程和定时器 定时器可以在js中使用,但它不是js的一项功能,如果我们在非浏览器环...
    如201608阅读 562评论 0 2