如何通过setTimeout理解JS运行机制详解


Posted in Javascript onMarch 23, 2019


setTimeout()函数:用来指定某个函数或某段代码在多少毫秒之后执行。它返回一个整数,表示定时器timer的编号,可以用来取消该定时器。

例子

console.log(1);
setTimeout(function () {
 console.log(2);
}, 0);
console.log(3);

问:最后的打印顺序是什么?(如果不了解js的运行机制就会答错)

正确答案:1 3 2

解析:无论setTimeout的执行时间是0还是1000,结果都是先输出3后输出2,这就是面试官常常考查的js运行机制的问题,接下来我们要引入一个概念,JavaScript 是单线程的。

二、 JavaScript 单线程

JavasScript引擎是基于事件驱动和单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行程序,即主线程。

通俗的说:JS在同一时间内只能做一件事,这也常被称为 “阻塞式执行”。

任务队列

那么单线程的JavasScript是怎么实现“非阻塞执行”呢?

答:异步容易实现非阻塞,所以在JavaScript中对于耗时的操作或者时间不确定的操作,使用异步就成了必然的选择。
诸如事件点击触发回调函数、ajax通信、计时器这种异步处理是如何实现的呢?

答:任务队列

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

任务队列:一个先进先出的队列,它里面存放着各种事件和任务。

同步任务

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

  • 输出
  • 如:console.log()
  • 变量的声明
  • 同步函数:如果在函数返回的时候,调用者就能够拿到预期的返回值或者看到预期的效果,那么这个函数就是同步的。

异步任务

  • setTimeout和setInterval
  • DOM事件
  • Promise
  • process.nextTick
  • fs.readFile
  • http.get
  • 异步函数:如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。

除此之外,任务队列又分为macro-task(宏任务)与micro-task(微任务),在ES5标准中,它们被分别称为task与job。

宏任务

  1. I/O
  2. setTimeout
  3. setInterval
  4. setImmdiate
  5. requestAnimationFrame

微任务

  1. process.nextTick
  2. Promise
  3. Promise.then
  4. MutationObserver

宏任务和微任务的执行顺序

一次事件循环中,先执行宏任务队列里的一个任务,再把微任务队列里的所有任务执行完毕,再去宏任务队列取下一个宏任务执行。

注:在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

三、setTimeout运行机制

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毫秒。

利用闭包实现 setTimeout 间歇调用

for (let i = 0; i < 4; i++) {
 (function (j) {
 setTimeout(function () {
  console.log(j);
 }, 1000 * i)
 })(i);
}

执行后,会隔1s输出一个值,分别是:0 1 2 3

  • 此方法巧妙利用IIFE声明即执行的函数表达式来解决闭包造成的问题。
  • 将var改为let,使用了ES6语法。

这里也可以用setInterval()方法来实现间歇调用。

详见:setTimeout和setInterval的区别

利用JS中基本类型的参数传递是按值传递的特征实现

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值被复制了。

基于Promise的解决方案

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,会导致错误被丢进黑洞。

使用ES7中的async await特性的解决方案(推荐)

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理解JS运行机制详解

主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop。

有时候 setTimeout明明写的延时3秒,实际却5,6秒才执行函数,这又是因为什么?

答:setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。

浏览器的JS引擎遇到setTimeout,拿走之后不会立即放入异步队列,同步任务执行之后,timer模块会到设置时间之后放到异步队列中。js引擎发现同步队列中没有要执行的东西了,即运行栈空了就从异步队列中读取,然后放到运行栈中执行。所以setTimeout可能会多了等待线程的时间。

这时setTimeout函数体就变成了运行栈中的执行任务,运行栈空了,再监听异步队列中有没有要执行的任务,如果有就继续执行,如此循环,就叫Event Loop。

七、总结

JavaScript通过事件循环和浏览器各线程协调共同实现异步。同步可以保证顺序一致,但是容易导致阻塞;异步可以解决阻塞问题,但是会改变顺序性。

知识点梳理:

  • 理解JS的单线程的概念:一段时间内做一件事
  • 理解任务队列:同步任务、异步任务
  • 理解 Event Loop
  • 理解哪些语句会放入异步任务队列
  • 理解语句放入异步任务队列的时机

