一. 线程模型
第一版——单线程
通过单线程处理安排好的任务
- 任务代码已经按照顺序写进线程里,线程按顺序执行这些任务
第二版——事件循环机制
线程处理过程中,接收并执行新任务
- 循环机制:线程语句后添加for 循环语句。线程循环执行
- 事件:执行过程中可以暂停等待用户输入
第三版——消息队列
线程处理其他线程发送过来的任务
- 消息队列:数据结构,存放要执行的任务,先进先出
- 任务类型:输入事件、微任务、文件读写、WebSocket、JS定时器等;JS执行、解析DOM、样式计算、布局计算、CSS动画等
- 实现步骤:
- 构造队列,成为消息队列
- 其他线程的任务添加至队列尾部
- 主线程从队列头部中读取任务并执行
- 安全退出:设置一个退出标志变量
- Chrome:渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程
- 单线程消息队列的缺点:
- 执行效率低,效率和实时性无法统筹兼顾
1.1 解决方法:微任务 - 单个任务执行时间过长
2.1 解决方法:回调功能
- 执行效率低,效率和实时性无法统筹兼顾
二. WebAPI——事件循环的应用
1. setTimeout
定时器,指定某个函数在多少毫秒后执行
- 要求:为保证回调函数能在指定时间内执行,不能将定时器的回调函数直接添加到消息队列中。
- 解决:渲染进程会将定时器的回调任务添加到延迟队列中
延迟队列:另外一个消息队列,这个队列中维护了 需要延迟执行的任务列表,包括定时器和 Chromium 内部一些需要延迟执行的任务
- 实现步骤:
- 渲染进程创建一个回调任务,添加到延迟队列
- 消息循环系统调用内部专门用来处理延迟任务的函数(ProcessDelayTask 函数),执行延迟任务
- 执行完成后,循环进入下一个任务
- 注意事项:
- 如果当前任务执行时间过久,会影延迟到期定时器任务的执行
- 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
- 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
- 延时执行时间有最大值
- 使用 setTimeout 设置的回调函数中的 this 不符合直觉,指向全局环境,使用匿名函数或 bind 方法解决
2. XMLHttpRequest
从 Web 服务器获取数据,操作 DOM 来更新页面内容,整个过程只更新网页的一部分,不会刷新整个页面
-
准备知识:
- 同步回调:回调函数 callback 在主函数返回之前执行的过程,是在当前主函数的上下文中执行回调函数
- 异步回调:回调函数在主函数外部执行的过程。
2.1 一种情况把异步函数做成一个任务,添加到信息队列尾部
2.2 另一种是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务
将一个函数作为参数传递给另外一个函数,作为参数的这个函数就为回调函数
// 回调函数 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) // 异步函数 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)
- 系统调用栈:类似于JS的调用栈,当循环系统在执行一个任务的时候,都要为这个任务维护一个系统调用栈
-
运行机制:
- 创建 XMLHttpRequest 对象,用来执行实际的网络请求
- 为xhr 对象注册回调函数
ontimeout,用来监控超时请求,如果后台请求超时了,该函数会被调用;
onerror,用来监控出错信息,如果后台请求出错了,该函数会被调用;
onreadystatechange,用来监控后台请求过程中的状态,比如可以监控到 HTTP 头加载 完成的消息、HTTP 响应体消息以及数据加载完成的消息等
-
配置基础请求信息
-
发起请求
4.1 渲染进程会将请求发送给网络进程然后网络进程负责资源的下载,等网络进程接收到数据之后,就会利用 IPC 来通知渲染进程
4.2 渲染进程接收到消息之后,会将 xhr 的回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候, 就会根据相关的状态来调用对应的回调函数如果网络请求出错了,就会执行 xhr.onerror;
如果超时了,就会执行 xhr.ontimeout;
如果是正常的数据接收,就会执行 onreadystatechange 来反馈相应的状态。
function GetWebData(URL){ /** * 1: 新建 XMLHttpRequest 请求对象 */ let xhr = new XMLHttpRequest() /** * 2: 注册相关事件回调处理函数 */ xhr.onreadystatechange = function () { switch(xhr.readyState){ case 0: // 请求未初始化 console.log(" 请求未初始化 ") break; case 1://OPENED console.log("OPENED") break; case 2://HEADERS_RECEIVED console.log("HEADERS_RECEIVED") break; case 3://LOADING console.log("LOADING") break; case 4://DONE if(this.status == 200||this.status == 304){ console.log(this.responseText); } console.log("DONE") break; } } xhr.ontimeout = function(e) { console.log('ontimeout') } xhr.onerror = function(e) { console.log('onerror') } /** * 3: 打开请求 */ xhr.open('Get', URL, true);// 创建一个 Get 请求, 采用异步 /** * 4: 配置参数 */ xhr.timeout = 3000 // 设置 xhr 请求的超时时间 xhr.responseType = "text" // 设置响应返回的数据格式 xhr.setRequestHeader("X_TEST","time.geekbang") /** * 5: 发送请求 */ xhr.send(); }
-
注意事项
-
跨域问题:默认情况下,跨域请求是不被允许的
-
HTTPS 混合内容:包含了不符合 HTTPS 安全要求的内容
混合内容: HTTP 资源,通过 HTTP 加载的图像、 视频、样式表、脚本等
-
三. 微任务 vs 宏任务
宏任务
消息队列中的任务称为宏任务
- 事件循环系统机制:
- 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
- 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任 务;
- 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
- 最后统计执行完成的时长等信息
- 不足之处:难以完成对时间精度要求较高的需求,如页面的渲染事件、各种 IO 的完成事件、执行 JavaScript 脚本的事件、用户交互的事件
微任务
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
可以在实时性和效率之间做一个有效的权衡
在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列,用来存放微任务
-
产生方式:
-
第一种方式:使用 MutationObserver ,通过监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务
-
第二种方式:使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务
-
-
执行时机
WHATWG 把执行微任务的时间点称为检查点
- 在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务
-
注意:如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行
微任务应用
1. MutationObserver
采用了 “异步 + 微任务” 的策略:
- 通过异步操作解决了同步操作的性能问题
- 通过微任务解决了实时性的问题
- 响应函数改成异步调用,可以不用每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化
- 采用微任务保持消息通知的及时性,在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。当执行到检查点的时候,V8 引擎就会按照顺序执行微任务
2. Promise
- 与微任务关系
- 通过微任务将定时器改造从而实现回调函数延时调用,提升代码的执行效率
// 原来resolve延迟调用onResolve_是使用定时器的方法
function resolve(value) {
setTimeout(()=>{
onResolve_(value)
},0)
}
二者联系
- 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列
- 微任务的执行时长会影响到当前宏任务的时长
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行
四. 异步编程的改进
Promise
异步回调:
- 页面编程的一大特点。
异步编程模型:
- 在执行一项耗时的任务时,比如下载网络文件任务、获取摄像头等设备信息任务, 这些任务都会放到页面主线程之外的进程或者线程中去执行,等该进程处理完这个任务后,会将该任务添加到渲染进程的消息队列中,并排队等待循环系统的处理。排队结束之后,循环系统会取出消息队列中的任务进行处理,并触发相关的回调操作
- 产生原因:异步回调代码逻辑不连贯、不线性,不符合人的直觉,影响了我们的编码方式,从而出现 Promise 封装异步代码
使用
function XFetch(request) {
function executor(resolve, reject) {
let xhr = new XMLHttpRequest()
xhr.open('GET', request.url, true)
xhr.ontimeout = function (e) { reject(e) }
xhr.onerror = function (e) { reject(e) }
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200) {
resolve(this.responseText, this)
} else {
let error = {
code: this.status,
response: this.response
}
reject(error, this)
}
}
}
xhr.send()
}
return new Promise(executor)
}
var x1 = XFetch(makeRequest('https://time.geekbang.org/?category'))
var x2 = x1.then(value => {
console.log(value)
return XFetch(makeRequest('https://www.geekbang.org/column'))
})
var x3 = x2.then(value => {
console.log(value)
return XFetch(makeRequest('https://time.geekbang.org'))
})
x3.catch(error => {
console.log(error)
})
说明:
- 引入 Promise,在调用 XFetch 时,会返回一个 Promise 对象。
- 构建 Promise 对象时,需要传入一个executor 函数,XFetch 的主要业务流程都在 executor 函数中执行。
- 如果运行在 excutor 函数中的业务执行成功了,会调用 resolve 函数;
- 如果执行失败 了,则调用 reject 函数。
- 在 excutor 函数中调用 resolve 函数时,会触发 promise.then 设置的回调函数;而调用 reject 函数时,会触发 promise.catch 设置的回调函数
功能
- 消灭嵌套调用
1.1 回调函数延时绑定
1.2 将回调函数创建好的 Promise 对象返回到最外层
// 创建 Promise 对象 x1,并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
resolve(100)
}
let x1 = new Promise(executor)
//x1 延迟绑定回调函数 onResolve
function onResolve(value){
console.log(value)
let x2 = new Promise((resolve, reject) => {
resolve(value + 1)
})
console.log(x2)
return x2
}
let x2 = x1.then(onResolve)
x2.then((value) => {
console.log(value)
console.log(x2)
})
- 合并多个错误
2.1 冒泡性质,使用最后一个对象捕获所有异常
function executor(resolve, reject) {
let rand = Math.random();
console.log(1)
console.log(rand)
if (rand > 0.5)
resolve()
else
reject()
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
console.log("succeed-1")
return new Promise(executor)
})
var p3 = p1.then((value) => {
console.log("succeed-2")
return new Promise(executor)
})
var p4 = p3.then((value) => {
console.log("succeed-3")
return new Promise(executor)
})
p4.catch((error) => {
console.log("error")
})
console.log(2)
async/await
使用同步代码实现异步访问资源的能力,并且使得代码逻辑更 加清晰
生成器 vs 协程
-
生成器函数:带星号函数,可以暂停执行和恢复执行,协程的实现
1.1 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
1.2 外部函数可以通过 next 方法恢复函数的执行。 -
协程:一种比线程更加轻量级的存在,是跑在线程上的任务。
一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。
如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程
function* genDemo() {
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')
说明:
-
通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
-
要让 gen 协程执行,需要通过调用 gen.next。
-
当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
-
如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程, 并将 return 后面的内容返回给父协程。
-
协程配合Promise
执行器:执行生成器的代码封装成的函数
//foo 函数 同步代码实现异步操作
function* foo() {
let response1 = yield fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}
/*
执行器 co框架
co(foo());
*/
// 执行 foo 函数的代码
let gen = foo() // 创建 gen 协程
function getGenPromise(gen) {
return gen.next().value // 主线程的控制权交给父协程
}
getGenPromise(gen).then((response) => {
console.log('response1')
console.log(response)
return getGenPromise(gen)
}).then((response) => {
console.log('response2')
console.log(response)
})
async
异步执行、隐式返回 Promise
- 隐式返回 Promise
async function foo() {
return 2
}
console.log(foo()) // Promise {<resolved>: 2}
await
async function foo() {
console.log(1)
let a = await 100 // 1. 创建一个Promise对象,主线程控制权转交给父协程执行,并返回 promise_对象
console.log(a) // 4. foo 协程激活之后,会把刚才的 value 值赋给变量 a,然后 foo 协 程继续执行后续语句,执行完成之后,将控制权归还给父协程
console.log(2)
}
console.log(0)
foo()
// 2. 调用 promise_.then 来监控 promise状态
console.log(3)
// 3. 父协程执行结束,执行微任务队列里的 resolve(100) ,触发 promise_.then 中的回调函数,将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程
/*
0
1
3
100
2
*/
学习资料
李兵:《15丨消息队列和事件循环:页面是怎么“活”起来的?》
《16丨WebAPI:setTimeout是如何实现的?》
《17丨WebAPI:XMLHttpRequest是怎么实现的?》
《18丨宏任务和微任务:不是所有任务都是一个待遇》
《19丨Promise:使用Promise,告别回调函数》
《20丨async-await:使用同步的方式去写异步代码》