JavaScript异步编程
璃安猫: 观看学习视频总结的一些异步编程的相关知识点,还有许多不完善的地方,之后会逐渐补充,若有不对之处烦请指正。
JS工作模式
JS是单线程的,但浏览器不是单线程的 即API不是单线程
JavaScript采用单线程模式工作的原因
JS是在浏览器端的脚本语言,目的是为了实现页面的动态交互(通过DOM操作),因此采用单线程避免出现复杂的线程同步问题,成为特性之一
单线程:JS执行环境中负责执行代码的线程只有一个
JS将任务执行模式分成了两种 同步模式(Synchrorous) / 异步模式(Asynchronous)
这里所说的同步/异步模式指的是运行环境提供的API是以同步/异步模式的方式工作 而不是JS的运行模式
同步模式
执行代码方式与编写代码一致,并不是同时执行,而是排队执行
//首先将以下代码添加到一个匿名函数内 然后逐行执行
//压入调用栈 执行完毕后出栈
console.log('global begin')
//函数 / 变量的声明不会产生调用
function bar () {
//打印结果 执行完毕出栈
console.log('bar task')
}
function foo() {
// 打印结果
console.log('foo task')
// bar函数调用 压入调用栈 执行完毕后出栈
bar()
}
//foo函数调用 压入调用栈
foo()
//最后压入调用栈 执行完毕后清空调用栈
//此时为纯同步的任务执行情况
//存在问题:
//存在阻塞——若中间某一任务执行时间过长,则后续任务则会被延迟执行
console.log('global end')
使用异步模式用于解决耗时任务的问题 如Ajax node.js 的大文件读写
异步模式
不会去等待这个任务的结束才开始下一个任务
任务开启过后就会执行下一个任务 后续逻辑一般会通过回调函数的方式定义
因为单线程的Js无法同时处理大量耗时任务
问题 代码执行顺序混乱
console.log('global begin');
//将计数器放入API计时后继续执行
setTimeout( function timer1() {
console.log('timer1 inxole')
}, 1800)
//将计数器放入API计时后继续执行
setTimeout(function timer2() {
console.log('timer1 invoke')
setTimeout(function inner() {
console.log('inner invoke')
}, 1000)
}, 1000)
console.log('global end')
//事件回调
//API计时器结束则进入事件回调
//以上例子 先结束timer2() 然后将inner的计数器放入API
//接下来timer1()结束 执行结束后出栈
//inner计数器结束后 执行结束后出栈
总结:
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
JS异步编程
回调函数
回调函数是所有异步编程方案的根基,由调用者定义并交给执行者执行的函数
ES5后为异步而生的的语法
function foo (callback) {
setTimeout(function () {
callback()
}, 3000)
}
foo(function () {
console.log('回调函数')
console.log('调用者定义,执行者执行')
console.log('调用者告诉执行者异步任务结束后应该做什么')
})
Promise
promise——一种更优的异步编程统一方案,为了解决回调函数嵌套导致的回调地狱问题
其本质为对象,表示异步任务最终结果是成功还是失败,因此Promise有三种状态:pending(待定)、fulfilled(成功)、Rejected(失败)
Promise用法
//Promise 基本示例
//其中接收两个参数 resolve 和 reject 都是函数
const promise = new Promise( function (resolve, reject) {
//这里用于“兑现”承诺
//只能调用以下两者的其中一个
//resolve 将 promise状态修改为fulfilled 将resolve的结果传递出去
resolve(100) //承诺达成
//将peomise状态修改为rejected失败 返回失败理由
// reject(new Error('promise rejected'))
})
//promise的then方法去指定其onfulfilled 和 onrejected 的回调函数
//第一个参数为成功的回调函数 第二个参数为失败的回调函数
//即便是承诺内无内容 then方法也需要进入队列,等待同步操作执行完了 才会执行
promise.then(function (value) {
//打印得到的惨呼
console.log('resolved', value);
}, function ( error ) {
console.log('rejected', error);
})
//先打印该字符串 才会执行then方法内的内容
console.log('end');
Promise使用案例
//Promise 方式的 AJAX
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
//执行回调函数
ajax('/api/users.json').then(function (res) {
console.log(res);
}, function (error) {
console.log(error);
})
//返回成功
//当url改成不存在的文件时 返回Error not found
Promise常见误区
promise本质上是使用回调函数定义的异步任务结束后所需要执行的任务,回调函数是通过then方法传递进去的,并且将回调函数分为了两种 :onfulfilled 和 onrejected
错误用法:若执行串联promise的任务,仍会出现回调函数嵌套的问题
//Promise 方式的 AJAX
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
//执行回调函数 多个连续的请求仍然会形成回调地狱
//嵌套使用是使用promise最常见的误区
//正确做法是借助Promise then 方法链式调用的特点去保证异步任务的扁平化
ajax('/api/users.json').then(function (urls) {
ajax(urls.users).then(function (users){
ajax(urls.users).then(function (users){
ajax(urls.users).then(function (users){
})
})
})
})
Promise链式调用
//Promise 的链式调用
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
var promise = ajax('/api/users.json')
//执行回调函数 返回结果也是一个promise对象
var promise2 = promise.then(
//成功后的回调
function onFulfilled (value) {
console.log('onFulfilled', value);
},
//失败后的回调
function onRejected (error) {
console.log('onRejected', error);
}
)
console.log(promise2); //打印一个Promise对象
console.log(promise === promise2); //并不是一个对象
// //因此并不是返回的this方法的链式调用
//执行完一个回调任务后,再去返回一个新的承诺,相互之前无影响
//链式then方法是依次执行 可以在then当中手动回调一个promise对象
ajax('/api/users.json')
.then(function (value) {
console.log(1111);
return ajax('/api/users.json')
}) // => promise添加状态名并回调
//每个then方法实际上是为了上一个then方法返回的promise对象添加状态名过后的回调
//此时的then执行的则是上一个return的ajax的回调任务 可以避免回调的嵌套
.then(function (value) {
console.log(2222);
//则是返回的promise的返回的值 下一个then方法的获取值则是该值
//若没有返回任何值 则默认返回undefined
return 'foo'
})
.then(function (value) {
console.log(value);
})
.then(function (value) {
console.log(4444);
})
.then(function (value) {
console.log(5555);
})
- Promise 对象的then方法会返回一个全新的Promise对象
- 后面的then方法就是在为上衣个then返回的Promise注册回调
- 前面then方法中回调函数的返回值会作为后面then方法回调的参数
- 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束
Promise 异常处理
//Promise 异常处理
function ajax (url) {
return new Promise(function (resolve, reject) {
// foo() 执行onRejected
// throw new Error() 执行onRejected
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
//为promise当中的异常做一些处理
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
//文件路径不存在则会执行请求失败的回调函数
//若promise执行异常 或手动抛出一个异常 onRejected的回调也会被执行
ajax('/api/users.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value);
return ajax('/error-url') //不存在一定失败 但并没有被捕获到
}, function onRejected (error) {
//promise失败或是异常都会被执行
console.log('onRejected', error);
})
//可以用catch方法来注册promise失败/异常时的回调
ajax('/api/users.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value);
return ajax('/error-url') //该异常被捕获到
}) // => promise {}
// catch更为常用 利于链式调用
//是给上一个then返回的promise做回调 而不是第一个promise
//能够捕获到后续then操作中的promise的异常 而上方的例子无法捕获其他then返回的promise异常
.catch(function onRejected (error) {
//promise失败或是异常都会被执行
console.log('onRejected', error);
})
//不推荐使用 最好是在代码中明确捕获每一个可能的异常
//可以在全局上注册一个unhandledrejection事件 处理代码中没有被手动捕获的promise异常
window.addEventListener('unhandledrejection', event => {
const { reason, promise } = event
console.log(reason, promise);
//reason => Promise 失败原因, 一般是一个错误对象
// promsie => 出现异常的Promise对象
event.preventDefault()
},false)
//node当中 process当中注册以下事件
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise);
//reason => Promise 失败原因, 一般是一个错误对象
// promsie => 出现异常的Promise对象
})
Promise静态方法
Promise.resolve() - 快速把一个值转换成promise对象
//常用 Promise 静态方法
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
// 返回状态为fulfilled的Promise对象 作为promise对象的返回值
Promise.resolve('foo')
.then(function (value) {
console.log(value); //'foo'
})
// //等价于下面的例子
new Promise(function (resolve, reject) {
resolve('foo')
})
//接收到的为promise对象 则会原样返回该promise对象
var promise = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
//返回true 说明Promise.resolve原样返回promise
console.log(promise === promise2);
//传入对象也可以作为一个promise对象被执行
//对象中的then提供了一个接口 可以被then的对象
//利于第三方的promise转换成原生的promise对象
Promise.resolve({
then: function (onFulfilled, onRejected) {
onFulfilled('foo')
}
})
.then(function (value) {
console.log(value);
})
//用于创建一定是失败的promise对象 传入的参数会作为promise失败的理由
Promise.reject(new Error('rejected'))
.catch(function (error) {
console.log(error);
})
Promise并行执行
Promise也可以提供多个任务并行执行任务
有两个方法可以对promise异步任务并行执行
Promise.all() - 等待所有任务结束后才会结束(若有一个失败,则认定为失败并返回第一个失败的结果)
Promise.race() - 只会等待第一个结束的任务一起结束
ES2020新增Promise.allSettled() - 返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象表格,每个对象表示对应的promise结果。
//Promise 并行执行
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
//Ajax请求之间相互没有依赖 则可以使用并行执行同时请求
//Promise.all()
//对于多个同步任务的执行 可以使用all方法将多个promise合并为一个promise统一管理
//all方法会返回一个全新的promise对象 只有当all内的promise全部完成 其返回的promise才会完成
var promise = Promise.all([
ajax('/api/users.json'),
ajax('/api/users.json'),
ajax('/api/users.json'),
ajax('/api/users.json'),
ajax('/api/users.json'),
])
//promise完成后会返回一个数组,包含all内每个promise执行的结果
//执行中的promise有一个失败了 执行完毕后其也会因失败而结束
promise.then(function (values) {
console.log(values)
}).catch(function (error ) {
console.log(error);
})
ajax('/api/urls.json')
.then(value => {
console.log(value);
//将文件内的json对象中可枚举属性值转换成数组
const urls = Object.values(value)
//将urls数组内元素逐个执行ajax请求函数并整合成数组
const tasks = urls.map(url => ajax(url))
//使用promise并行执行
return Promise.all(tasks)
})
//promise.all并行执行任务的结果
.then(values => {
console.log(values);
})
//Promise.race() 也可以将多个promise对象组合成一个promise对象
//Promise.race() 只会等待第一个结束的任务然后结束
//paromise.all 是等待所有任务结束后才会结束
const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject) => {
//500毫秒过后请求失败
setTimeout(() => reject(new Error('timeout')), 500)
})
//500毫秒内执行完第一个请求任务可以返回结果 500毫秒过后则未执行完第一个请求任务 返回错误
Promise.race([
request,
timeout
])
.then( value => {
console.log(value);
})
.catch(error => {
console.log(error);
})
Promise.all() VS Promise.allSettled()
const arr = [Promise.resolve('1'), Promise.reject('2'), Promise.resolve('3')];
Promise.all(arr).then(value => console.log(value)).catch(err=>console.log(err))
// => 2
Promise.allSettled(arr).then(value => console.log(value)).catch(err=>console.log(err))
/**
* [
{ status: "fulfilled", value: "1" },
{ status: "rejected", reason: "2" },
{ status: "fulfilled", value: "3" },
]
*/
Promise执行时序
浏览理解:
宏任务指的是回调队列中的任务,宏任务当中有额外的任务(如绝大部分的异步调用)则会作为一个新的宏任务进到队列中排队
微任务是直接在当前任务结束过后立即执行
// 微任务
//回调队列中的任务称为[宏任务] 宏任务执行的过程中可能会临时加一些额外的需求 可选择作为一个新的宏任务进到队列中排队
//目前绝大部分异步调用都是作为宏任务执行
//微任务 : Promise / MutationObserver /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
详见本人文章 手写模拟Promise
Generator
Promise虽然拥有链式调用实现串联执行 但代码可读性较低 因此学习更优的异步编程调用的写法
Generator 除用于异步编程外,还用于构造迭代器
Generator 用法
//生成器函数仅在普通函数前多一个 *
function * foo () {
console.log('start');
try{
//可以使用 yield 关键词向外返回一个值
//返回对象当中还有一个done属性 表示是否全部执行结束
//并且不会立即结束该函数foo的执行 而是暂定该函数foo的执行 直到下一个next()操作 才会继续往下执行
const res = yield 'foo'
console.log(res);
} catch (e) {
console.log(e);
}
}
//调用生成器函数并不会立即执行 而是得到一个生成器对象
const generator = foo ()
//当调用生成器函数的 next() 方法才会开始执行
const result = generator.next()
console.log(result);
//传入参数时 则会作为 yield 的返回值
// generator.next('bar')
//对生成器内部抛出一个异常 内部再继续往下执行就会得到该异常
generator.throw(new Error('Generator error'))
Generator函数异步方案
// Generator 配合 Promise 的异步方案
const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = require("constants")
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
function * main () {
const users = yield ajax('/api/users.json')
console.log(users);
const posts = yield ajax('api/posts.json')
console.log(posts);
}
const g = main()
//此时 yield 返回对象的value就是返回的promise对象
const result = g.next()
//此时执行返回的promise的回调函数 可以拿到promise的执行结果
//使用递归的方式不断迭代 直到生成器函数执行结束过后结束递归
// 这样会无限调用造成地狱回调问题
result.value.then(data => {
//将得到的结果传给生成器函数 该函数则会继续往下执行 并且传入的数据会作为yield的返回值
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)
})
})
})
Generator异步递归调用优化
// Generator 配合 Promise 的异步方案
const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = require("constants")
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
function * main () {
try {
const users = yield ajax('/api/users.json')
console.log(users);
const posts = yield ajax('api/posts.json')
console.log(posts);
} catch (e) {
//捕获执行的异常
console.log(e);
}
}
// const g = main()
// //递归函数 封装generator
// function handleResult (result) {
// if (result.done) return //生成器函数结束
// //未结束则执行回调函数 将结果传给生成器函数 使其继续执行
// result.value.then(data => {
// handleResult(g.next(data))
// //处理失败的回调错误
// }, error => {
// //在生成器内生成一个异常
// g.throw(error)
// })
// }
// //生成器函数第一次调用结果
// handleResult(g.next())
//方便复用 可以封装成一个公共的函数 可以用于任一个生成器函数
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库
co(main)
在异步调用开发中还是常选择async和await的方式
Async/await语法糖
语言层面的异步编程标准 提供扁平化的异步编程函数
await 后的值会包装成promise 对象 而接下来的代码则会放到微任务中执行
// Async / Await 语法糖
function ajax (url) {
return new Promise(function (resolve, reject) {
//AJAX请求
var xhr = new XMLHttpRequest()
//请求方式和地址
xhr.open('GET', url)
//请求类型
//获取结果为json对象而不是字符串
xhr.responseType = 'json'
//请求完成过后执行
xhr.onload = function () {
if (this.status === 200) {
//请求成功 返回请求的结果
resolve(this.response)
} else {
//请求失败 返回错误信息 即当前状态文本
reject(new Error(this.statusText))
}
}
//开始执行异步请求
xhr.send()
})
}
//标准的async函数
//await关键词只能出现再async函数内部 不能在外层使用
async function main () {
try {
const users = await ajax('/api/users.json')
console.log(users);
const posts = await ajax('api/posts.json')
console.log(posts);
} catch (e) {
//捕获执行的异常
console.log(e);
}
}
//可直接在外面调用该函数
//不需要再配合co这样的执行器 因为是语言层面的异步编程标准
//其次会返回一个promise对象 利于对整体代码进行控制
const promise = main()
promise.then(() => {
console.log('all comeleted');
})