进阶:setTimeout用法 & 任务队列异步函数节流 (10
饥人谷学习进阶第 10 天
定时器
JS提供定时执行代码的功能,主要由setTimeou()和setInterval()两个函数来完成。
setTimeout 和setInterval
setTimeout()
用来指定某个函数或某段代码,在多少毫秒之后执行(延时执行)。它返回一个整数,表示定时器的编号,之后可以用其来取消这个定时器
var timerId = setTimeout(func|code, delay)
setTimeout函数接受两个参数,第一个参数func|code是将要推迟执行的函数名或者一段代码,第二个参数delay是推迟执行的毫秒数
console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);
// 输出: 1 3 2 因为setTimeout指定第二个语句推迟1000ms再执行
注意:推迟执行的代码必须以字符串的形式,放入setTimeout,因为引擎内部使用eval函数,将字符串转为代码。如果推迟执行的是函数,则可以直接将函数名,放入setTimeout。一方面eval函数有安全顾虑,另一方面为了便于JavaScript引擎优化代码,setTimeout方法一般总是采用函数名的形式
setTimeout(fn,1000);
// or
setTimeout(function () {}, 1000)
setInterval()
用法同setTimeout,区别在于setInterval指定某个任务每隔一段时间就执行一次(间隔执行),也就是无限次定时执行
var i = 1;
var timer = setInterval (function() {
console.log(i++);
}, 1000)
// 每隔1000ms就输出一个i,1 2 3 4 5 6 ...,直到用户执行clearInterval(timer)
clearTimeout(),clearInterval()
上面两个定时器函数都返回一个表示计时器编号的整数值,将该整数传入clearTimeout和clearInterval函数,可以取消对应的定时器
单线程模型
单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
运行机制
setTimeout和setInterval:将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等待下一轮Evant Loop时重新判断。这意味着,setTimeout指定的代码,必须等到本次执行的所有代码都执行完了,才会执行。
setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f,0),那么不会立刻执行
setTimeout(f,0)将第二个参数设为0,作用是让f在现有的任务(脚本的同步任务和“任务队列”中已有的事件)一结束就立刻执行。也就是说,setTimeout(f,0)的作用是,尽可能早地执行指定的任务。
// 代码实例1:
var i=0;
for(var i=0; i<10; i++){
setTimeout(function(){
console.log(i)
}, 1000)
}
// 输出:10次 10
// 代码实例2:
var t = true;
setTimeout(function(){
t = false;
}, 1000);
while(t){ }
console.log('end')
// 输出: 陷入死循环,console.log('end')这行代码一直等待没办法执行
异步与回调
JS是一个单线程的语言,永远只有一个通道(主线程)在运行程序
JS中所谓的异步,应称为伪异步(会产生阻塞,并会相互干扰)
当主线程开始执行异步任务,实际就是执行对应的回调函数。所谓回调函数,就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

如图,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
模拟JS异步:
var foo = function(){
console.log('foo begins');
setTimeout(function(){
console.log('foo finishes');
},1000);
};
var bar = function(){
console.log('bar executed');
}
foo();
bar();
// 输出:
// foo begins
// bar executed
// foo finishes
上述程序模拟foo运行1秒而在中间的1秒运行中后面的bar也可以运行
JS异步方法存在的阻塞与干扰:
var foo = function(){
console.log('foo begins');
setTimeout(function(){
console.log('foo finishes');
},1000);
};
var bar = function(){
while(){
}
}
// foo();
// bar();
1秒之后foo finishes没有被打印出来,因为bar方法进入死循环,js引擎卡死,导致foo方法也没有被运行完,本质上取决于JS单线程程序块按队列执行的特性
异步与回调:
function f1(callback){
setTimeout(function(){
//做某件事,可能很久
console.log('别急,开始执行f1')
for(var i=0;i< 100000;i++){
}
console.log('f1执行完了')
callback()
}, 0);
}
function f2(){
console.log('执行f2');
}
function f3(){
console.log('执行f3');
}
f1(f2) //当f1执行完之后再执行 f2
f3()
函数节流
执行频繁的事件等以最后一次为准(避免极短时间内很多重复执行)
var timer;
function fn () {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function() {
console.log('do something')
}, 1000)
}
fn()
fn()
fn()
// do something
改进:
function throttle (fn, delay) {
var timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(function() {
fn(arguments);
}, delay)
}
}
function fn() {
console.log('do something');
}
function fn2 = throttle(fn, 1000);
fn2();
JS高级教程中的函数节流:
function throttle(method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function() {
method.call(context);
}, 1000)
}
参考