No4.浏览器中的页面循环系统

前段时间在《极客时间》上学了一个专栏,通篇略过,干货很多,但理解至关不够透彻,因而计划用几周的时间,对本专栏内容用做者的总结以及本身的相对逐字理解,来个通篇的文字记录学习,书读百遍,其义自现。
本篇是这个专栏的第四章:《浏览器中的页面循环系统》。本章分为六节。

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


本节主要专门介绍页面的事件循环系统,但愿经过几段总结能对页面的事件循环系统有一个总体上的理解。前端

使用单线程处理安排好的任务

单线程处理的流程就是把全部任务代码按照顺序写进主线程里,等线程运行时,这些任务按照顺序在线程中执行,等全部任务执行完成,线程自动退出。web

在线程运行过程当中处理任务

固然并不是全部任务均可以使用单线程处理,有时咱们须要在线程运行的过程当中处理任务。
那么要想在线程运行过程当中,能接受并执行新的任务,就须要采用事件循环机制。 相较与单线程处理任务,此线程作了两点改进:编程

  • 引入了循环机制。(好比一个实现方式是添加for循环。线程一直循环执行)。
  • 引入了事件。
处理其余线程发送过来的任务

如何设计好一个线程模型,能让其可以接受其余线程发送的消息呢?
一个通用的模式是消息队列:「消息队列是一种数据结构、能够存放要执行的任务。它符合队列“先进先出”的特色。」
有了队列以后继续改进步骤以下:设计模式

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

渲染进程专门有一个 IO 线程用来接收其余进程传进来的消息,接收到消息以后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面的“处理其余线程发送的任务”同样。跨域

消息队列中的任务类型

消息队列中的任务都有哪些呢?
输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等。除此以外,消息队列中还包含了不少与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。浏览器

页面使用单线程的缺点
  • 第一个问题是如何处理高优先级的任务。
    因为优先级的问题使得微任务应用而生,微任务是如何权衡效率和实时性的呢? 一般咱们把消息队列中的任务称为宏任务,每一个宏任务中都包含了一个微任务队列,在执行宏任务的过程当中,若是 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,所以也就解决了执行效率的问题.等宏任务中的主要功能都直接完成以后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,由于 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题
  • 第二个是如何解决单个任务执行时长太久的问题. 针对这种状况,JavaScript 能够经过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。
总结

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

16 | WebAPI : setTimeout是如何实现的


浏览器怎么实现setTimeout

经过上一小节的学习,咱们知道:对于一些事件执行的过程是:这些事件先被添加到消息队列,而后事件循环系统就会按照消息队列中的顺序来执行事件。也就是说,执行一段异步任务,须要先将任务添加到消息队列中。
不过经过定时器设置回调函数有点特别,它们须要在指定的时间间隔内被调用,但消息队列中的任务是按照顺序执行的,因此为了保证回调函数能在指定时间内执行,你不能将定时器的回调函数直接添加到消息队列中。
从Chromium队列的部分源码中咱们知道,在Chrome中除了正常使用的消息队列外,还有另一个消息队列,这个队列中维护了须要延迟执行的任务列表,包括了定时器和Chromium内部一些须要延迟执行的任务。 因为消息队列排队和一些系统级别的限制,经过setTimeout设置的回调任务并不是老是能够实时的执行,这样就不能知足一些实时性要求较高的需求。bash

使用setTimeout的一些注意事项
  • 若是当前任务执行时间太久,会影响延迟到期定时器任务的执行。
  • 若是 setTimeout 存在嵌套调用,那么系统会设置最短期间隔为 4 毫秒。
  • 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒.
  • 延时执行时间有最大值:大约 24.8 天
  • 使用 setTimeout 设置的回调函数中的 this 不符合直觉.

17 | WebAPI:XMLHttpRequest是怎么实现的?


在深刻讲解 XMLHttpRequest 以前,咱们得先介绍下同步回调异步回调这两个概念.网络

回调函数 VS 系统调用栈

回调函数:将一个函数做为参数传递给另一个函数,那做为参数的这个函数就是回调函数。前端工程师

  • 同步回调函数代码:
let callback = function(){
    console.log('i am do homework')
}
function doWork(cb) {
    console.log('start do work')
    cb()
    console.log('end do work')
}
doWork(callback)
//start do work
//i am do homework
//end do work
复制代码
  • 异步回调函数代码:
let callback = function(){
    console.log('i am do homework')
}
function doWork(cb) {
    console.log('start do work')
    setTimeout(cb,1000)   
    console.log('end do work')
}
doWork(callback)
复制代码
XMLHttpRequest运做机制

对回调函数有了一个认知后,那么接着咱们来分析下从发起请求到接收数据的完整流程:

首先从XMLHttpRequest的用法开始:

  • 第一步:建立XMLHttpRequest对象。
  • 第二步:为xhr对象注册回调函数。
  • 第三步:配置基础的请求信息。
  • 第四步:发起请求。
XMLRequest使用过程当中的“坑”
  • 跨域问题
  • HTTPS混合内容的问题:这是指HTTPS页面中包含了不符合HTTPS安全要求的内容,好比包含了HTTP资源。
小结

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

18 | 宏任务和微任务:不是全部的任务都是一个待遇


前面咱们已经知道微任务能够在实时性和效率之间作一个有效的权衡。微任务已被普遍应用,好比Promise以及以Promise为基础开发出来的不少其余的技术。
宏任务与微任务的区别:

