JavaScript异步编程
众所周知,目前主流的JavaScript环境都是以单线程模式去执行的JavaScript代码,JavaScript采用单线程模式工作的原因与它最早的设计初衷有关,最早JavaScript就是运行在浏览器端的脚本语言,目的是为了实现页面上的动态交互,而实现页面交互的核心就是dom操作,这也决定了它必须使用单线程,否则会出现复杂的线程同步问题。试想一下,假定我们在JavaScript中同时又多个线程一起工作,其中一个线程修改了某个dom元素,而另外一个线程同时删除这个元素,那此时浏览器就无法明确该以哪个线程工作结果为准,所以为了避免线程同步的问题,从一开始JavaScript就被设计为了单线程模式工作,这也成了这门语言最为核心的特性之一。这里的单线程指的是在js执行环境中负责执行代码的线程只有一个。一次只能执行一个任务,有多个任务就需要排队,一个一个完成。这种模式最大的优点就是更安全更简单,缺点也同样很明显,如果遇到某个特别耗时的任务,后边的任务都必须排队等待这个任务的结束,这也就会导致整个程序的执行会被拖延,出现假死的情况。为了解决耗时任务阻塞的问题,JavaScript将任务的执行模式分成了两种,同步模式和异步模式。
同步模式和异步模式
同步模式
同步模式指代码当中的任务依次执行,后一个任务必须等待前一个任务结束,按照代码书写的顺序执行。
异步模式
不会去等待这个任务的结束才开始下一个任务。对于耗时任务,开启过后就立即往后执行下一个任务,耗时任务的后续逻辑通过回调函数的方式定义。耗时任务完成会自动执行这里的回调函数。如果没有异步模式,单线程的JavaScript语言无法同时处理大量耗时任务。但是对于开发者而言,异步模式最大的问题就是代码的执行顺序混乱。
回调函数:由调用者定义,交给执行者执行的函数。
事件循环和消息队列
JavaScript线程首先执行同步任务,在遇到异步任务(eg:setTimeout)的时候,发起异步调用,然后继续执行同步任务。与此同时,异步调用线程执行异步任务后,将异步任务的回调放入消息队列。待同步任务执行完毕后,Event Loop会去消息队列中寻找任务,依次执行消息队列中的任务。
异步编程的几种方式
Promise异步方案、宏任务/微任务队列
Promise就是一个对象,用来表示一个异步任务最终结束过后,究竟是成功还是失败。就像是一个承诺,一开始是待定的状态- Pending,成功后叫Fulfilled,失败后叫Rejected。承诺明确后会有对应的任务执行,onFilfilled, onRejected.
基本用法
const promise = new Promise(function (resolve, reject) {
// 兑现承诺
// resolve(100) // 承诺达成
reject(new Error('promise rejected')) // 承诺失败
})
promise.then(function (value) {
console.log('resolved', value)
return 1
}, function (error) {
console.log('rejected', error)
}).then(function(value){
console.log(value) // 1
})
- Promise对象的then方法会返回一个全新的Promise对象,所以可以使用链式调用
- 后面的then方法就是在为上一个then返回的Promise注册回调
- 前面then方法中回调函数的返回值会作为后面then方法回调的参数
- 如果回调中返回的是Promise,那后面的then方法的回调会等待这个Promise结束
异常处理
function ajax(url) {
return new Promise(function (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(new Error(this.statusText))
}
}
xhr.send()
})
}
// then方法的第二个回调函数进行异常捕获
ajax('/api/users.json').then(
function onFulfilled(value) {
console.log('onFulfilled', value)
},
function onRejected(error) {
console.log('onRejected', error)
}
)
// 使用catch进行异常捕获
ajax('/api/users.json')
.then(function onFulfilled(value) {
console.log('onFulfilled', value)
})
.catch(function onRejected(error) {
console.log('onRejected', error)
})
使用then的第二个回调捕获异常,只能捕获到前一个抛出的异常,而使用catch,因为每一个then都会返回一个promise对象,所以catch首先捕获的是前一个then 的异常,然后会捕获链上往前的异常,也就是catch会捕获链上catch以前的异常。
Promise 静态方法
Promise.resolve()
Promise.reject()
Promise 并行执行
Promise.all()
// Promise.all 返回一个全新的Promise
var promise = Promise.all([
ajax('/api/user.json'),
ajax('api/posts.json')
])
// 所有的Promise完成,全新的promise才会完成
// 所以的异步任务都成功,promise才成功
// 只要有一个异步任务失败,promise就失败
promise.then(function (values) {
// 接收的是数组,包含每个异步任务执行的结果
console.log(values)
}).catch(function (error) {
console.log(error)
})
Promise.race()
Promise.race()也会将多个promise对象组合返回一个新的promise对象,但与 all 不同的是:
all 等待所有任务结束,它才会结束
race 只会等待第一个结束的任务,也就是只要有一个任务完成了,新的promise对象也就完成了。
const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject => {
setTimeout(() => reject(new Error('timeout')), 500)
}))
// Promise.race()将多个异步任务组合后返回一个新的promise对象
// 多个异步任务中只要有一个完成(成功或失败),新的promise对象就完成了
// 这里如果request请求在500毫秒内请求成功,就返回成功,使用.then方法
// 如果500毫秒请求没有返回结果,就会reject一个错误,走到catch
Promise.race([require, timeout])
.then(value => {
console.log(value)
})
.catch(error => {
console.log(error)
})
const p = Promise.all([p1,p2,p3])
p.then(() => {})
.catch(err => {})
- Promise.all(): p1, p2, p3全部返回成功,p 才会返回成功, p1, p2, p3中任意一个返回失败,p 就返回失败。 失败后,其他异步任务仍会继续执行。
- Promise.race(): p1, p2, p3任意一个返回成功,p 就返回成功, p1, p2, p3中任意一个返回失败,p 就返回失败。 失败后,其他异步任务仍会继续执行。
- Promise.allSettled():等到p1,p2,p3全部执行完,不管成功失败,p 的状态为fulfilled。监听函数接收到的参数时数组
[{status:'fulfilled', value: 42}, {status:'rejeceted}, reason:-1]
- Promise.any(): p1, p2, p3只要有一个成功,p 就返回成功,p1,p2,p3全部失败,p 才返回失败
Promise 执行时序
console.log('global start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve()
.then(() => {
console.log('promise')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
console.log('promise 3')
})
console.log('global end')
// global start
// global end
// promise
// promise 2
// promise 3
// setTimeout
按照前面说的,回调进入回调队列,依次执行,可能我们会认为先打印setTimeout,再打印promise,但是结果不是这样的。这是因为js将任务分为了宏任务和微任务。微任务会插队,在本轮任务的末尾直接执行。
大部分异步任务都会作为宏任务。
微任务包括Promise,MutationObserver, process.nextTick/
Generator异步方案、Async/Await语法糖
基本使用
// 比普通的函数多了一个 *
function * foo() {
console.log('start')
// 用 yield 返回一个值,next 方法返回的就是这个值
// yield 不会结束生成器的执行,只是 暂停
// 如果next方法传入一个参数,会作为上一个yield 的返回值
// yield 'foo'
// const res = yield 'foo'
// console.log(res) // bar
try {
const res = yield 'foo'
console.log(res) // bar
} catch (e) {
console.log(e)
}
}
// 调用生成器并不会立即执行,而是得到一个生成器对象
const generator = foo()
// 调用next方法,函数体才会执行
const result = generator.next()
// 返回结果中有一个done属性,表示生成器是否一起执行完了
console.log(result) //{value: "foo", done: false}
// 再一次调用next方法时,会从 yield 位置开始执行
// generator.next('bar')
// 如果调用生成器的throw方法,也会继续往下执行,但是它会抛出一个异常
// 在生成器内部使用try{}catch(){}语句来接收异常
generator.throw(new Error('Generator error'))
function* main() {
try {
const users = yield ajax(url1)
console.log(users)
const posts = yield ajax(url2)
console.log(posts)
} catch (e) {
console.log(e)
}
}
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)
Async / Await 语法糖
// 将生成器的 * 改为 async ,yield 改为 await
async function main() {
try {
const users = await ajax(url1)
console.log(users)
const posts = await ajax(url2)
console.log(posts)
} catch (e) {
console.log(e)
}
}
// 直接调用,不需要 co
// async 函数返回一个promise对象
const promise = main()
promise.then(() => {
console.log('all completed')
})
手撕Promise
/**
* 手撕Promise
* 首先,promise是一个类,传入一个函数作为参数,直接调用
* promise 有三个状态, pending, fulfilled, rejected
* 在 resolve 和 reject调用后状态修改,且状态修改后不能再修改
* 将 resolve 和 reject 中的参数记录下来,作为 then 方法成功和失败回调的参数
* 如果 promise 中执行出错,要捕获错误,可以使用try catch来捕获
* 需要捕获错误的地方包括promise传入的函数执行器,和 then 方法的回调
*/
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECRED = 'rejected'
class MyPromise {
constructor(fn) {
try {
// promise 传入一个函数,直接调用,函数的参数为 resolve 和 reject
fn(this.resolve, this.reject)
} catch (err) {
this.reject(err)
}
}
// 定义初始状态
status = PENDING
// then 方法成功回调的参数
value = undefined
// then 方法失败回调的参数
error = undefined
// 初始化存储 then 回调的值
sCallback = []
fCallback = []
resolve = (value) => {
// 如果状态不是 pending ,不做修改
if (this.status !== PENDING) return
// resolve 后将状态修改为成功
this.status = FULFILLED
// 将结果记录
this.value = value
// 如果有储存的成功回调,则调用,数组需要循环调用
// this.sCallback && this.sCallback(value)
while (this.sCallback.length) this.sCallback.shift()()
}
reject = (error) => {
// 如果状态不是 pending ,不做修改
if (this.status !== PENDING) return
// reject 后将状态修改为失败
this.status = REJECRED
// 将结果记录
this.error = error
// 如果有储存的失败回调,则调用,数组需要循环调用
// this.fCallback && this.fCallback(error)
while (this.fCallback.length) this.fCallback.shift()()
}
/**
* then 方法参数为成功回调和失败回调
* 根据状态判断执行哪个回调
* 如果是异步调用,执行 then 方法时状态还是 pending,则要将两个回调储存起来
* 储存的方法在 resolve 和 reject 的方法里对应的调用
* 同一个promise可能会有多个 then 调用,也就会有多组成功和失败的回调,将异步时回调储存为数组
* then 方法可以链式调用,所以它返回的是一个promise对象,将回调中返回的值作为下一个then方法的参数
* then 方法返回的promise对象不能是自身,将 newPromise 与 返回值进行判断
* 在pending状态也要判断不能返回自身
* then 方法可以不传递参数,不传递参数时,下一个then可以拿到这个then应该拿到的结果
* 所以 then 不传递参数时,相当于把结果传递到下一个then
*/
then(sCallback = value => value, fCallback = error => { throw error }) {
let newPromise = new MyPromise((resolve, reject) => {
// 这里是同步执行,所以可以将要执行的操作放在这里
if (this.status === FULFILLED) {
setTimeout(() => {
try {
// 调用后获取返回的值
const x = sCallback(this.value)
// 判断返回的值如果是 promise 对象,根据promise的结果进行resolve和reject
// 如果是普通值,直接resolve
// 这个操作在失败是也会调用,所以包装成一个方法
// then 方法不能返回自己,所以将 newPromise 传进去判断
// 但是这里其实拿不到newPromise,可以将这段代码放入 setTimeout 中
// 放入setTimeout 中并不是为了延时,只是为了等 newPromise 创建好了可以引用,所以时间设为0
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
} else if (this.status === REJECRED) {
setTimeout(() => {
try {
const x = fCallback(this.error)
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
} else {
// 调用 then 方法时,promise的异步还没执行完,状态还是pending,把两个回调储存
// 判断不能返回自身
this.sCallback.push(() => {
setTimeout(() => {
try {
const x = sCallback(this.value)
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
})
this.fCallback.push(() => {
setTimeout(() => {
try {
const x = fCallback(this.error)
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
})
}
})
return newPromise
}
/**
* 实现finally方法, finally 方法不管promise成功失败都会执行回调
* finally 会将promise的结果往下传
* 可以利用 then 方法来实现
* finally 方法返回一个新的promise对象,由于then方法就是返回一个promise对象,所以直接返回
* 如果finally返回一个promise对象,要等promise对象有了结果,才会执行下方的 then
*/
finally(callback) {
return this.then(value => {
return MyPromise.resolve(callback()).then(() => value)
}, err => {
return MyPromise.resolve(callback()).then(() => { throw err })
})
}
/**
* 实现 catch,catch方法只有一个回调,就是失败回调,返回一个promise
*/
catch(callback) {
return this.then(undefined, callback)
}
/**
* 实现一个all方法, all 方法传入一个数组,数组中会有异步调用,返回一个新的promise对象
* 数组中所有异步都成功,将结果以数组形式返回,否则一个出错就出错
*/
static all(args) {
let results = []
let index = 0
return new MyPromise((resolve, reject) => {
function addData(key, value) {
results[key] = value
// index代表给results中添加了几个值,如果index和args长度相等,说明全部成功
// 不能用results长度来判断,因为results赋值不是通过 push 方法,而是针对 key 来赋值的
index++
if (index == args.length) {
resolve(results)
}
}
for (let i = 0; i < args.length; i++) {
// 判断是promise对象还是普通值,普通值直接加入results数组
if (args[i] instanceof MyPromise) {
// promise 对象
args[i].then(value => {
addData(i, value)
}, reject)
} else {
// 普通值
addData(i, args[i])
}
}
})
}
/**
* 实现一个Promise.resolve方法
* Promise.resolve方法后面要接 then 方法
* 参数如果是个promise对象,就按照这个promise执行,返回它
* 参数如果是个普通值,创建一个新的promise对象
*/
static resolve(value) {
if (value instanceof MyPromise) return value
return new MyPromise(resolve => { resolve(value) })
}
/**
* Promise.reject 方法,返回一个新的Promise,状态为reject
* 参数原封不动的作为reject的理由
*/
static reject(reason) {
return new Promise((resolve, reject) => { reject(reason) })
}
}
function thenValue(newPromise, x, resolve, reject) {
if (newPromise === x) return reject(new TypeError('then方法不能返回自己'))
if (x instanceof MyPromise) {
// 如果是promise对象
x.then(resolve, reject)
} else {
// 如果是普通值
resolve(x)
}
}