如何通过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 相关文章推荐
谈谈关于JavaScript 中的 MVC 模式
Apr 11 Javascript
解决jquery1.9不支持browser对象的问题
Nov 13 Javascript
js阻止冒泡及jquery阻止事件冒泡示例介绍
Nov 19 Javascript
JS中frameset框架弹出层实例代码
Apr 01 Javascript
AngularJS基础 ng-open 指令简单实例
Aug 02 Javascript
详解RequireJs官方使用教程
Oct 31 Javascript
JavaScript基础之静态方法和实例方法分析
Dec 26 Javascript
vue+elementUI实现表单和图片上传及验证功能示例
May 14 Javascript
基于vue如何发布一个npm包的方法步骤
May 15 Javascript
使用Vue Composition API写出清晰、可扩展的表单实现
Jun 10 Javascript
解决vue-router路由拦截造成死循环问题
Aug 05 Javascript
Taro小程序自定义顶部导航栏功能的实现
Dec 17 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
PHP5 字符串处理函数大全
2010/03/23 PHP
从零开始学YII2框架(五)快速生成代码工具 Gii 的使用
2014/08/20 PHP
PHP基于MySQL数据库实现对象持久层的方法
2015/06/17 PHP
PHP中把错误日志保存在系统日志中(Windows系统)
2015/06/23 PHP
php+redis实现多台服务器内网存储session并读取示例
2017/01/12 PHP
PHP基于phpqrcode类生成二维码的方法详解
2018/03/14 PHP
php5对象复制、clone、浅复制与深复制实例详解
2019/08/14 PHP
用nodejs实现PHP的print_r函数代码
2014/03/14 NodeJs
javascript修改IMG标签的src问题
2014/03/28 Javascript
原创jQuery弹出层插件分享
2015/04/02 Javascript
javascript转换静态图片,增加粒子动画效果
2015/05/28 Javascript
在JavaScript中操作时间之getUTCDate()方法的使用
2015/06/10 Javascript
JavaScript File API文件上传预览
2016/02/02 Javascript
AngularJS学习第一篇 AngularJS基础知识
2017/02/13 Javascript
Angular4实现鼠标悬停3d倾斜效果
2017/10/25 Javascript
js通过Date对象实现倒计时动画效果
2017/10/27 Javascript
mint-ui在vue中的使用示例
2018/04/05 Javascript
微信实现自动跳转到用其他浏览器打开指定APP下载
2019/02/15 Javascript
如何基于vue-cli3.0构建功能完善的移动端架子
2019/04/24 Javascript
vue-cli3配置与跨域处理方法
2019/08/17 Javascript
Vue数字输入框组件示例代码详解
2020/01/15 Javascript
python 读写、创建 文件的方法(必看)
2016/09/12 Python
python模仿网页版微信发送消息功能
2018/02/24 Python
Python加载带有注释的Json文件实例
2018/05/23 Python
Python开发最牛逼的IDE——pycharm
2018/08/01 Python
Django框架基础模板标签与filter使用方法详解
2019/07/23 Python
Python可变对象与不可变对象原理解析
2020/02/25 Python
Django使用Profile扩展User模块方式
2020/05/14 Python
Html5基于canvas实现电子签名并生成PDF文档
2020/12/07 HTML / CSS
如何进行Linux分区优化
2013/02/12 面试题
我的动漫时代的创业计划书范文
2014/01/27 职场文书
气象学专业个人求职信
2014/04/22 职场文书
员工试用期自我评价
2014/09/18 职场文书
师德师风建设整改措施思想汇报
2014/10/11 职场文书
班主任工作实习计划
2015/01/16 职场文书
黄石寨导游词
2015/02/05 职场文书