消息队列和事件循环
1. 第一版的线程模型
对于一些确认好的任务,可以使用一个单线程来按照顺序处理这些任务。
2. 第二版的线程模型
要在线程执行过程中:接收并处理新的任务,就需要引入循环语句和事件系统(即:事件循环机制)。
相较于第一版的改进:
- 第一点 引入了循环机制,实现方式是在线程语句最后添加了一个for循环语句,线程会一直循环执行。
- 第二点 引入了事件, 在线程运行过程中,等待输入任务,一旦接收到用户输入,激活线程,然后执行任务。
3. 第三版线程模型
如果要接收其他线程任务发送过来的任务,就需要引入消息队列。
相较于第二版(第二版线程模型中,所有任务都来自于线程内部)改进:
- 第一: 添加一个消息队列 ;
- 第二:IO线程中产生的新任务添加到系统消息队列尾部;
- 第三:渲染主线程会循环地从消息队列的头部中读取任务,执行任务。
消息队列是一种数据结构,可以存放要执行的任务。它符合”先进先出“的特点。要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
4. 跨进程发送任务
如果其他进程想要发送任务给页面主线程,那么先通过IPC把任务发送给渲染进程的IO线程,IO线程再把任务发送给页面主线程。
由图看出,渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程。
- 消息队列机制并不太灵活,为了适应效率和实时性,引入了微任务。
使用单线程的缺点
- 第一个问题是如何处理高优先级的任务。
- 添加微任务,来权衡效率和实时性。
- 第二个问题是如何解决单个任务执行时长过久的问题。
- 添加回调功能
WebAPI: setTimeout总结
浏览器的页面是通过消息队列和事件循环系统来驱动的。
为了支持定时器的实现,浏览器增加了延时队列。
使用setTimeout的一些注意事项:
- 如果当前任务执行时间过久,会影响定时器任务的执行。
- 如果setTimeout存在嵌套调用,那么系统会设置最短时间间隔为4毫秒
- 未激活的页面,setTimeout执行最小间隔是1000毫秒
- 延时执行时间有最大值2147483647 毫秒,溢出会导致定时器立即执行;
- 使用 setTimeout 设置的回调函数中的 this 不符合直觉,会是回调时候对应的this对象,可以使用箭头函数解决,或者使用bind方法。
注意:延迟队列也是宏任务,实际上blink维护了很多不同优先级的队列,这些队列里面都是宏任务。
延迟消息队列主要放一些定时执行的任务,如JavaScript设置定时器的回调,还有浏览器内部的一些定时回调任务! 这类任务需要等到指定时间间隔之后才会被执行!
而正常的消息队列中的任务只会按照顺序执行,执行完上个任务接着执行下个任务,不需要关系时间间隔!
微任务是在宏任务执行过程中的某个时间点执行的,通常是在宏任务快要结束的时候执行。
宏任务
页面中大部分的任务在主线程上执行,这些任务包括:
- 渲染事件(如解析DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JavaScript脚本执行事件;
- 网络请求完成、文件读写完成事件。
缺点:
宏任务的时间粒度比较大,执行的时间间隔不能精确控制,不能满足一些高实时性的需求。比如监听DOM变化的需求。
微任务
异步回调的概念,主要又两种方式:
- 第一种是异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。 (比如setTimeout 和 XMLHttpRequest 的回调函数都是通过这种方式来实现的)
- 第二种方式的执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式提现的。
微任务到底是什么呢?
- 微任务就是一个需要异步执行的函数,执行时机在主函数执行结束之后、当前宏任务结束之前。
微任务是怎么产生的?
-
第一种方式:使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
-
第二种方式:使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。
微任务队列是如何被执行的?
- 在当前宏任务中的JavaScript快执行完成时,即JavaScript引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务
- WHATWG 把执行微任务的时间点称为检查点
Promise
Promise解决的是异步编码风格的问题。
异步编程的问题:过多的异步回调导致代码逻辑不连续,
怎么解决?
封装异步代码,让处理流程变得线型,重点关注输入数据和输出结果。
新的问题:回调地狱
原因:
- 第一是嵌套调用,由于嵌套太多的回调函数就很容易使自己陷入回调地狱,代码的可读性变得很差。
- 第二是任务的不确定性,每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。
解决:
Promise:消灭嵌套调用和多次错误处理
Promise怎么解决嵌套回调的?
- Promise实现了回调函数的延时绑定。
- 将回调函数onResolve的返回值穿透到最外层。
Promise 是怎么处理异常的?
使用最后一个对象来捕获所有异常:
- Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。
- 具备了这样“冒泡”的特性后,就不需要在每个 Promise 对象中单独捕获异常了。
async/await
ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。
生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。
函数为何能暂停和恢复?
- 协程是一种比线程更加轻量级的存在
- 一个线程也可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
写在最后
学习资源来自极客时间 – 李兵老师 《浏览器工作原理与实践》。接下来,让我们一起每日打卡,check完成所有课程吧 ~