最后,希望大家阅后有所收获。?

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
jQuery 位置函数offset,innerWidth,innerHeight,outerWidth,outerHeight,scrollTop,scrollLeft
Mar 23 Javascript
Jquery:ajax实现翻页无刷新功能代码
Aug 05 Javascript
jquery重复提交请求的原因浅析
May 23 Javascript
javascript实现校验文件上传控件实例
Apr 20 Javascript
使用javascript将时间转换成今天,昨天,前天等格式
Jun 25 Javascript
Angularjs中$http以post请求通过消息体传递参数的实现方法
Aug 05 Javascript
JS用斜率判断鼠标进入DIV四个方向的方法
Nov 07 Javascript
BootStrap按钮标签及基本样式
Nov 23 Javascript
详解Node.js串行化流程控制
May 04 Javascript
详解用Webpack与Babel配置ES6开发环境
Mar 12 Javascript
vue自定义树状结构图的实现方法
Oct 18 Javascript
javascript实现放大镜功能
Dec 09 Javascript
vue中axios请求的封装实例代码
Mar 23 #Javascript
vueScroll实现移动端下拉刷新、上拉加载
Mar 22 #Javascript
浅谈Angular单元测试总结
Mar 22 #Javascript
JavaScript面试技巧之数组的一些不low操作
Mar 22 #Javascript
Vue-CLI 3.X 部署项目至生产服务器的方法
Mar 22 #Javascript
微信小程序城市选择及搜索功能的方法
Mar 22 #Javascript
使用node搭建自动发图文微博机器人的方法
Mar 22 #Javascript
You might like
PHP简介
2006/10/09 PHP
phpmyadmin的#1251问题
2006/11/25 PHP
php.ini 配置文件的深入解析
2013/06/17 PHP
基于simple_html_dom的使用小结
2013/07/01 PHP
ThinkPHP实现将本地文件打包成zip下载
2014/06/26 PHP
php学习笔记之面向对象
2014/11/08 PHP
一个简单至极的PHP缓存类代码
2015/10/23 PHP
JavaScript使用过程中需要注意的地方和一些基本语法
2010/08/26 Javascript
js实现网页标题栏闪烁提示效果实例分析
2014/11/20 Javascript
创建js对象和js类的方法汇总
2014/12/24 Javascript
详解jQuery中的empty、remove和detach
2016/04/11 Javascript
jquery树形菜单效果的简单实例
2016/06/06 Javascript
微信小程序 开发指南详解
2016/09/27 Javascript
JS判断输入的字符串是否是数字的方法(正则表达式)
2016/11/29 Javascript
jQuery加载及解析XML文件的方法实例分析
2017/01/22 Javascript
Node.js使用Angular简单示例
2018/05/11 Javascript
JavaScript中常用的简洁高级技巧总结
2019/03/10 Javascript
微信JS-SDK updateAppMessageShareData安卓不能自定义分享详解
2019/03/29 Javascript
vue+iview实现分页及查询功能
2020/11/17 Vue.js
[01:07:19]DOTA2-DPC中国联赛 正赛 CDEC vs XG BO3 第一场 1月19日
2021/03/11 DOTA
简单的通用表达式求10乘阶示例
2014/03/03 Python
Python Sleep休眠函数使用简单实例
2015/02/02 Python
Python 冒泡,选择,插入排序使用实例
2015/02/05 Python
python实现自动更换ip的方法
2015/05/05 Python
解决Mac下首次安装pycharm无project interpreter的问题
2018/10/29 Python
python字符串中匹配数字的正则表达式
2019/07/03 Python
python画双y轴图像的示例代码
2019/07/07 Python
pymysql之cur.fetchall() 和cur.fetchone()用法详解
2020/05/15 Python
Html5实现移动端、PC端 刮刮卡效果
2016/06/30 HTML / CSS
手对手的教你用canvas画一个简单的海报的方法示例
2018/12/10 HTML / CSS
教育实习生的自我评价分享
2013/11/21 职场文书
结对共建协议书
2014/08/20 职场文书
2014年大学宣传部工作总结
2014/12/19 职场文书
2016应届毕业生实习心得体会
2015/10/09 职场文书
导游词之平津战役纪念馆
2019/11/04 职场文书
SQL解决未能删除约束问题drop constraint
2022/05/30 SQL Server