JS promise 的回调和 setTimeout 的回调到底谁先执行

目录
  • 任务 VS 微任务
  • 执行过程
  • 案例分析
  • 结语 & 参考资料

首先提一个小问题:运行下面这段 JS 代码后控制台的输出是什么?

console.log("script start");

setTimeout(function () {
  console.log("setTimeout1");
}, 0);

new Promise((resolve, reject) => {
  setTimeout(function () {
    console.log("setTimeout2");
    resolve();
  }, 100);
}).then(function () {
  console.log("promise1");
});

Promise.resolve()
  .then(function () {
    console.log("promise2");
  })
  .then(function () {
    console.log("promise3");
  });

console.log("script end");

可以先尝试自己分析一下结果,然后再看答案:

script start
script end
promise2
promise3
setTimeout1
setTimeout2
promise1

怎么样,你猜对了吗?如果对这个输出结果感到很迷惑,这篇文章或许可以帮到你。

PS:文中按照标准分析理论结果,但实际上各个浏览器对任务队列的支持情况很混乱,所以如果你在浏览器执行代码后发现结果不同也不必纠结;总体来说 Chrome 的支持比较好。

如果对 Promise 的用法还不熟悉,可以看我的上一篇博客:前端 | JS Promise:axios 请求结果后面的 .then() 是什么意思?

任务 VS 微任务

JavaScript 设计的本质是单线程语言,但随着硬件性能的飞速发展,纯单线程已经不太能够满足需求了。因此 JS 逐渐发展出了任务和微任务,来模拟实现多线程。

浏览器中,对于每个网页(有时也可能是多个同源网页),网页的代码和浏览器自身的用户界面程序运共享同一个主线程,它除了运行浏览器交给它的 JS 代码,也负责收集和派发事件、渲染和绘制网页内容等等。因此,如果主线程中的某个任务阻塞了,其他任务都会受到影响;这就是为什么有时候网页代码出现了错误会导致整个网页渲染失败。

每个主线程都由一个事件循环 Event loops 驱动。事件循环可以理解为一个任务队列,JS 引擎不断的进行“循环-等待”,按顺序处理队列中的任务。事件循环中的任务称作“任务 Task”,由宿主环境(浏览器)创建;每个任务都是宿主计划执行的 JavaScript 代码,如程序初始化、解析HTML、事件触发的回调(例如点击网页上的按钮),或是由 setTimeout() setInterval() 等 API 添加的回调函数。

JS 引擎在执行一个任务的过程中,有时会进行一些异步操作,不会立即执行,但又想在同一个任务中完成、不留到事件循环中的下一个任务里;例如常用的 promise、监控 DOM 的回调等。这时,JS 引擎会创建一个“微任务 Mircotask”,并加入当前的微任务队列中。(有时为了区分,也把任务task称为“宏任务”。)

事件循环、任务、微任务的示意图如下:

执行过程

一个主线程的执行过程如下:

  • 拿出事件循环中的下一个任务
  • 执行任务本身的 Script 代码;期间可能会往任务队列、微任务队列创建添加新任务
  • script 执行完后,检查微任务队列
    • 如果有微任务,顺序执行,期间可能还会创建新的任务和微任务
    • 如果微任务队列为空,这个任务执行结束,回到第一步

可以看出,在一个任务中会反复检查微任务队列,直到没有微任务存在了才会执行下一个任务。因此在任务脚本和微任务脚本中创建的所有微任务都会在这个任务结束前执行,同时也意味着会早于其他所有创建的任务执行(因为新建的任务都加入了任务队列)。

案例分析

明白了任务和微任务的区别,下面再来看文章开头的例子:

console.log("script start");

setTimeout(function () {
  console.log("setTimeout1");
}, 0);

new Promise((resolve, reject) => {
  setTimeout(function () {
    console.log("setTimeout2");
    resolve();
  }, 100);
}).then(function () {
  console.log("promise1");
});

Promise.resolve()
  .then(function () {
    console.log("promise2");
  })
  .then(function () {
    console.log("promise3");
  });

console.log("script end");

接下来逐步跟踪代码的执行过程;如果感觉文字不够直观,可以看这篇博客中给出的逐步执行动画。

整个 Script 会被宿主环境传给 JS 引擎,作为任务队列中的一个任务;首先执行任务中的脚本代码:

  • line1: console.log("script start") 是同步代码,直接输出
  • line3: 执行 setTimeout(),在0秒后将 console.log("setTimeout1"); 加入任务队列
  • line8: 执行 setTimeout(),在0.1秒后将 console.log("setTimeout2"); 和 resolve() 加入任务队列
  • line16: 返回一个已成功的 promise,第一个 then 回调被加入微任务队列
  • line24: console.log("script end") 是同步代码,直接输出
  • 任务 script 执行完毕

