[前端漫谈_4] 从 薛定谔的猫 聊到 Event loop

前言

上次咱们从高阶函数聊到了 promise ,此次咱们聊聊:javascript

  • promise A+ 规范和 promise 应用来看 promise 的特性
  • promise 和 eventloop 的关系

从薛定谔的猫(Erwin Schrödinger's Cat)来理解 promise

薛定谔的猫是奥地利著名物理学家薛定谔提出的一个思想实验,那么这和 promise 有什么关系呢?在这个著名的实验中,假设在盒子里会有一只猫,而后咱们打开盒子只会出现两个结果,猫死了或者是活着:html

那么 promise 也相似,根据 promise A+ 规范 当一个 promise 被建立出来之后,它就拥有三种可能状态 Pending (初始时为 pending)/ Fulfilled / Rejected 若是咱们把范围放宽一点,那么 Fulfilled / Rejected 又能够被称为 Settled前端

okay,相信你已经理解了 promise 的三种状态,那细心同窗看到上面有 then()catch() 这样的方法可能不理解,咱们再回到上面猫的例子里面,如今这个科学家比较变态,在第一次实验以后,猫出现了两种状态,可是他并没结束实验,而是针对这两种状况作了处理并继续了实验:html5

与之相似,一个完整的 promise ,在 Pending 状态发生变化时,只多是两种状况,FulfilledRejected,而且咱们能够看到箭头是单向的,意味着这个过程是 不可逆 的。java

这意味着,当 Pending 状态发生了变化,不管是变成 Fulfilled 仍是 Rejected 都没法再改变了。node

针对这两种状况,咱们在 then() 里面能够传入两个回调函数 onFulfillmentonRejection 做为来处理不一样的状况。git

从图中咱们能够看到,当 onFulfillment 时,咱们一般会作一些异步的操做,而 onRejection 一般是作错误处理。而后咱们把当前的 promise 从新返回,直到下次他的 then() 再次被执行。github

一个promise.then().then().then() 这样的方式就是咱们 上一篇文章 中所说的 链式调用web

经过 promise 的执行来看特性

经过上一节,咱们已知 promise 自己的几个特性:vim

  • promise 有三种状态: Pending (初始时为 pending)/ Fulfilled / Rejected
  • promise 状态的转变是不可逆的: Pending -> Fulfilled 或者 Pending -> Rejected
  • promise 支持 then() 的链式调用。

可是还有一些特性,咱们须要从代码的角度来分析。

1. 建立后,当即执行

由于 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
复制代码

2. 异常处理的方式

根据 promise A+ 规范promisethen() 接收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)
    })
复制代码

表面上看,达到的效果是同样的,因此这样有什么好处呢?

  1. 减小代码量。
  2. onFulfilled() 中若是发生错误,也会进行捕获,不会中断代码的执行。

3. 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')
  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 看到这里你可能会有一些问题,例如:

  • 什么是 同步任务 ?
  • 什么是 执行栈?
  • 什么是 microtasks
  • 什么是 异步任务队列

要明白这些,就不得不聊聊 Event loop。

Event loop 是什么?为何咱们须要 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 就是实现异步的关键。

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

每一个任务都由特殊任务源来定义。 来自同一个特殊任务源的全部任务都将发往特定事件循环

因此咱们能够按照不一样的来源进行分类,不一样来源的任务都对应到不一样的任务队列中

  • (macro-task 宏任务)来源:I/O, setTimeout + setInterval + setImmediate, UI renderder ···
  • (micro-task 微任务)来源:Promiseprocess.nextTickMutationObserver, Object.observe ···

明白了这些概念以后,咱们来看看完整的执行过程。

Event loop 完整的执行过程

下图参考了 Philip Roberts的演讲 PPT同时加深和细化:

图的顺序从上往下看:

  1. 代码开始执行,JavaScript 引擎对全部的代码进行区分。
  2. 同步代码被压入栈中,异步代码根据不一样来源加入到宏任务队列尾部,或者微任务队列的尾部。
  3. 等待栈中的代码被执行完毕,此时通知任务队列,执行位于队列首部的宏任务。
  4. 宏任务执行完毕,开始执行其关联的微任务。
  5. 关联的微任务执行完毕,继续执行下一个宏任务,直到任务队列中全部宏任务被执行完毕。
  6. 执行下一个任务队列。

步骤 3 - 4 - 5 就是一个事件循环的基本原理。

最后

不知道这篇文章有没有让你充分理解呢?有任何想法和建议,都留下你的评论吧~

小册 你不知道的 Chrome 调试技巧 已经开始预售啦。

欢迎关注公众号 「前端恶霸」,扫码关注,好货等着你~

相关文章
相关标签/搜索