15 | 消息队列和事件循环:页面是怎么"活"起来的

渲染进程我们已经知道他有一个主线程,这个主线程非常非常的繁忙,要处理DOM、布局,还要处理JS任务和各种输入事件,因此为了保证不同类型任务的执行,需要一个系统来调度这些任务,这个调度系统就是本节要探究的消息队列和事件循环。

引入事件循环和消息队列过程

  1. 如果只是一些确定好的任务,然后使用一个单线程按照顺序处理这些任务就可以了,线程执行完毕退出。
  2. 但是在单线程执行任务的过程中,会处理新的任务,这个时候就需要引入循环语句事件循环,循环机制保证线程会一直执行,事件循环保证可以处理临时任务。
  3. 接着,如果有来自其他线程的任务,这个时候就需要引入消息队列(一种数据结构,先进先出)了,通过从消息队列中取出其他任务,得以实现解决其他线程发过来的任务。
  4. 最后,还有一个情况时来自其他进程发来的任务,这个时候是通过浏览器的IPC机制把其他进程任务发给渲染进程的IO进程,IO进程再发给页面主线程。

消息队列中的任务类型

包括:输入事件、鼠标移动、鼠标点击、鼠标滚动、微任务、文件读写、WebSocket、定时器、JS操作DOM、解析DOM、样式计算、布局阶段、CSS动画等。

页面使用单线程的缺点

通过上面简单的学习我们知道,页面线程中的所有任务都是来自消息队列,那么:
问题一:如何处理高优先级任务。
问题二:如何解决单个任务执行过长的问题。

如何处理高优先任务

比如,如何优先处理DOM的变化。
解决办法就是引入了微任务
我们通常把消息队列中的任务称为宏任务,每个宏任务包含了一个微任务队列,当执行宏任务过程中,DOM有变化了,就将这个变化添加到微任务列表中,当宏任务执行完后,不会去执行下一个宏任务,而是执行当前的微任务列表。因此就解决了实效和实时性问题。
常见的宏任务:setTimeout、setIntervval、
常见的微任务:Promise、process.nextTick

如何解决单个任务执行时间过长的问题

单线程意味着每次只能执行一个任务,其他任务处于等待状态,因此为了解决一个任务执行时间过长,JS通过异步-回调功能来规避这个问题,也即让要执行的JS任务滞后执行。

16 | WebAPI:setTimeOut是如何实现的?

setTimeOut的使用这里不再赘述。

浏览器是怎么实现setTimeout的

首先,我们知道渲染进程中所有运行在主线程上的任务都需要先添加到消息队列中去,然后事件循环系统按照顺序执行消息队列中的任务。
所以说要执行一段异步任务,需要先将其放在消息队列中去。
但是定时器设置回调函数有些特别,它是在一段时间间隔后执行的,但是消息队列是按顺序执行的,因此不能将定时器直接放入到消息队列中去。

在Chrome中,除了消息队列,还维护了一个延迟消息队列,定时器以及Chrome就是放到了这个延迟消息队列中去。

使用setTimeout的一些注意事项

  • 如果当前任务执行过久,会影响定时器任务的执行。
  • 如果setTimeout存在嵌套调用,那么系统会设置最短时间间隔为4ms。
  • 未激活的页面,setTimeout执行最小间隔是1000毫秒
  • 延时执行时间有最大值:24.8天。
  • 使用setTimeout设置的回调函数中的this不符合直觉,使用箭头函数解决。

17 | WebApi:XMLHttpRequest是怎么实现的

xhr提供了异步从Web服务器获取数据局部刷新页面的能力。

同步回调和异步回调

将一个函数作为参数传递给另一个函数 ,这个作为参数的函数就叫做回调函数
若回调函数在主函数返回之前执行的,我们把这个回调过程称为同步回调
把这种回调函数在主函数外部执行的过程称为异步回调

异步回调过程

  • 第一种是把异步函数做成一个任务,添加到消息对了尾部。
  • 第二种是把异步函数添加到微任务列表中,等当前任务执行完毕就执行微任务。
    异步函数的两个类型不同也就导致了一个是宏任务,一个是微任务。

XMLHttpRequest运作机制

  • 创建XMLHttpRequest对象: let xhr = new XMLHttpRequest()
  • 为xhr对象注册回调函数:ontimeout监控超时、onerror监控错误信息、onreadyshatechange监控后台请求过程。
  • 通过open接口配置基础请求信息:请求地址、请求方式、请求方法、超时时间…
  • 通过xhr.send发起请求。

