同步与异步模式
js最初是设计使用在浏览器上的脚本语言,由于需要对DOM进行操作,因此是单线程的执行语言。
同步模式
- 非同步执行而是排队执行;
- 变量或函数的声明不会产生任何的调用;
- js在执行引擎当中维护了一个正在工作(执行)的工作表,里面记录当前的执行任务,当工作表中所有的任务被清空,这一轮的工作结束;
- 排队执行会存在如果遇到耗时多的任务,那么后面的任务就会被延迟执行->阻塞。
异步模式
// 异步举例
console.log('global begin')
setTimeout(function timer1() {
console.log('timer1 invoked')
}, 1800)
setTimeout(function timer2() {
console.log('timer2 invoked')
setTimeout(function inner() {
console.log('inner invoked')
}, 1000);
}, 1000);
console.log('global end')
// global begin
// global end
// timer2 invoked
// timer1 invoked
// inner invoked
- 如果没有异步模式,单线程的js无法同时处理大量的耗时任务;
- 难点:代码的执行顺序混乱;
- 下达这个任务开启的指令然后继续往下执行,不会等待任务结束。
js实现异步编程的4种方法:
4种解决方式的根本都是利用了浏览器定时器的工作原理。
回调函数
异步编程最基本的方法。
优点:简单易理解。
缺点:不利于代码阅读和维护,各部分之间高度耦合,流程会很混乱,而且每个任务只能指定一个回调函数。事件监听
采用事件驱动模式,任务执行不取决于代码的执行顺序,而是某个事件是否发生。
优点:易理解,可以绑定多个事件,每个事件可以绑定多个回调函数,能够“去耦合”,有利于实现模块化。
缺点:整个程序变成事件驱动型,运行流程不清晰。发布/订阅
假设存在一个“信号中心”,某个任务执行完成,就向信号中心“发布(publish)”一个信号,其他任务可以向信号中心“订阅(subscribe)”这个信号,从而知道自己什么时候开始执行任务,这就是“发布/订阅模式”,也称“观察者模式”。
这种方法的性质与事件监听类似,但是可以通过“信号中心”查看,了解有多少信号、每个信号有多少订阅者,从而监控程序的运行。
- Promise对象
回调函数
由调用者定义,交给执行者执行的函数。
事件循环与消息队列
js引擎线程会维护一个执行栈(调用栈call stack),同步代码会依次加入执行栈并执行,结束会退出执行栈。
js引擎线程如果遇到异步(DOM事件监听、网络请求、setTimeout
计时器等),会交给单独的线程(⚠️Web APIs)维护异步任务,直到满足一定条件(用户点击DOM、网络请求成功、计时器结束),由事件触发线程将异步对应的回调函数封装成任务并加入消息队列。
如果执行栈为空,事件循环就会启动,从消息队列中取出一个任务(即异步的回调函数)放入执行栈中执行。
- 事件循环
在线程运行过程中,接收并执行新的任务,
- 消息队列
消息队列是一种数据结构,可以存放要执行的任务,类似于待办事件列表。
异步编程的几种方式
Promise异步方案、宏任务/微任务队列
Promise异步方案
Promise对象用于表示一个异步操作的最终完成(或失败)及其结果值。
Promise有三个状态:
1.pending([待定]初始状态)
2.fulfilled([实现]操作成功)
3.rejected([被否决]操作失败)
当Promise状态发生改变,就会触发.then()
里的响应函数处理后续步骤,由于.then()
和.catch()
方法返回的是一个新的Promise对象,因此它们可以被链式调用。
- Promise基本用法
// Promise 基本实例
const promise = new Promise((resolve, reject) => {
// 这里用于“兑现”承诺
resolve('100') // 承诺达成
reject(new Error('promise rejected!')) // 承诺失败
})
promise.then((value) => {
console.log('resolved', value)
}, (error) => {
console.log('rejected', error)
})
- Promise使用案例
// Promise方式的AJAX
function ajax(url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(this.statusText)
}
}
xhr.send()
})
}
Promise常见误区与链式调用
嵌套使用的方式是使用Promise最常见的错误,应该经尽可能保证异步任务的扁平化。
链式调用:
每一个.then()
方法实际上都是为上一个.then()
返回的Promise对象添加状态明确过后的回调。通过链式调用避免回调的嵌套。Promise异常处理
最好使用catch明确捕获每一个异常。-
Promise静态方法
-
Promise.resolve()
:快速地把一个值转换成Promise对象;如果包裹一个Promise对象,那么该Promise对象会被原样返回;还可以传入一个有then方法的Promise对象,一般用于将第三方库的Promise对象转换为原生的Promise对象 -
Promise.reject()
:快速地创建一个失败的Promise对象
-
-
Promise并行执行
同步执行多个Promise的方式:-
Promise.all()
:接收一个包含Promise对象的数组,将其中的Promise对象看作一个个异步任务,返回一个全新的Promise对象,等待所有的任务结束 -
Promise.race()
:只会等待第一个任务结束
-
宏任务/微任务队列
回调队列中的任务称之为「宏任务」
事件循环作为任务驱动的主线程,首先执行完调用栈上当前的宏任务(同步任务),然后再遍历微任务队列,把微任务队列上所有任务都执行完毕(清空微任务队列)(微任务也可以往微任务队列中添加微任务),接着渲染线程,最后从宏任务队列中取一个任务,进入下一个消息循环。
宏任务执行过程中可以临时加上一些额外需求,对于额外需求可以选择作为一个新的宏任务进到队列中排队,也可以作为当前任务的「微任务」,直接在当前任务结束过后立即执行,而非到队伍末尾重新排队。
Promise的回调会作为微任务执行,setTimeout以宏任务的形式进入队列末尾。
微任务的提出是为了提高整体的响应能力。
目前绝大多是异步调用都是作为宏任务执行,Promise&MutationObserver、process.nextTick会作为微任务执行。
-
产生宏任务的方式
- script中的代码块
- setTimeout()
- setInterval()
- setImmediate()(非标准、IE和Node.js中支持)
- 注册事件
-
产生微任务的方式
- Promise
- MutationObserver
- queueMicrotask()
何时使用微任务
微任务执行的时机,晚于当前本轮事件循环的Call Stack(调用栈)中的代码(宏任务),早于时间处理函数和定时函数。
使用微任务的最主要原因简单归纳为:
1.减少操作中用户可感知到的延迟(微任务中操作dom之后立即渲染);
2.确保任务顺序的一致性,即便是结果或数据是同步可用的;
3.批量操作的优化。
Generator异步方案、Async/Await语法糖
Generator异步方案
Generator函数是一个封装的异步任务,或者说是异步任务的容器。
- generator由
function *
定义,不同于普通函数,可以暂停执行; - 异步操作需要暂停的地方,用
yield
语句注明; - 调用
next()
执行generator函数,从上次返回的yield
语句处继续执行。
Async/Await语法糖
语言层面的异步编程标准。
async
函数返回一个Promise对象,await
等待接收async
函数的返回值。是Generator的语法糖。