驯服定时器和线程

作者: 打铁大师 | 来源:发表于2017-11-09 10:56 被阅读119次
定时器.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》

相关文章

  • 驯服定时器和线程

    定时器并不属于JavaScript 虽然我们一直在JavaScript中使用定时器,但是它并不是javascrip...

  • 6、驯服线程和定时器

    定时器和线程是如何工作的 JavaScript提供了两种方式,用于创建定时器以及两个相应的清除方法。这些方法都是w...

  • 30天学习计划 js忍者秘籍 第8章 驯服线程和定时器

    9.26-9.30 第8章 驯服线程和定时器 定时器可以在js中使用,但它不是js的一项功能,如果我们在非浏览器环...

  • IOS常见问题分析

    1、定时器问题 1) 定时器在子线程中不启动: 子线程中启动定时器的方法:

  • 浏览器内核(3)

    定时器触发线程 负责执行异步定时器五类的函数的线程,如:setTimeout, setinterval. 主线程依...

  • Java 进阶:多线程2

    目录 一、Lock 接口 二、线程间的通信 三、线程池 四、定时器 Timer 五、多线程和异常 一、Lock 接...

  • 进程/多线程

    进程与线程 线程 创建定时器 线程同步synchronized 线程通信-wait/notify Lock&Con...

  • 运行循环 RunLoop

    观察RunLoop的活动阶段 定时器 图片加载 线程常驻 CD定时器

  • 定时器和线程问题

    定时器和线程 定时器并不是JavaScript中的一项功能。 定时器作为对象和方法的一部分,才能在浏览器中使用。 ...

  • 8.23成长笔记

    一、工作 1.定时器和线程池的使用,敲代码熟悉了一遍。 (1)Timer定时器类,scheduleAtFixedR...

网友评论

    本文标题:驯服定时器和线程

    本文链接:https://www.haomeiwen.com/subject/hnvjmxtx.html