XMLHttpRequest使用过程中的坑

  • 安全策略的跨域问题
  • HTTPS混合内容的问题:https页面中包含了不符合https安全要求的内容(http资源)

18 | 宏任务和微任务:不是所有任务都是一个待遇

微任务可以在实时性和效率之间做一个有效的权衡。

宏任务

就是一些鼠标、渲染、交互、脚本、网络读写、文件读写等等操作被称为宏任务。
宏任务在主线程上的执行,是由页面线程引入了消息队列和循环机制,消息队列中的任务是通过事件循环来执行的。
宏任务难以满足时间精度要求较高的任务,比如一个setTimeout中包裹一个setTimeout,虽然设定时间都是0之后,但是会有其它任务窜进来执行。

微任务

微任务就是一个需要异步执行的一个函数,执行时机是在主函数执行完毕之后、当前宏任务结束之前。
V8引擎在执行JS脚本的时候,除了创建一个全局的执行上下文,还会在其内部创建一个微任务队列,由于实在V8引擎内部给的,所以我们无法通过JS访问。
也就是说,每一个宏任务都关联了一个微任务队列。

在现代浏览器里面,产生微任务的主要两个方式:

  1. 使用MutationObserve监听某个DOM节点的变换。
  2. 使用Promise。

执行时机:就是在本轮宏任务执行完毕后,去检查微任务队列中是否有微任务,需要注意一点的是执行微任务过程中产生的新的微任务不会推迟到下个宏任务中去执行,而是在当前宏任务中继续执行。

19 | 使用Promise,告别回调函数

Promise已经成为现代前端的水和电。so important!,那么Promise的出现是为了解决什么问题呢?

异步编程的问题

Web页面的单线程架构决定了异步回调,而异步回调决定了我们的编码方式。在之前的很多回调中,代码逻辑变得不连续且混乱。
然后,为了解决这个问题,我们可以封装异步代码,让处理流程变得线性。但同时出现了新的问题:回调地狱。
回调地狱主要是嵌套混乱,下面的任务依赖上一个任务,嵌套多次代码可读性很差,且任务的不确定性(执行每个任务都有两种可能结果),所以也增加了代码的混乱程度。
于是,解决问题的两个思路就是:消灭嵌套调用、合并多个任务的错误处理。

Promise

Promise的出现就解决了消灭嵌套调用和多次错误处理的问题。
Promise的核心其实就是resolve函数,resolve函数执行会触发.then的回调,但回调函数还没有执行,而是采用了延迟绑定,可以理解为.then放入到了微任务队列中去,等待宏任务执行完毕后检查执行。

20 | async/await:使用同步的方式去写异步代码

ES7引入了async和await,这是JavaScript异步编程的一个重大改进,提高了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。
Promise编程模型因为充斥大量then方法,虽然解决了地狱回调的问题,但是语义方面的缺陷,使得async和wait出现了。
本节会首先介绍生成器(Generator)是如何工作的,接着讲解Generator的底层实现机制–协程(Coroutine),又因为async和await使用了Generator和Promise两种技术,接着通过Generator和Promise来分析async和await到底是如何以同步的方式来编写异步代码的。

生成器 VS协程

生成器函数一个带有星号的函数,可以暂停和恢复执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* genDeomo(){
console.log('开始执行第一段')
yield 'generator 2'
console.log("开始执行第二段")
yield 'generator 2'
console.log("开始执行第三段")
yield 'generator 2'
console.log("执行结束")
return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

在生成器函数内部执行一段代码,如果遇到yield关键字,那么JS引起会返回该关键字后面的内容给外部,并暂停该函数的执行,如果遇到return关键字,JS引擎会结束该生成函数,并将return后面的内容进行最后的返回。
外部函数通过next方法恢复函数的执行。

那么,这个暂停和恢复执行的实现其关键原理是什么呢?这就需要先了解协程的概念。
协程是一种比线程更加轻量级的存在。我们可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

然后通过Generator函数的写法,经过改造后就可以用同步的方式写出异步的代码了,略过不表。

async/await

async和await技术背后的实现方式就是Generator和Promise应用,往底层说就是微任务和协程应用。

async

根据MDN定义,async是一个通过异步执行隐式返回Promise作为结果的函数。
通过async声明的函数返回的是Promise对象,如代码所示:

1
2
3
4
async function foo() {
return 2
}
console.log(foo()) // Promise {<resolved>: 2}

await

1
2
3
4
5
6
7
8
9
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)