宏任务

页面中的大部分任务都是在主线程上执行的。如渲染事件、用户交互事件、JavaScript脚本执行事件、网络请求等等。这些在消息队列中的任务称为宏任务。
虽然宏任务能够知足咱们大部门的平常需求,可是有时对时间精度要求较高的需求,宏任务就难以胜任了。

微任务

微任务就是一个须要异步执行的函数,执行时机是在主函数执行结束以后、当前宏任务结束以前。
产生微任务的两种方式:

  • 第一种方式是使用 MutationObserver 监控某个 DOM 节点,而后再经过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • 第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。 经过微任务的工做流程,咱们能够得出以下结论:
  • 微任务和宏任务是绑定的,每一个宏任务在执行时,会建立本身的微任务队列。
  • 微任务的执行时长会影响到固然宏任务的执行时长,所以写代码的时候必定要注意微任务的执行时长。
  • 在一个宏任务中,分别建立一个用于回调的宏任务和微任务,不管什么状况下,微任务早于宏任务执行。
监听DOM变化演变

微任务应用在了MutationObserver中,MutationObserver是用来监听DOM变化的一套方法。 监听DOM变化一直是前端工程师一项很是核心的需求。
下面是监听DOM变化演变的简单总结:

  • 早起观测DOM变化就是轮询检测。好比使用 setTimeout 或者 setInterval 来定时检测 DOM 是否有改变。无疑这种方式实时性很差,效率还低效。
  • 2000年的时候引入了Mutation Event,Mutation Event采用了观察者的设计模式,当DOM有变更时当即出发相应的事件。此方式属于同步回调。虽然这种方式解决了实时性问题,可是由于会产生较大性能开销、致使页面性能出现问题,被反对使用并逐步从web标准事件中删除。
  • MutationObserver替代MutationEvent,相较于Event方式,Observer采用了一次触发异步回调。且采用微任务的处理,使得实时性与性能功能都获得有效提升。

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

微任务的另外一个应用:Promise。 本节简单介绍JavaScript引入Promise的动机,以及解决问题的几个核心关键点。 讲到动机,也就是说Promise解决了什么问题。众所周知,他解决的是异步编码风格的问题。

页面编程的一大特色就是:异步编程,下面分析异步编程的代码风格进化。

  • 以前的代码编码风格,一段代码可能会出现五次回调,这种回调致使代码逻辑不连贯、不连线,不符合人的直觉。
  • 而后开发人员们经过封装异步代码,让处理流程变得线性,可是这种处理方式若是嵌套了太多的回调函数就容易陷入回调地狱。
  • 陷入回调地狱的后代码看上去很乱主要是两点:嵌套调用和任务不肯定性(成功或者失败)。因而Promise出现,解决了这两个问题。
Promise:消灭嵌套调用和屡次错误处理

Promise经过两步解决嵌套回调问题:

  • 首先,Promise实现了回调函数的延时绑定(.then)
  • 其次,将回调函数返回值穿透到最外层。

Promise处理异常: 经过最后一个catch,将全部对象合并到一个函数来处理以前的全部异常。

Promise与微任务

Promise 之因此要使用微任务是由 Promise 回调函数延迟绑定技术致使的。

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

当Promise解决回调地狱代码风格的同时,咱们发现写不少的then函数,仍是有些不太容易阅读。 基于这个缘由,ES7引入了async/await,这是JavaScript异步编程的一个重大改进,提供了在不阻塞主线程的状况下使用同步代码实现异步访问资源的能力。而且使得代码逻辑更加清晰。

本节首先介绍生成器(Generator)是如何工做的,接着介绍了Generator的底层实现机制--协程。
这是由于async/await使用了Generator和Promise两种技术。因此紧接着经过Generator和Promise来分析async/await究竟是如何经过以同步方式来编写异步代码的。

生成器 VS 协程

生成器函数:生成器函数是一个带星号函数,并且是能够暂停执行和恢复执行的。 具体使用方式就是:在生成器函数内部执行一段代码,若遇到yiled关键字,那JS引擎将返回该关键字后面的内容且暂停该函数执行,外部函数经过next方法恢复函数的执行。
那么JavaScript引擎V8是如何实现一个函数的暂停和恢复的?

搞懂它的暂停和恢复,须要首先了解协程的概念。协程是一种比线程更加轻量级的存在。能够把协程看做是跑在线程上的任务,一个线程能够存在多个协程。但在线程上同时只能执行一个协程。
在JS中,生成器就是协程的一种实现方式。

asnyc/await

为了更近一步改进生成器代码,ES7引入了async/awit,实现了更加直观简洁的代码。
async/aswit技术背后的实现就是Promise和生成器应用。往底层说就是微服务和协程应用。

async:是一个经过异步执行并隐式返回Promise做为结果的函数。 await:咱们知道了 async 函数返回的是一个 Promise 对象,那下面咱们再结合文中这段代码来看看 await 究竟是什么。

async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)
//输出结果:0 3 100 2
复制代码

async/await 无疑是异步编程领域很是大的一个革新,也是将来的一个主流的编程风格。其实,除了 JavaScript,Python、Dart、C# 等语言也都引入了 async/await,使用它不只能让代码更加整洁美观,并且还能确保该函数始终都能返回 Promise。

相关文章
相关标签/搜索