此时:

  • 控制台输出了 script start script end
  • 任务队列中(除当前任务以外)有2个任务(两个 setTimeout() 的回调按时间先后顺序排列)
  • 微任务队列中有1个任务(promise 的回调)

接下来检查微任务队列,执行队首的微任务:

  • console.log("promise2") 输出
  • 隐式 return,相当于返回一个 Promise.resolve(undefined);因此 Promise 链中的下一个 then 回调被加入微任务队列
  • 微任务执行完毕

此时:

  • 控制台输出了 script start script end promise2
  • 任务队列中(除当前任务以外)有2个任务(两个 setTimeout() 的回调按时间先后顺序排列)
  • 微任务队列中有1个任务(第二个 promise 回调)

再次检查微任务队列,执行队首的微任务:

  • console.log("promise3") 输出
  • 隐式 return(但此时 Promise 链已经结束了,所以无事发生)
  • 微任务执行完毕

此时:

  • 控制台输出了 script start script end promise2 promise3
  • 任务队列中(除当前任务以外)有2个任务(两个 setTimeout() 的回调按时间先后顺序排列)
  • 微任务队列为空

检查微任务队列,发现没有微任务了,当前任务结束;开始执行任务队列中的下一个任务(0秒后执行的回调):

  • console.log("setTimeout1"); 输出
  • 任务 script 执行完毕

此时:

  • 控制台输出了 script start script end promise2 promise3 setTimeout1
  • 任务队列中(除当前任务以外)有1个任务
  • 微任务队列为空

检查微任务队列,发现没有微任务,当前任务结束;开始执行任务队列中的下一个任务(0.1秒后执行的回调):

  • console.log("setTimeout2"); 输出
  • resolve(); 将 promise 的状态更改为已成功;then 回调被加入微任务队列
  • 任务 script 执行完毕

此时:

  • 控制台输出了 script start script end promise2 promise3 setTimeout1 setTimeout2
  • 任务队列中只有当前任务
  • 微任务队列中有一个任务(promise 的回调)

检查微任务队列,执行队首的微任务:

  • console.log("promise1") 输出
  • 隐式 return(但此时 Promise 链已经结束了,所以无事发生)
  • 微任务执行完毕

此时:

  • 控制台输出了 script start script end promise2 promise3 setTimeout1 setTimeout2 promise1
  • 任务队列中只有当前任务
  • 微任务队列为空

检查微任务队列,发现没有微任务,当前任务结束。任务队列中没有其他任务,执行完毕。

结语 & 参考资料

异步操作已经是平时开发过程中不可避免经常会遇到的用法了,平时都是马马虎虎的用,最近终于认真学习了一下,感觉颇有收获。不过话说回来,理论学习和实际开发毕竟存在差异。首先各种浏览器的支持只能说是惨不忍睹,所以真实开发过程中不能太过依赖理论分析的结果,需要实际测试代码功能的兼容性;另一方面,过于复杂的嵌套异步操作,容易造成没必要的错误,同时导致代码很难理解和维护,能不用最好不用,KISS。

以上是个人学习JS的任务/微任务机制时的一些思考和总结,希望能对你有所帮助;文中可能存在疏漏和错误,敬请讨论和指正。

Tasks, microtasks, queues and schedules

深入:微任务与Javascript运行时环境

