setTimeout()
函数:用来指定某个函数或某段代码在多少毫秒以后执行。它返回一个整数,表示定时器timer
的编号,能够用来取消该定时器。javascript
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
console.log(3);
复制代码
问:最后的打印顺序是什么?(若是不了解js的运行机制就会答错)java
正确答案:1 3 2
面试
解析:不管setTimeout的执行时间是0仍是1000,结果都是先输出3后输出2,这就是面试官经常考查的js运行机制的问题,接下来咱们要引入一个概念,JavaScript 是单线程的。ajax
JavasScript引擎是基于事件驱动和单线程执行的,JS引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JS线程在运行程序,即主线程。浏览器
通俗的说:JS在同一时间内只能作一件事,这也常被称为 “阻塞式执行”。闭包
那么单线程的JavasScript是怎么实现“非阻塞执行”呢?异步
答:异步容易实现非阻塞,因此在JavaScript中对于耗时的操做或者时间不肯定的操做,使用异步就成了必然的选择。async
诸如事件点击触发回调函数、ajax通讯、计时器这种异步处理是如何实现的呢?函数
答:任务队列oop
全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。
任务队列:一个先进先出的队列,它里面存放着各类事件和任务。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
console.log()
除此以外,任务队列又分为macro-task(宏任务)与micro-task(微任务),在ES5标准中,它们被分别称为task与job。
一次事件循环中,先执行宏任务队列里的一个任务,再把微任务队列里的全部任务执行完毕,再去宏任务队列取下一个宏任务执行。
注:在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
setTimeout 和 setInterval的运行机制是将指定的代码移出本次执行,等到下一轮 Event Loop 时,再检查是否到了指定时间。若是到了,就执行对应的代码;若是不到,就等到再下一轮 Event Loop 时从新判断。
这意味着,setTimeout指定的代码,必须等到本次执行的全部同步代码都执行完,才会执行。
优先关系:异步任务要挂起,先执行同步任务,同步任务执行完毕才会响应异步任务。
console.log('A');
setTimeout(function () {
console.log('B');
}, 0);
while (1) {}
复制代码
你们再猜一下这段程序输出的结果会是什么?
答:A
注:建议先注释掉while循环代码块的代码,执行后强制删除进程,否则会形成“假死”。
同步队列输出A
以后,陷入while(true){}
的死循环中,异步任务不会被执行。
相似的,有时addEventListener()
方法监听点击事件click
,用户点了某个按钮会卡死,就是由于当前JS正在处理同步队列,没法将click
触发事件放入执行栈,不会执行,出现“假死”。
for (var i = 0; i < 4; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
复制代码
输出结果为,隔1s后一块儿输出:4 4 4 4
for循环是一个同步任务,为何连续输出四个4?
答:由于有队列插入的时间,即便执行时间从1000改为0,仍是输出四个4。
那么这个问题是如何产生和解决的呢?请接着阅读
执行到异步任务的时候,会直接放到异步队列中吗? 答案是不必定的。
由于浏览器有个定时器(timer)模块,定时器到了执行时间才会把异步任务放到异步队列。
for循环体执行的过程当中并无把setTimeout放到异步队列中,只是交给定时器模块了。4个循环体执行速度很是快(不到1毫秒)。定时器到了设置的时间才会把setTimeout语句放到异步队列中。
即便setTimeout设置的执行时间为0毫秒,也按4毫秒算。
这就解释了上题为何会连续输出四个4的缘由。
HTML5 标准规定了
setTimeout()
的第二个参数的最小值,即最短间隔,不得低于4毫秒。若是低于这个值,就会自动增长。在此以前,老版本的浏览器都将最短间隔设为10毫秒。
for (let i = 0; i < 4; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000 * i)
})(i);
}
复制代码
执行后,会隔1s输出一个值,分别是:0 1 2 3
IIFE
声明即执行的函数表达式来解决闭包形成的问题。这里也能够用setInterval()
方法来实现间歇调用。
var output = function (i) {
setTimeout(function () {
console.log(i);
}, 1000 * i)
}
for (let i = 0; i < 4; i++) {
output(i);
}
复制代码
执行后,会隔1s输出一个值,分别是:0 1 2 3
实现原理:传过去的i
值被复制了。
const tasks = [];
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(i);
resolve();
}, 1000 * i);
});
//生成所有的异步操做
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
//同步操做完成后,输出最后的i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i);
}, 1000)
})
复制代码
执行后,会隔1s输出一个值,分别是:0 1 2 3 4 5
优势:提升了代码的可读性。
注意:若是没有处理Promise的reject,会致使错误被丢进黑洞。
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { //声明即执行的async
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(i);
}
await sleep(1000);
console.log(i);
})();
复制代码
执行后,会隔1s输出一个值,分别是:0 1 2 3 4 5
主线程从任务队列中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop。
有时候 setTimeout明明写的延时3秒,实际却5,6秒才执行函数,这又是由于什么?
答:setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤仍是空闲。
浏览器的JS引擎遇到setTimeout,拿走以后不会当即放入异步队列,同步任务执行以后,timer模块会到设置时间以后放到异步队列中。js引擎发现同步队列中没有要执行的东西了,即运行栈空了就从异步队列中读取,而后放到运行栈中执行。因此setTimeout可能会多了等待线程的时间。
这时setTimeout函数体就变成了运行栈中的执行任务,运行栈空了,再监听异步队列中有没有要执行的任务,若是有就继续执行,如此循环,就叫Event Loop。
JavaScript经过事件循环和浏览器各线程协调共同实现异步。同步能够保证顺序一致,可是容易致使阻塞;异步能够解决阻塞问题,可是会改变顺序性。
知识点梳理:
最后,但愿你们阅后有所收获。🤠