一、消息队列和事件循环

要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。
**

  • 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型。
  • 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。
  • 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
  • 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
  • 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。

image.png
步骤:

  • 添加一个消息队列;
  • IO 线程中产生的新任务添加进消息队列尾部;
  • 渲染主线程会循环地从消息队列头部中读取任务,执行任务。

二、WebAPI:setTimeout是如何实现的?

  • 首先,为了支持定时器的实现,浏览器增加了延时队列。
  • 其次,由于消息队列排队和一些系统级别的限制,通过 setTimeout 设置的回调任务并非总是可以实时地被执行,这样就不能满足一些实时性要求较高的需求了。
  • 最后,在定时器中使用过程中,还存在一些陷阱,需要你多加注意

三、WebAPI:XMLHttpRequest是怎么实现的?

1. 回调函数 VS 系统调用栈

将一个函数作为参数传递给另外一个函数,那作为参数的这个函数就是回调函数

回调函数 callback 是在主函数 doWork 返回之前执行的,我们把这个回调过程称为同步回调

  1. let callback = function(){
  2. console.log('i am do homework')
  3. }
  4. function doWork(cb) {
  5. console.log('start do work')
  6. cb()
  7. console.log('end do work')
  8. }
  9. doWork(callback)

setTimeout 函数让 callback 在 doWork 函数执行结束后,又延时了 1 秒再执行,这次 callback 并没有在主函数 doWork 内部被调用,我们把这种回调函数在主函数外部执行的过程称为异步回调

  1. let callback = function(){
  2. console.log('i am do homework')
  3. }
  4. function doWork(cb) {
  5. console.log('start do work')
  6. setTimeout(cb,1000)
  7. console.log('end do work')
  8. }
  9. doWork(callback)

异步回调是指回调函数在主函数之外执行,一般有两种方式:

  • 第一种是把异步函数做成一个任务,添加到信息队列尾部;
  • 第二种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了。

2. XMLHttpRequest 运作机制

image.png

  1. function GetWebData(URL){
  2. /**
  3. * 1: 新建 XMLHttpRequest 请求对象
  4. */
  5. let xhr = new XMLHttpRequest()
  6. /**
  7. * 2: 注册相关事件回调处理函数
  8. */
  9. xhr.onreadystatechange = function () {
  10. switch(xhr.readyState){
  11. case 0: // 请求未初始化
  12. console.log(" 请求未初始化 ")
  13. break;
  14. case 1://OPENED
  15. console.log("OPENED")
  16. break;
  17. case 2://HEADERS_RECEIVED
  18. console.log("HEADERS_RECEIVED")
  19. break;
  20. case 3://LOADING
  21. console.log("LOADING")
  22. break;
  23. case 4://DONE
  24. if(this.status == 200||this.status == 304){
  25. console.log(this.responseText);
  26. }
  27. console.log("DONE")
  28. break;
  29. }
  30. }
  31. xhr.ontimeout = function(e) { console.log('ontimeout') }
  32. xhr.onerror = function(e) { console.log('onerror') }
  33. /**
  34. * 3: 打开请求
  35. */
  36. xhr.open('Get', URL, true);// 创建一个 Get 请求, 采用异步
  37. /**
  38. * 4: 配置参数
  39. */
  40. xhr.timeout = 3000 // 设置 xhr 请求的超时时间
  41. xhr.responseType = "text" // 设置响应返回的数据格式
  42. xhr.setRequestHeader("X_TEST","time.geekbang")
  43. /**
  44. * 5: 发送请求
  45. */
  46. xhr.send();

第一步:创建 XMLHttpRequest 对象。
第二步:为 xhr 对象注册回调函数。
第三步:打开请求。
第四步:配置基础的请求信息。
第五步:发起请求。
**

3. XMLHttpRequest 使用过程中的“坑”

由于加了安全限制,导致使用起来非常麻烦。想更加完美地使用 XMLHttpRequest,你就要了解浏览器的安全策略。

3.1. 跨域问题

A 站点中去访问不同源的 B 站点的内容,不是同一个源,所以就涉及到了跨域。

3.2 HTTPS 混合内容的问题

HTTPS 混合内容是 HTTPS 页面中包含了不符合 HTTPS 安全要求的内容,比如包含了 HTTP 资源,通过 HTTP 加载的图像、视频、样式表、脚本等,都属于混合内容。

通常,如果 HTTPS 请求页面中使用混合内容,浏览器会针对 HTTPS 混合内容显示警告,用来向用户表明此 HTTPS 页面包含不安全的资源。

小结

对比上一节,setTimeout 是直接将延迟任务添加到延迟队列中,而 XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。

四、宏任务和微任务

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

常见的微任务有MutationObserver、Promise 以及以 Promise 为基础开发出来的很多其他的技术。

1. 宏任务

主线程上执行的任务包括:

  • 渲染事件(如解析 DOM、计算布局、绘制);
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
  • JavaScript 脚本执行事件;
  • 网络请求完成、文件读写完成事件。

为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务

2. 微任务

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
**

2.1 微任务是如何产生的

  • 第一种方式是使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • 第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

通过 DOM 节点变化产生的微任务或者使用 Promise 产生的微任务都会被 JavaScript 引擎按照顺序保存到微任务队列中。

2.2 微任务队列是何时被执行

在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。

3. 宏任务微任务小结

  • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
  • 微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以你在写代码的时候一定要注意控制微任务的执行时长。
  • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
  • 通过异步操作解决了同步操作的性能问题
  • 通过微任务解决了实时性的问题

五、Promise

1. 异步编程

image.png

2. 回调地狱

产生:

  • 第一是嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。
  • 第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的错误处理的方式,明显增加了代码的混乱程度。

解决:

  • 第一是消灭嵌套调用
  • 第二是合并多个任务的错误处理

3. Promise:消灭嵌套调用和多次错误处理

Promise 主要通过下面两步解决嵌套回调问题的。

  • 首先,Promise 实现了回调函数的延时绑定
  • 其次,需要将回调函数 onResolve 的返回值穿透到最外层

错误处理:
Promise可以使用最后一个对象来捕获所有异常,是因为 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。具备了这样“冒泡”的特性后,就不需要在每个 Promise 对象中单独捕获异常了。

思考

1.Promise 中为什么要引入微任务?

因为promise的resolve和reject回调采用延时绑定机制,宏任务粒度太大所以引入微任务

2.Promise 中是如何实现回调函数返回值穿透的?

因为每一次在.then中都会返回一个新的promise

3.Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获异常的函数?

出错后通过包装成promise.reject形式返回,如果then中没有第二个参数处理异常,则继续返回promise.reject直到传递给最后