laor
2023/03/12阅读:17主题:全栈蓝
JS中的异步编程
前言
JavaScript 在浏览器端运行是单线程的,这是由浏览器(运行环境)决定的,这种行为是为了避免多线程执行不同任务会发生冲突的情况。
-
JavaScript 代码只在一个线程上运行,称之为主线程。 -
即使HTML5提供了web worker API可以让浏览器开一个线程运行比较复杂耗时的 JavaScript任务,但是这个线程仍受主线程的控制。
当然,这不是这篇文章讨论的重点。
为了解决耗时任务阻塞执行的问题,从而有了同步模式和异步模式。
同步模式和异步模式
同步模式(Synchronous)
-
大多数情况下的代码都是在同步模式执行,请注意不是同时执行,而是按照顺序执行。 -
当某一段代码执行时间过长,后面的代码的执行就会被阻塞。对于用户来说就会有页面卡顿卡死。
console.log('global begin')
function bar () {
console.log('bar task')
}
function foo () {
console.log('foo task')
bar()
}
foo()
console.log('global end')
异步模式(Asynchronous)
-
不会去等待这个任务执行结束才开始执行下一个任务。 -
任务开启过后立即开启下一个任务。 -
后续逻辑一般通过定义回调函数的方式去处理。 -
这样单线程的JavaScript语言可以同时处理大量耗时任务。 -
但是,「代码执行顺序可能比较混乱,跳跃,可读性差。」
console.log('global begin')
setTimeout(function timer1() {
console.log('timer1 invoke')
}, 1800)
setTimeout(function timer2() {
console.log('timer2 invoke')
setTimeout(function inner() {
console.log('inner invoke')
}, 1000)
}, 1000)
console.log('global end')
// 依次打印 global begin、global end、timer2 invoke、timer1 invoke、inner invoke
相关概念
-
「执行JavaScript代码的线程只有一个」,但「浏览器并不是单线程」, setTimeout倒计时的工作交给浏览器的 异步调用线程
。 -
函数调用栈 -
消息队列与事件循环,事件循环机制会监听调用栈及消息队列,当调用栈为空,就去消息队列取第一个任务来入栈执行,若消息队列也为空,则暂停执行,当消息队列入栈任务时,又重新开始从消息队列取第一个任务入栈执行。
代码分析
-
入栈
console.log
函数,打印global begin
,出栈; -
入栈
setTimeout
函数执行并出栈,同时为timer1
开启一个倒计时器放入异步调用线程计时; -
入栈
setTimeout
函数执行并出栈,同时为timer2
开启一个倒计时器放入异步调用线程计时; -
入栈
console.log
函数,打印global end
,出栈;1000ms后...
-
timer2
放入消息队列,事件循环机制监听到后,将timer2
函数入栈并执行; -
入栈
console.log
函数,打印timer2 invoke
,出栈; -
入栈
setTimeout
函数执行并出栈,同时为inner
开启一个倒计时器放入异步调用线程计时,然后timer2
出栈;再过800ms...
-
timer1
放入消息队列,事件循环机制监听到后,将timer1
函数入栈并执行; -
入栈
console.log
函数,打印timer1 invoke
,出栈,然后timer1
出栈;再过200ms...
-
inner
放入消息队列,事件循环机制监听到后,将inner
函数入栈并执行; -
入栈
console.log
函数,打印inner invoke
,出栈,然后inner
出栈;

回调函数
-
回调函数是所有异步编程方案的根基。 -
由调用者定义,交给执行者执行的函数称为「回调函数」。
// 回调函数
function foo(callback) {
setTimeout(function () {
callback()
}, 3000)
}
foo(function () {
console.log('这就是一个回调函数')
console.log('调用者定义这个函数,执行者执行这个函数')
console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})
-
直接使用传统回调方式去完成复杂的异步流程
// 回调地狱,只是示例,不能运行
$.get('/url1', function (data1) {
$.get('/url2', data1, function (data2) {
$.get('/url3', data2, function (data3) {
$.get('/url4', data3, function (data4) {
$.get('/url5', data4, function (data5) {
$.get('/url6', data5, function (data6) {
$.get('/url7', data6, function (data7) {
// 略微夸张了一点点
})
})
})
})
})
})
})
因此,CommonJS社区率先提出了Promise的规范,在ES2015中被规范化,成为语言规范。
Promise

