前言: 上一篇我们知道了 “回调地狱” 是如何产生的。并且成功引入了 Promise 的核心知识。距离 《手写 Promise》 又更近了一步,但是我们仍需要去理解更深层次的概念----- 宏任务和微任务 ,才能继续往下讲解。
我非常希望你能在阅读完前置任务后再往下阅读本文(0/3)
我的每一篇博文的出发点都是:我是在面向一个初学者来进行讲解的,所以我在阐述某些知识点的时候,不会写一些很高深的话来凸显自己的水平,都会以:“假如我是一个初学者,当时如果有人这样告诉我的话该有多好~” 这也是费曼学习法的观点。
Promise 在实际开发过程中是百分百最经常使用的,所以如果你是以找工作为目的的学习,我希望你能好好的理解这几篇文章内容,真心希望大家能在我的博文中领悟到一些东西。🎁
<hr/>
一. 做个 demo 热热身
- 别着急,我们先回顾一下。之前在讲 JS代码的运行机制时,我们大概知道
setTimeout
会将自身参数,也就是传入的回调函数推进一个任务队列,然后等待主线程执行完毕后再将任务队列中的代码推进主线程去执行。并且会无限循环这个过程,这个过程被我们称为 “事件循环(eventloop)” 。 - 下面的代码,我相信你已经知道原因了。
- ok,我们现在看下一段代码。思考一下,你觉得会是?
结果如下:
如果你没感觉到差异,小伙子水平不错哦~ 👏 - 不理解别着急,我来解释一下原因是什么。首先
setTimout('1')
的输出肯定被放进了 任务队列 ,所以console.log('3')
的输出结果 3 是肯定要在 1 之前,这个我相信你肯定知道原因。关键就在于为什么在 Promise 这里的 2 会在第一输出。 - 下面我不写箭头函数了,我害怕箭头函数会让初学者看不出来这就是一个函数而已。
没错,它真的就是一个普普通通的函数。如果你对 Class 熟悉的话,它其实就是下面的写法。
我再强调一遍,这个 “executor” 构造器函数就是一个普普通通的函数仅此而已,不是什么关键词!!!就是一个函数。当这个函数作为 Promise 类的参数时,会被在 Promise 类的 constructor 函数中马上被调用。 - 所以我们可以得出,传递给 Promise类 的回调函数,会被马上执行。它被并没有被推进任务队列中去。
又因为它是在console.log(3)
之前写的,根据我们 JS 代码从上往下执行的原理,自然而然的结果就是2->3->1
- 是不是觉得很简单呢?我们稍微加一点难度,看下面一段代码。
别直接想结果,你想一想控制台会是什么样子的?
奇怪?我们的 2 跑哪里了?如果你不知道这一点,我希望你回过头去看一下上一篇- 手写“回调地狱”,再来继续下面的学习。 - 从上一章节我们其实我们知道,我们如果要取 Promise,resolve 保存的数据,需要在 Promise 实例身上的
then
方法中去拿,由此可知我们需要按照下面的写法才能拿到数字 2。 - 那么问题来了,接下来的输出顺序会是?我们打印一下:
怎么回事呢?怎么顺序就又变了呢?🤔
二. 宏任务和微任务(MacroTask & MicroTask)
setTimout
的回调函数是会被放在任务队列的对吧?我们都知道任务队列里的任务会乖乖排队,等着主线程老大执行完再进去主线程去执行。关键点就来了!这里有个插队的!!没错,就是我们的微任务队列。- 是的,在 JS 里其实存在另外一个任务队列---微任务队列(MicroTask)。我们的
setTimout
的回调函数其实被放进了一个叫宏任务队列里。为什么会产生两个队列呢?其实很好理解。 - 我们假设一个场景。我们去饭店吃饭。我们总是会把菜点完,然后把写着好几个菜的菜单拿给服务员,最好服务员一起送过去厨房做对吧?这时候有意思的点就来了,厨师👨🍳拿到菜单以后,一看《土豆炖牛肉》,《红烧肉》,《鸡蛋汤》。我感觉如果是一个正常的厨师师傅,都会给做菜顺序安排合理。“我不可能一看,第一行《土地炖牛肉》,后面的我压根不看,我就必须把这个菜先做了才能做后面的。”结果第一道菜50多分钟,客人干等着50分钟。
- 这时候就把《土地炖牛肉》开启高压锅放进了宏任务队列,转手把《鸡蛋汤》和《红烧肉》放进主线程去做。
- 当《土豆炖牛肉》快好的前几分钟,(注意!:这时候红烧肉和鸡蛋汤已经吃饱喝足被消灭了) 你突然又点了一个《拍黄瓜》🥜。师傅一看,《拍黄瓜》简单啊,虽然这个菜是最后上的,但是师傅直接给你放《微任务》啪啪啪几分钟给你搞好了,速度肯定比《土豆炖牛肉》剩下几分钟还要快,自然而然你就先吃到了《拍黄瓜》。
- 同理,在 JS 中,我们去拿一些页面相对更重要数据的时候,就是需要去“插“一些相对不那么“重要”的宏任务的队,才能保证我们页面的正确加载。而很显然,我们的 Promise 就是需要存放相对重要的数据时用得,所以它需要比
setTimeout
这个宏任务队列在事件循环的时候先执行。所以事件循环会是下面的顺序。
如果没有的话,再去询问宏任务队列 - 了解了这些以后,我们再回过头看一下之前的代码,就很清晰了。
首先控制台先执行同步任务console.log('3')
,之后主线程为空。然后紧接着就去询问微任务队列,console.log('2')
,微任务队列执行完毕,最后询问宏任务队列。console.log('1')
。
结果输出结果就如下所示:
三. 不使用 Promise 创建一个微任务
- 到这里我们可能会有疑问。我们可以直接像
setTimeout
那样很简单的创建一个宏任务一样创建微任务吗?你别说,还真有它就是挂在window
对象身上的一个方法----queueMicrotask。 - 别觉得这个单词这么长就害怕,其实理解起来非常简单。微软公司怎么拼?MicroSoft对吧,那微任务不就是 MicorTask 吗?
- 使用方法非常简单,这个函数接受一个回调函数作为参数。和
setTimeout
一模一样的使用方法。
我们看一下控制台输出结果
四. 趁热打铁
- 是不是感觉好像悟了什么?哪我考考你,下面代码的运行结果是?
- 其实这个问题的关键点就在于这两个函数谁先执行?
- 这里我直接揭晓答案,其实不管是微任务还是宏任务,当队列中有多个任务在排队的时候,都遵循先进先出。就和现实生活排队一样,我先进去队里排队,自然而然我就是要先执行的。上面代码的执行顺序,先把
console.log("2")
放进了微任务队列,而 resolve("3") 是后一个微任务,那么自然在事件循环在主线程执行完回过头看微任务队列的时候,先看到的就是console.log("2")
,那么它就先执行。
所以控制台上正确的显示结果就是
五. 思考题
我们稍微调整一下上面的代码,我仅仅只改动了一丢丢代码。
请你回答出上面代码在控制台的输出结果,写在评论区~🎁
如果你能准确回答出,说明后面的手写 Promise 一定不是什么问题~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。