到此这篇关于JS promise 的回调和 setTimeout 的回调到底谁先执行 的文章就介绍到这了,更多相关JS promise 的回调和 setTimeout 的回调执行 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript异步回调的Promise模式封装实例

    网页的交互越来越复杂,JavaScript 的异步操作也随之越来越多.如常见的 ajax 请求,需要在请求完成时响应操作,请求通常是异步的,请求的过程中用户还能进行其他的操作,不会对页面进行阻塞,这种异步的交互效果对用户来说是挺有友好的.但是对于开发者来说,要大量处理这种操作,就很不友好了.异步请求完成的操作必须预先定义在回调函数中,等到请求完成就必须调用这个函数.这种非线性的异步编程方式会让开发者很不适应,同时也带来了诸多的不便,增加了代码的耦合度和复杂性,代码的组织上也会很不优雅,大大降低了

  • JS动画实现回调地狱promise的实例代码详解

    1. js实现动画 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>animate</title> <style> .ball { width: 40px; height: 40px; margin-bottom: 5px; border-radius: 20px; } .ball1 { ba

  • 如何将Node.js中的回调转换为Promise

    前言 在几年前,回调是 JavaScript 中实现执行异步代码的唯一方法.回调本身几乎没有什么问题,最值得注意的是"回调地狱". 在 ES6 中引入了 Promise 作为这些问题的解决方案.最后通过引入   async/await 关键字来提供更好的体验并提高了可读性. 即使有了新的方法,但是仍然有许多使用回调的原生模块和库.在本文中,我们将讨论如何将 JavaScript 回调转换为 Promise.ES6 的知识将会派上用场,因为我们将会使用 展开操作符之类的功能来简化要做的事

  • JS promise 的回调和 setTimeout 的回调到底谁先执行

    目录 任务 VS 微任务 执行过程 案例分析 结语 & 参考资料 首先提一个小问题:运行下面这段 JS 代码后控制台的输出是什么? console.log("script start"); setTimeout(function () { console.log("setTimeout1"); }, 0); new Promise((resolve, reject) => { setTimeout(function () { console.log(&

  • 详解promise.then,process.nextTick, setTimeout 以及 setImmediate的执行顺序

    本文介绍了详解promise.then,process.nextTick, setTimeout 以及 setImmediate的执行顺序,分享给大家,具体如下: 先举一个比较典型的例子: setImmediate(function(){ console.log(1); },0); setTimeout(function(){ console.log(2); },0); new Promise(function(resolve){ console.log(3); resolve(); conso

  • 浅谈js promise看这篇足够了

    一.背景 大家都知道nodejs很快,为什么会这么快呢,原因就是node采用异步回调的方式来处理需要等待的事件,使得代码会继续往下执行不用在某个地方等待着.但是也有一个不好的地方,当我们有很多回调的时候,比如这个回调执行完需要去执行下个回调,然后接着再执行下个回调,这样就会造成层层嵌套,代码不清晰,很容易进入"回调监狱",就容易造成下边的例子: async(1, function(value){ async(value, function(value){ async(value, fu

  • node.js Promise对象的使用方法实例分析

    本文实例讲述了node.js Promise对象的使用方法.分享给大家供大家参考,具体如下: Promise对象是干嘛用的? 将异步操作以同步操作的流程表达出来 一.Promise对象的定义 let flag = true; const hello = new Promise(function (resolve, reject) { if (false) {//异步操作成功 resolve("success"); } else { reject("error");

  • js Promise并发控制数量的方法

    目录 问题 背景 思路 & 实现 问题 要求写一个方法控制 Promise 并发数量,如下: promiseConcurrencyLimit(limit, array, iteratorFn) limit 是同一时间执行的 promise 数量,array 是参数数组,iteratorFn 每个 promise 中执行的异步操作. 背景 开发中需要在多个promise处理完成后执行后置逻辑,通常使用Promise.all: Primise.all([p1, p2, p3]).then((res)

  • JS Promise axios 请求结果后面的.then() 是什么意思

    目录 Promise 对象 Promise 对象的状态 回调函数 Promise.then() 绑定回调函数 使用 Promise:链式调用 链式调用的实现 错误处理 常见错误 创建 Promise 对象 Promise 其他静态方法 创建已决议的 Promise 对象 多个 Promise 对象 结语&参考文献 Promise 是JS中一种处理异步操作的机制,在现在的前端代码中使用频率很高.Promise 这个词可能有点眼生,但你肯定见过 axios.get(...).then(res =>

  • 解决火狐浏览器下JS setTimeout函数不兼容失效不执行的方法

    今天检查自己用JQuery+AJAX+PHP做的网站后台登录检测,发现登陆成功后执行页面跳转函数这段JavaScript(JS)代码特效在IE和谷歌浏览器Chrome下都可以很好地执行,兼容性还不错.结果到了火狐(FireFox)浏览器下setTimeout这个JS内置函数不执行了,无效了,也没报错!打开FireBUG指望它能检测出JS的错误,结果没用...Javascript(JS)脚本代码在各浏览器下的兼容是一个很头疼的问题,经过一番调试和搜索,终于解决了setTimeout这个JS代码在火

  • js实现漂浮回顶部按钮实例

    本文实例讲述了js实现漂浮回顶部按钮的方法.分享给大家供大家参考.具体实现方法如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <

  • 深入理解js promise chain

    新的标准里增加了原生的Promise. 这里只讨论链式使用的情况,思考一下其中的细节部分. 一,关于 then() 和 catch() 的复习 then() 和 catch() 的参数里可以放置 callback 函数用来接收一个 Promise的最终结果. then() 可以接收一个参数,那么这个 callback 只会在 Promise resolve() 的时候被调用. then() 还可以接收第二个参数,那么第二个 callback 用来处理 Promise reject() 的情况.

  • js中的setInterval和setTimeout使用实例

    setInterval() 定义和用法 setInterval() 方法可按照指定的周期(以毫秒计)来执行函数或表达式.该方法会不停地循环调用函数,直到使用 clearInterval() 明确停止该函数或窗口被关闭.clearInterval() 函数的参数即 setInterval() 返回的 ID 值. 语法 setInterval(code,millisec[,"lang"])code 必需.要调用的函数或要执行的代码串.millisec 必须.周期性执行或调用 code 之间

随机推荐