基本用法
const promise = new Promise(function (resolve, reject) {
// 这里用于“兑现”承诺
// resolve(100) // 承诺达成
reject(new Error('promise rejected')) // 承诺失败
})
promise.then(function (value) {
// 即便没有异步操作,then 方法中传入的回调仍然会被放入队列,等待下一轮执行
console.log('resolved', value)
}, function (error) {
console.log('rejected', error)
})
console.log('end')
Promise 方式的 AJAX
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () { // readyState = 4
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax('/api/foo.json').then(function (res) {
console.log(res)
}, function (error) {
console.log(error)
})
-
嵌套使用 Promise 是最常见的误区 -
正确做法是使用Promise then方法链式调用的特点保证异步任务扁平化
// 嵌套使用 Promise 是最常见的误区
ajax('/api/urls.json').then(function (urls) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
})
})
})
})
})
链式调用
-
Promise的then方法会返回一个全新的Promis对象,后面的then方法就是在为上一个then返回的Promise注册的回调 -
前面then方法中回调函数的返回值会作为后面then方法回调的参数 -
如果回调中返回的还是Promise,那后面then方法的回调会等待它的结束,并接收它返回的结果
ajax('/api/users.json')
.then(function (value) {
console.log(1111)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(2222)
console.log(value)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(3333)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(4444)
return 'foo'
}) // => Promise
.then(function (value) {
console.log(5555)
console.log(value) // foo
})
Promise异常处理
-
then方法的第二个参数onRejected函数接收当前Promise的异常(Promise失败或者出现报错等)。
-
Promise实例的catch方法注册onRejected回调,适合链式调用,更常用。
-
区别
a. catch方法是对then方法返回的Promise注册的异常回调,而then方法的第二个参数只是对当前Promise注册的异常回调。
b. Promise链条上任何的异常都会被向后传递直到被捕获。then的第二个参数只捕获当前Promise的异常,而catch能捕获到链条里的所有异常,所以最后一个then里面的异常只能被catch捕获。
-
全局注册
unhandledrejection
进行全局捕获Promise异常。 -
应该在代码中明确的捕获每一个可能的异常,而不是丢给全局去统一处理。
function ajax() {
return new Promise(function (resolve, reject) {
foo()
// throw new Error()
setTimeout(() => {
resolve()
// reject(new Error('error msg'))
})
})
}
ajax()
.then(function onFulfilled(value) {
foo1()
console.log('onFulfilled', value)
}, function onRejected(error) {
console.log('onRejected', error) // onRejected ReferenceError: foo is not defined
}).catch(err => {
// foo()注释时,err ReferenceError: foo1 is not defined
// then没onRejected回调时,err ReferenceError: foo is not defined
console.log('err', err)
})
// 全局捕获 Promise 异常,类似于 window.onerror
window.addEventListener('unhandledrejection', event => {
const { reason, promise } = event
console.log(reason, promise)
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象
event.preventDefault()
}, false)
// Node.js 中使用以下方式
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise)
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象
})
Promise静态方法
-
Promise.resolve
-
传入一个值。转换为一个状态为「fulfilled」的Promise对象 -
如果传入的是一个 Promise 对象, Promise.resolve
方法原样返回 -
如果传入的是带有一个跟 Promise 一样的 then 方法的对象, Promise.resolve
会将这个对象作为 Promise 执行
Promise.resolve('foo')
.then(function (value) {
console.log(value) //foo
})
// 等同于 ---->
new Promise(function (resolve, reject) {
resolve('foo')
})
// 如果传入的是一个 Promise 对象,Promise.resolve 方法原样返回
var promise = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
console.log(promise === promise2)
// 如果传入的是带有一个跟 Promise 一样的 then 方法的对象,
// Promise.resolve 会将这个对象作为 Promise 执行
Promise.resolve({
then: function (onFulfilled, onRejected) {
onFulfilled('foo')
}
})
.then(function (value) {
console.log(value) // foo
})
-
Promise.reject
:传入任何值,都会作为这个 Promise 失败的理由
Promise.reject(new Error('rejected'))
.catch(function (error) {
console.log(error) // Error: rejected
})
Promise.reject('anything')
.catch(function (error) {
console.log(error) //anything
})
Promise 并行执行
-
Promise.all将多个Promise合并成一个Promise处理,都成功才成功,有一个失败就失败。 -
Promise.race方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
var promise = Promise.all([
ajax('/api/users.json'),
ajax('/api/posts.json')
])
promise.then(function (values) {
console.log(values)
}).catch(function (error) {
console.log(error)
})
// Promise.race 实现超时控制
const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('timeout')), 500)
})
Promise.race([
request,
timeout
])
.then(value => {
console.log(value)
})
.catch(error => {
console.log(error)
})
Promise执行时序/ 宏任务 VS 微任务
-
回调队列中的任务称为「宏任务」 -
当前宏任务的「微任务」,会在当前任务执行结束后立即执行,常见的微任务: Promise
、MutationObserver
、Node中的process.nextTick
console.log('global start')
// setTimeout 的回调是 宏任务,进入回调队列排队
setTimeout(() => {
console.log('setTimeout')
}, 0)
// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
.then(() => {
console.log('promise')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
console.log('promise 3')
})
console.log('global end')
-
promise的链式调用解决了嵌套的回调函数地狱。 -
但是链式调用的回调函数的可读性不够好,不如同步代码。
Generator异步执行方案(ES2015)
-
调用生成器函数。生成生成器对象 generator
,不会立即执行生成器函数函数体 -
调用 generator.next
,函数体开始执行,直到yield
关键字,yield
后面的值,作为返回对象的value进行返回,函数体暂停执行。 -
再调用 next
方法,并传递参数,此参数将作为yield
语句的返回值,并继续执行函数体,直到下一个yield
位置。 -
generator.throw
抛出一个异常
// 生成器函数
function * foo () {
console.log('start')
try {
const res = yield 'foo' // yield暂停生成器函数的执行
console.log(res) // bar
} catch (e) {
console.log(e) // Error: Generator error
}
}
const generator = foo() // 不会立即执行这个函数的函数体,而是得到一个生成器对象
const result = generator.next()
console.log(result) // {value: 'foo', done: false}
// generator.next('bar')
let result1 = generator.throw(new Error('Generator error')) // done 为false时才能调用该语句进行捕获
console.log(result1) // {value: undefined, done: true}
用同步的方法实现异步逻辑:
function* main() {
try {
const users = yield ajax('/api/users.json')
console.log(users)
const posts = yield ajax('/api/posts.json')
console.log(posts)
const urls = yield ajax('/api/urls11.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
const result = g.next()
result.value.then(data => {
const result2 = g.next(data)
if (result2.done) return
result2.value.then(data => {
const result3 = g.next(data)
if (result3.done) return
result3.value.then(data => {
g.next(data)
})
})
})
以上代码进行一个递归优化:
function co(generator) {
const g = generator()
function handleResult(result) {
if (result.done) return // 生成器函数结束
result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}
handleResult(g.next())
}
co(main)
可见用Generator的方式需要一个co执行器辅助函数
Async / Await 语法糖:语言层面的异步编程标准(ES2017)
-
Async函数实际生成器函数一种更加方便的语法糖,写法和上面的实现非常类似 -
返回一个Promise对象
async function main () {
try {
const users = await ajax('/api/users.json')
console.log(users)
const posts = await ajax('/api/posts.json')
console.log(posts)
const urls = await ajax('/api/urls.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
作者介绍