上次咱们从高阶函数聊到了 promise
,此次咱们聊聊:javascript
promise A+
规范和 promise
应用来看 promise
的特性promise
和 eventloop 的关系薛定谔的猫是奥地利著名物理学家薛定谔提出的一个思想实验,那么这和 promise
有什么关系呢?在这个著名的实验中,假设在盒子里会有一只猫,而后咱们打开盒子只会出现两个结果,猫死了或者是活着:html
那么 promise
也相似,根据 promise A+ 规范 当一个 promise
被建立出来之后,它就拥有三种可能状态 Pending (初始时为 pending)/ Fulfilled / Rejected 若是咱们把范围放宽一点,那么 Fulfilled / Rejected 又能够被称为 Settled:前端
okay,相信你已经理解了 promise
的三种状态,那细心同窗看到上面有 then()
和 catch()
这样的方法可能不理解,咱们再回到上面猫的例子里面,如今这个科学家比较变态,在第一次实验以后,猫出现了两种状态,可是他并没结束实验,而是针对这两种状况作了处理并继续了实验:html5
与之相似,一个完整的 promise
,在 Pending 状态发生变化时,只多是两种状况,Fulfilled 和 Rejected,而且咱们能够看到箭头是单向的,意味着这个过程是 不可逆 的。java
这意味着,当 Pending
状态发生了变化,不管是变成 Fulfilled
仍是 Rejected
都没法再改变了。node
针对这两种状况,咱们在 then()
里面能够传入两个回调函数 onFulfillment
和 onRejection
做为来处理不一样的状况。git
从图中咱们能够看到,当 onFulfillment
时,咱们一般会作一些异步的操做,而 onRejection
一般是作错误处理。而后咱们把当前的 promise
从新返回,直到下次他的 then()
再次被执行。github
一个promise.then().then().then()
这样的方式就是咱们 上一篇文章 中所说的 链式调用。web
经过上一节,咱们已知 promise
自己的几个特性:vim
promise
有三种状态: Pending (初始时为 pending)/ Fulfilled / Rejected。promise
状态的转变是不可逆的: Pending -> Fulfilled 或者 Pending -> Rejected 。promise
支持 then()
的链式调用。可是还有一些特性,咱们须要从代码的角度来分析。
由于 promise
原意为承诺,也就是我预先承诺了未来要达成的一件事情。
因此有同窗会认为必须等到承诺兑现,也就是 promise
的状态从 Pending
变为 Fulfilled
或者 Rejected
时,其构造函数接收的函数才会被执行。
可是实际上,一个 promise
被建立时,即便咱们没有定义 then()
,其构造函数接收的函数也会当即执行:
let p = new Promise((resolve, reject) => {
console.log('A new promise was created1')
console.log('A new promise was created2')
console.log('A new promise was created3')
setTimeout(() => {
console.log('log setTimeout')
}, 3000)
resolve('success')
})
console.log('log outside')
复制代码
输出结果:
A new promise was created1
A new promise was created2
A new promise was created3
log outside
log setTimeout
复制代码
根据 promise A+ 规范 , promise
的 then()
接收2个参数:
promise.then(onFulfilled, onRejected)
复制代码
其中 onFulfilled
执行结束后调用,onRejected
拒绝执行后调用,看看这段代码:
let p = new Promise((resolve, reject) => {
reject('reject')
//throw 'error'
})
p.then(
data => {
console.log('1:', data)
},
reason => {
console.log('reason:', reason)
}
)
复制代码
最后打印的是:
reason: reject
复制代码
能够正常运行不是吗?可是咱们发现实际应用中,咱们并无这样来定义 then()
:
p.then(
data => {
console.log('1:', data)
},
reason => {
console.log('reason1:', reason)
}
).then(
data => {
console.log('2:', data)
},
reason => {
console.log('reason2:', reason)
}
).then(
data => {
console.log('3:', data)
},
reason => {
console.log('reason3:', reason)
}
)
复制代码
而是使用 catch()
配合 onFulfilled()
:
p.then(data => {
console.log('1:', data)
}).then(data => {
console.log('2:', data)
}).then(data => {
console.log('3:', data)
}).catch(e => {
console.log('e2:', e)
})
复制代码
表面上看,达到的效果是同样的,因此这样有什么好处呢?
onFulfilled()
中若是发生错误,也会进行捕获,不会中断代码的执行。看一段代码:
let p = new Promise((resolve, reject) => {
console.log('A new promise was created1')
console.log('A new promise was created2')
console.log('A new promise was created3')
resolve('success')
})
console.log('log outside')
p.then(data => {
console.log('then:', data)
})
复制代码
执行结果:
A new promise was created1
A new promise was created2
A new promise was created3
log outside
then: success
复制代码
咱们能够很清楚的看到,then()
中打印的内容是在最后的,为何会这样呢?由于 p.then()
中传入的函数会被推入到 microtasks
(异步任务队列的一种) 中,而任务队列都是在执行栈中的代码(同步任务)以后处理。
下面这些代码都在同步任务中处理:
console.log('A new promise was created1')
console.log('A new promise was created2')
console.log('A new promise was created3')
console.log('log outside')
复制代码
okay 看到这里你可能会有一些问题,例如:
要明白这些,就不得不聊聊 Event loop。
在 W3C文档 中咱们能够找到关于它的描述:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
翻译一下就是:
客户端必须使用本章节中所描述的事件循环,来协调事件,用户交互,脚本,呈现,网络等等。 事件循环有两种:用于浏览上下文的事件循环和用于 worker 的事件循环。
咱们写好一段 JavaScript 代码,而后浏览器打开这个页面,或者在 node
环境中运行它,就能够获得咱们指望的结果,可是这段代码怎么执行的呢?
不少同窗都知道,是 JavaScript
引擎在执行代码,而 JavaScript
引擎都是依托于一个宿主环境的,最通用的 JavaScript
宿主环境是浏览器。
这和 EventLoop 有什么关系呢?
由于宿主环境是浏览器,因此 JavaScript
引擎被设计为单线程。
为何不能是多线程呢?举个例子:加入咱们同时两个线程都操做同一个 DOM
元素,那应该如何处理呢?对吧。
okay,既然是单线程,意味着咱们只能顺序执行代码,可是若是咱们执行某一行特别耗费时间,是否是在这行后面的内容就被阻塞了呢?
因此咱们须要在单线程的引擎中来实现异步,而 Event loop 就是实现异步的关键。
首先当一段代码给到 JavaScript 引擎的时候,会区分这段代码是同步仍是异步:
异步的代码加入到任务队列中,而任务队列又分为 宏任务队列(macro tasks) 和 微任务队列(micro tasks)。
一个浏览器的上下文环境可能对应有多个宏任务队列可是只有一个微任务队列。你可能以为会是这样:
可是实际上,每一个宏任务都包含了一个微任务队列:
那么问题来了,咱们怎么去判断这段代码要加入到宏任务队列,仍是微任务队列中呢?
咱们参考下文档 中的解读:
Each task is defined as coming from a specific task source. All the tasks from one particular task source and destined to a particular event loop
每一个任务都由特殊任务源来定义。 来自同一个特殊任务源的全部任务都将发往特定事件循环
因此咱们能够按照不一样的来源进行分类,不一样来源的任务都对应到不一样的任务队列中
I/O
, setTimeout + setInterval + setImmediate
, UI renderder
···Promise
,process.nextTick
,MutationObserver
, Object.observe
···明白了这些概念以后,咱们来看看完整的执行过程。
下图参考了 Philip Roberts的演讲 PPT同时加深和细化:
图的顺序从上往下看:
JavaScript
引擎对全部的代码进行区分。步骤 3 - 4 - 5
就是一个事件循环的基本原理。
不知道这篇文章有没有让你充分理解呢?有任何想法和建议,都留下你的评论吧~
小册 你不知道的 Chrome 调试技巧 已经开始预售啦。
欢迎关注公众号 「前端恶霸」,扫码关注,好货等着你~