一, 引言
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
异步函数: 如果在函数返回的时候, 调用者还不能够得到预期结果, 而是需要在将来通过一定的手段得到, 那么这个函数就是异步的.
宏任务
- I/O
- setTimeout
- setInterval
- setImmdiate
- requestAnimationFrame
微任务
- process.nextTick
- MutationObserver
- Peomise.then catch finally
一次时间循环中, 微任务总是先于宏任务执行. 在当前的微任务没有执行完成时, 是不会执行下一个宏任务的.
三, 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);
- }
输出结果: 4 4 4 4
for 循环是一个同步任务, 为什么连续输出四个 4? 因为有队列插入的时间 执行时间从 1000 改成 0, 还是四个 4.
那么这个问题是如何产生和解决的呢? 请接着阅读
异步队列执行的时间
执行到异步任务的时候, 会直接放到异步队列中吗? 答案是不一定的. 因为浏览器有个 timer 模块, 定时器到了执行时间才会把异步任务放到异步队列. for 循环体执行的过程中并没有把 setTimeout 放到异步队列中, 只是交给定时器模块了. 4 个循环体执行速度非常快(不到 1 毫秒). 定时器到了设置的时间才会把 setTimeout 语句放到异步队列中, 即使 setTimeout 设置的执行时间为 0 毫秒, 也按 4 毫秒算. 这就解释了例 5 为什么会连续输出四个 4 的原因.
html5 标准规定了 setTimeout() 的第二个参数的最小值, 即最短间隔, 不得低于 4 毫秒. 如果低于这个值, 就会自动增加. 在此之前, 老版本的浏览器都将最短间隔设为 10 毫秒.
利用闭包实现 setTimeout 间歇调用
- for (let i = 0; i < 4; i++) {
- (function (j) {
- setTimeout(function () {
- console.log(j);
- }, 1000*i)
- })(i);
- }
会输出: 0 1 2 3
此方法巧妙利用 IIFE 声明即执行的函数表达式来解决闭包造成的问题.
将 var 改为 let, 使用了 ES6 语法. 这里也可以用 setInterval 方法来实现间歇调用. 详见: setTimeout 和 setInterval 的区别 https://www.jianshu.com/p/7ff04374fcea
利用 JS 中基本类型的参数传递是按值传递的特征实现
- var output = function (i) {
- setTimeout(function () {
- console.log(i);
- }, 1000*i)
- }
- for (let i = 0; i < 4; i++) {
- output(i);
- }
实现原理: 传过去的 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)
- })
优点: 提高了代码的可读性 注意: 如果没有处理 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);
- })();
六, 事件循环 Event Loop
主线程从 "任务队列" 中读取事件, 这个过程是循环不断的, 所以整个的这种运行机制又称为 Event Loop.
有时候 setTimeout 明明写的延时 3 秒, 实际却 5,6 秒才执行函数, 这又是因为什么?
答: setTimeout 并不能保证执行的时间, 是否及时执行取决于 JavaScript 线程是拥挤还是空闲. 浏览器的 JS 引擎遇到 setTimeout, 拿走之后不会立即放入异步队列, 同步任务执行之后, timer 模块会到设置时间之后放到异步队列中. JS 引擎发现同步队列中没有要执行的东西了, 即运行栈空了就从异步队列中读取, 然后放到运行栈中执行. 这时 setTimeout 函数体就变成了运行栈中的同步任务, 运行栈空了, 再监听异步队列中有没有任务, 如果有就继续执行, 如此循环, 就叫 Event Loop.
七, 总结
JavaScript 通过事件循环和浏览器各线程协调共同实现异步. 同步可以保证顺序一致, 但是容易导致阻塞; 异步可以解决阻塞问题, 但是会改变顺序性.
知识点梳理:
理解 JS 的单线程的概念: 一段时间内做一件事
理解任务队列: 同步任务, 异步任务
理解 Event Loop
理解哪些语句会放入异步任务队列
理解语句放入异步任务队列的时机
最后, 希望大家阅后有所收获.
来源: https://juejin.im/post/5c94e1515188252d8d190d4c