关于promise的知识一直不求甚解,只求能用。只是因为很多概念性的知识没有理解,经常一通胡乱操作。事实证明只了解api,会丢失很多抽象的细节。
Promise诞生的背景
JavaScript是单线程执行的,指的就是一个进程里只有一个主线程。也就是说JS引擎在同一时间内只能做一件事。仅就JavaScript自身来说,这样做实现更加简单和人脑工作方式接近,易于理解。但是在浏览器环境中,当我们加载一个网页时,如果有一张图片,下载需要30秒,那么在同步任务的模式下,必须等待加载图片的任务执行完毕后,才能做其他的任务,这显然不合理。好在在很早的时候,浏览器宿主环境就提供了一些js不具备的特性,DOM API, 定时器,HTTP请求等,可以用回调来实现异步,非阻塞的行为。但是过多的回调即不方便也不安全,给维护和调试带来了很大的麻烦。
在ES5以及更古早的版本中,JS引擎只用来执行任务,而执行的顺序完全由宿主环境(浏览器,nodejs)来决定!
直到ES6之后,js引入了Promise这个新特性,使得js引擎可以自己发起任务了
也就是说在ES6之前,尽管你能够编写出异步效果的代码,但直到ES6 JavaScript 才真正内建有直接的异步概念!在此之前 JavaScript引擎只是一台无情的任务执行机器,在什么时候执行什么代码完全由宿主环境说了算
PS:javascript 引擎不是独立运行的,它是运行在宿主环境中的,对多数人来说就是web浏览器和 nodejs,这些环境都有一个共同点,它们都提供了一种处理多个块执行的机制,且每个块在执行时都会调用js引擎,而这种机制就是我们所熟知的事件循环!!!
ES6 从本质上改变了在哪里管理事件循环,本来事件循环几乎已经是一种正式的技术模版了,但是ES6精准的指定了事件循环的工作细节,这意味着从技术上将事件循环纳入了 JavaScript引擎的势力范围,而不只是由宿主环境来决定,而这一改变的主要原因就是因为ES6中Promise的引入,这项技术要求能对事件循环的调度运行直接进行精准的控制
事件循环
我们已知JavaScript是单线程的,js在执行代码块时有两个比较重要的东西:执行栈,任务队列。这两种东西都是用来存储任务的,区别在于执行栈里存放的是按照顺序执行的同步任务,而任务队列里存放着异步任务
而任务队列又分为两种,我们把js引擎发起的任务叫做微任务(microtasks),把宿主环境发起的任务叫做宏任务(macrotasks)
- 宏任务(宿主环境发起):script标签中的整体的 JavaScript代码,setTimeout, setInterval, I/O, UI渲染,解析HTML,页面加载,输入,网络事件
- 微任务(JavaScript引擎发起):Promise.then(...)、MutationObserver、process.nextTick(node)
注意⚠️!Promise.then(...)里的回调才是微任务,而new Promise(...) 传入的函数是立即执行的,所以一般来说我们会把new Promise(...) 用函数封装起来,方便在需要的时候才调用
执行栈是一个存储任务调用的栈结构。当 js 执行时,会把任务压到一个栈的结构之中,后进先出,执行完函数后弹出。(一次执行一个任务!执行完毕后弹出)
任务队列是队列结构,是先进先出。
同步任务执行顺序(后进先出)
当浏览器第一次加载你的script,它默认的进了全局执行环境。如果在你的全局代码中你调用了一个函数,那么顺序流就会进入到你调用的函数当中,创建一个新的执行环境并且把这个环境添加到执行栈的顶部。
如果你在当前的函数中调用了其他函数,同样的事会再次发生。执行流进入内部函数,并且创建一个新的函数执行环境,把它添加到已经存在的执行栈**的顶部。浏览器始终执行当前在栈顶部的执行环境。一旦函数完成了当前的执行环境,它就会被弹出栈的顶部, 把控制权返回给当前执行环境的上一个执行环境
同步任务与异步任务的执行顺序
在浏览器中,执行 js 的函数(任务),是由 js引擎的“执行栈”负责的;当 js 引擎接收到一段代码时,就会从上到下解析代码,把同步任务按顺序放到执行栈中去执行。如果遇到异步任务,就会把异步任务先放到任务队列中挂起,等到同步任务执行完成后(也就是执行栈被清空了),再从任务队列中取出一个任务,压到执行栈中去执行。等到执行栈再次被清空,js 引擎就会再去检查任务队列中是否有任务等待执行,如果有则继续压入栈中执行,如此往复,直到任务队列被清空。这个过程,就是事件循环。
宏任务与微任务的执行顺序
微任务的执行优先级高于宏任务。从宏任务中取一个到执行栈执行的前提是微任务队列为空,换句话说,宏任务总是一个一个执行的(每执行完一个宏任务就要检查微任务队列是否还有微任务,如果有,就执行微任务并清空微任务队列,然后再从宏任务队列中取一个来执行(如果有的话)),而微任务总是一队一队执行的(并不是说把微任务全部放进执行栈中,而是一次执行一个,直到把微任务队列中的任务执行完毕)
********在事件循环中,指向顺序为
执行同步事件
遇到异步任务把它们放到对应的任务队列中(宏任务队列,微任务队列)
继续执行同步任务,如果遇到异步任务,执行步骤2,直到js执行栈被清空
执行并清空微任务,在执行过程中如果遇到异步任务,执行步骤2,然后依次取出微任务执行,直到微任务队列清空
逐个执行宏任务,如果期间遇到异步任务,执行步骤 2,当这个宏任务执行完成后,再检查微任务队列,如果不为空,则执行步骤4,然后检查宏任务队列是否为空,执行步骤 5