定时器并不属于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》
网友评论