外观
异步编程
作者:guo-zi-xin
更新于:2 个月前
字数统计:4.2k 字
阅读时长:15 分钟
Javascript是单线程的,但它却能同时处理成千上万的异步任务, 其核心原理是 事件循环机制,事件循环机制机制就像一位高效的时间管理大师,它通过巧妙的调度机制,让 JavaScript 在单线程环境下也能实现非阻塞的异步操作
什么是异步
同步: 在执行某段代码时,在没有得到返回结果之前, 会阻塞其它代码是执行, 但是一旦执行完成拿到返回值,即可执行其它代码
异步: 在某段代码执行异步过程调用发出后,这段代码不会立刻得到返回结果,而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果, 异步调用发出后, 不会阻塞后面代码的执行
回调函数(callback)
在最基本的层面上, Js的异步编程是通过回调实现的,会掉的势函数,可以传给其他函数,而其他函数会在满足某个条件时调用这个函数
定时器
一种最简单的异步操作就是在一定时间之后运行某些代码,如下面代码
javascript
setTimeout(asyncAdd(1, 2), 1000)setTimeout方法的第一个参数是一个函数,第二个参数是以毫秒为单位的时间间隔 asyncAdd方法可能是一个回调函数,而 setTimeout方法就是注册回调函数的函数。他还代指在什么异步条件下调用回调函数, 并且 setTimeout 方法只会调用一次回调函数
事件监听
给目标dom绑定一个监听函数,用的最多的是 addEventListener
javascript
document.getElementById('#myDiv').addEventListener('click', (event) => {
console.log('被点击了')
}, false)通过给id为 myDiv 的元素绑定点击事件的监听函数,把任务的执行时机推迟到了点击这个动作发生时, 此时, 任务的执行顺序与代码的编写顺序无关,只与点击事件有没有被触发有关。
网络请求
Js中另外一种常见的异步操作就是网络请求
javascript
const SERVER_URL = '/server';
let xhr = new XMLHttpRequest()
// 创建http请求
xhr.open('GET', SERVER_URL, true)
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return
// 当请求成功时
if (this.status === 200) {
handle(this.response)
} else {
console.error('this.statusText')
}
}
// 设置网络失败时的监听函数
xhr.onerror = function () {
console.error(this.statusText)
}
// 发送请求
xhr.send(null)这里使用 XMLHttpRequest 类以及回调函数来发送http请求异步处理服务器返回的响应
Node中的回调与事件
Nodejs服务端Javascript环境底层就是异步的, 定义了很多使用回调和事件的API。 例如读取文件默认的API就是异步的,它会在读取文件内容之后调用一个回调函数
javascript
const fs = require('fs')
let options = {}
// 读取配置文件,调用回调函数
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
throw err
} else {
Object.assign(options, JSON.parse(data))
}
startProgram(options)
})fs.readFile 方法以接收两个回调作为最后一个参数,它会异步读取指定文件,如果读取成功就会将第二个参数传递给回调的第二个参数,如果发生错误
Promise
Promise 是 JavaScript 中处理异步操作的一种方式。它是一个对象,代表了一个异步操作的最终完成或失败的结果。
Promise 有三种状态: pending (进行中)、 fulfilled (已成功) 和 rejected (已失败)。 一旦 Promise 的状态变为 fulfilled 或 rejected ,就称为 resolved (已解决)。 在 resolved 状态下, Promise 的结果值就被确定了。
基本用法
javascript
const promise = mew Promise((resolve, reject) => {
// 异步操作
if(/* 异步操作成功 */) {
resolve(result) // 将结果传递给resolve函数
} else {
reject(err) // 将错误信息传递给reject函数
}
})
promise.then((result) => {
// 处理异步操作成功的结果
})
.catch((error) => {
// 处理异步操作失败的结果
})在上面的示例中, 我们创建了一个 Promise 对象, 并在构造函数中传入一个执行器函数(executor function)。 执行器接收两个参数, resolve 和 reject 函数,用于将 Promise 的状态改编为 fulfilled 或者rejected。
执行器函数中进行异步操作,当异步操作成功时,调用resolve函数传递结果值;当异步操作失败时,调用 reject 函数传递错误信息。
紧接着, 我们通过调用Promise的 .then方法来设置异步操作成功时的回调函数,并通过 .catch方法来设置异步操作失败时的回调函数。 .then方法可以链式调用,每个.then方法都返回一个新的 Promise 实例, 因此可以实现连续的异步操作。
除了.then和 .catch方法外, Promise还提供了一些其他的方法,例如 finally方法、Promise.all、Promise.race等, 用于处理更复杂的异步操作场景。
需要注意的是, Promise的状态一旦改变就不会再改变, 因此即使异步操作完成后再次调用 resolve 或 reject 函数, 也不会对 Promise的状态产生影响
高级用法
Promise.all
该方法接收一个由 Promise 实例组成的数组作为参数,并返回一个新的 Promise 实例。该新的 Promise 实例在数组中的所有 Promise 实例状态都变成 fulfilled状态后, 才会变成 fulfilled状态, 并且将每个 Promise 实例的结果组成一个数组传递给回调函数
javascript
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3]).then((results) => {
console.log(results) // [1,2,3]
})Promise.race
该方法同样接收一个由 Promise 实例组成的数组作为参数, 并返回一个新的 Promise 实例。 新的实例在数组中的第一个 Promise 实例变为 fulfilled 或 rejected 状态后, 即变为对应的状态, 并将个第一个 Promise 实例的结果传递给回调函数
javascript
const promise1 = new Promise((resolve) => {
setTimeout(() => resolve('promise 1'), 2000)
})
const promise2 = new Promise((resolve) => {
setTimeout(() => resolve('promise 2'), 2000)
})
Promise.all([promise1, promise2]).then((result) => {
console.log(result) // 'promise 2'
})Promise.any
any方法是ES12中新增的方法,和race方法是类似的,不同的是any方法会等到一个fulfilled状态,才会决定新Promise的状态,如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态。
如果所有的Promise都是reject的,那么会报一个AggregateError的错误
All promises were rejected
javascript
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => reject('fail 1'), 1000)
})
const promise2 = new Promise((resolve) => {
setTimeout(() => resolve('success 2'), 2000)
})
Promise.any([promise1, promise2]).then((result) => {
console.log(result) // 'success 2'
}).catch((err) => {
console.log(err) // 如果全部失败,会走这里
})Promise.allSettled
all方法有一个缺陷:当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的 在ES11(ES2020)中,添加了新的API Promise.allSettled,该方法会在所有的Promise都有结果(settled),无论是fulfilled,还是rejected时,才会有最终的状态
javascript
const promise3 = new Promise((resolve) => {
setTimeout(() => resolve('ok 3'), 1000)
})
const promise4 = new Promise((_, reject) => {
setTimeout(() => reject('fail 4'), 2000)
})
Promise.allSettled([promise3, promise4]).then((results) => {
console.log(results)
// [
// { status: 'fulfilled', value: 'ok 3' },
// { status: 'rejected', reason: 'fail 4' }
// ]
})Generator
JavaScript Generator(生成器)是一种特殊类型的函数,它可以通过多次返回值的方式来生成一系列的值。Generator 函数使用 function* 语法进行定义,并使用 yield 关键字来产生(yield)值。
写法
javascript
function* generator() {
console.log('enter');
let a = yield 1;
let b = yield (function () {return 2})()
return 3
}
let g = generator() // 阻塞 不会执行任何语句
console.log(typeof g) // 返回object 这里不是function
console.log(gererator.next())
console.log(gererator.next())
console.log(gererator.next())
console.log(gererator.next())
// 输出结果
// object
// enter
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: true}
// {value: undefined, done: true}generator 中配合使用 yield关键词可以控制函数的执行顺序,每执行一次 next 方法, Generator函数会执行到下一个存在 yield 关键词的位置
- 调用 generator() 程序会阻塞 不会执行任何语句
- 调用 generator().next()后, 程序将继续执行,直到遇到yield关键词时执行暂停;
- 一直执行 next 方法, 最后返回一个对象,其存在两个属性: value 和 done
yield
yield也是ES6的关键词, 配合Generator执行以及暂停。 yield关键词最后返回一个迭代器对象, 该对象有value 和 done 两个属性, 其中done 属性代表返回值以及是否完成。yield配合着 Generator 再同时使用 next 方法,可以主动控制 Generator 执行进度
javascript
function* gen1() {
yield 1;
yield* gen2();
yield 4;
}
function* gen2() {
yield 2;
yield 3
}
let g = gen1();
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
// 执行结果
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: 4, done: false}
// {value: undefined, done: true}可以看到, 使用yield 关键词的话还可以配合着 Generator 函数嵌套使用, 从而控制函数执行进度。 这样对于 Generator 的使用, 以及最终函数的执行进度都可以很好的控制,从而形成符合你设想的执行顺序。 即便使用 Generator函数嵌套, 也只能一步一步地按照进度执行。
生成器原理
其实 在生成器内部,如果遇到yield关键字,那么v8引擎将返回关键字后面的内容给外部,并暂停该生成器函数的执行。生成器暂停后,外部的代码便开始执行,外部代码如果想要恢复生成器的执行,可以使用 result.next 方法。
v8引擎如何实现生成器函数的暂停执行和恢复执行?
主要原理是用到协程。 协程是一种比线程更加轻量级的存在。 我们可以把协程看成是跑在线程上的任务,一个线程可以存在多个协程,但是在线程上同时只能执行一个协程。 比如,当前执行的是A协程,要启动B协程,那么A协程就需要将主线程的控制权交给B协程,这就体现在A协程暂停执行,B协程恢复执行,同样的,也可以从B协程启动A协程。 通常,如果从A协程启动B协程,我们把A协程成为B协程的父协程
进程可以有多个线程, 线程可以有多个协程。 每一时刻,该线程只能执行其中某一个协程。
- 协程不是被操作系统内核所管理,而完全由程序来控制(在用户态执行), 这样的好处就是性能得到了很大提升,不会像切线程那样消耗资源。
co库
co 函数库用于处理 Generator 函数的自动执行。核心原理其实就是通过和 thunk 函数以及 Promise 对象进行配合,包装成一个库
javascript
const co = require('co');
let g = gen();
co(g).then(res =>{
console.log('res===',res);
})Async/Await
ES7 新增了两个关键字: async和await,代表异步JavaScript编程范式的迁移。它改进了生成器的缺点,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力。 其实 async/await 是 Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。
从字面上来看,async是“异步”的简写,await则为等待,所以 async 用来声明异步函数,这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。 因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力,使用await关键字可以暂停异步代码的执行,等待Promise解决。async 关键字可以让函数具有异步特征,但总体上代码仍然是同步求值的。
具体用法
javascript
const httpRequest = async () => {
let dataRet = await httpPromise(url)
console.log(dataRet)
}async 函数具体返回的是 Promise对象, 如果异步函数使用 return 关键字返回了值(没有return 会返回undefined), 这个值会被 Promise.resolve() 包装城 Promise对象。 异步函数始终返回Promise对象

await 等待什么
一般我们认为 await 在等待一个 async 函数完成。 不过按语法说明, await 等待的是一个表达式, 这个表达式的结果是 Promise 对象或其他值。
因为 async 函数返回一个 Promise 对象, 所以 await 可以用于等待一个 async函数的返回值: 这也可以说是 await 在等待 async 函数。 但要清楚, 它等的实际是一个返回值。
await 后面接普通函数调用或者直接量,是可以正常运行的
javascript
const getSomething = () => {
return 'something'
}
const testAsync = async () => {
return Promise.resolve('hello async')
}
const test = async () => {
const dataRet1 = await getSomething()
const dataRet2 = await testAsync()
console.log(dataRet1, dataRet2)
}
test() // something, hello asyncawait 表达式的运算结果取决于他等的是什么:
如果它等的不是一个 Promise对象, 那么await 表达式的运算结果就是它等到的内容;
如果它等的是一个 Promise 对象,await 就会阻塞后面的代码,等着Promise 对象 resolve, 然后将的得到的值作为 await 表达式的运算结果。
javascript
const testAsync = async (x) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x)
},3000)
})
}
const testAwait = async () => {
let result = await testAsync('hello world')
console.log(result) // 这里调用后三秒后执行 出现hello world
console.log('cuger') // 3秒钟之后出现 cug
}
testAwait();
console.log('cug') // 立即输出 cug上述例子表示了 await 必须用在 awync 函数的原因。 async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise对象中异步执行。 await 暂停当前 async的执行, 所以 'cug' 先输出, 'hello world'和 'cuger' 时3秒钟后同时出现的
async/await优势
async/await 的优势在于处理复杂流程时 可以代替 promise 多级链式调用的过程, 使代码结构清晰, 语义化明显
javascript
/**
* 传入参数 n 表示这个函数执行的时间(毫秒)
* 执行结果是 n + 200, 这个值将用于下一个步骤
*/
const takeLongTime = (n) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(n + 200)
}, n)
})
}
const step1 = (n) => {
console.log(`step1 with ${n}`)
return takeLoneTime(n)
}
const step2 = (n) => {
console.log(`step2 with ${n}`)
return takeLoneTime(n)
}
const step3 = (n) => {
console.log(`step3 with ${n}`)
return takeLoneTime(n)
}如果用promise来处理的话, 需要层级链式调用
javascript
const doIt = () => {
console.time('doIt');
const time1 = 300;
step1(time1).then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`)
console.timeEnd('doIt')
})
}
doIt()
// 输出结果
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms输出结果 result 是 step3() 的参数 700 + 200 = 900
doIt() 顺序执行了三个步骤, 一共用了 300 + 500 +700 = 1500 ms, 和 console.time()/console.timeEnd() 计算结果一致
如果用async/await 来处理:
javascript
const doIt = async () => {
console.time('doIt')
const time1 = 300
const time2 = await step1(time1)
const time3 = await step1(time2)
const result = await step1(time3)
console.log(`result is ${result}`)
console.timeEnd('doIt')
}
doIt()结果跟之前promise实现是一样的, 但这个代码看起来会清晰得多, 几乎和同步代码一样。
async/await对比 Promise 的优势就显而易见了:
代码读起来更加同步, Promise虽然摆脱了回调地狱,但是then的链式调用也会带来额外的理解负担;
Promise 传递中间值很麻烦,而 async/await 几乎是同步的写法, 很清晰;
错误处理友好, async/await可以使用 try/catch Promise的错误捕获比较冗余;
调试友好, Promise的调试很差, 由于没有代码块, 不能再一个返回表达式的箭头函数中设置断点, 如果在一个.then代码块中使用调试器的步进(step-over)功能, 调试器并不会进入后续的.then模块, 因为调试器只能跟踪同步代码的每一步
异常处理
利用 async/await 的语法糖,可以像处理同步代码的异常一样,来处理异步代码:
javascript
const exe = (flag) => {
return () => new Promise((resolve, reject) => {
console.log(flag);
setTimeout(() => {
flag ? resolve("yes") : reject("no");
}, 1000);
})
};
// async
const run = async () => {
try{
await exe(false)()
await ext(true)()
} catch (e) {
console.log(e)
}
}
run()这里定义一个异步方法 run,由于 await 后面需要直接跟 Promise 对象,因此通过额外的一个方法调用符号 () 把原有的 exe 方法内部的 Thunk 包装拆掉, 即执行 exe(false)() 或 exe(true)() 返回的就是 Promise 对象。在 try 块之后,使用 catch 来捕捉。运行代码会得到这样的输出:
javascript
false
no这个 false 就是 exe 方法对入参的输出,而这个 no 就是 setTimeout 方法 reject 的回调返回,它通过异常捕获并最终在 catch 块中输出。就像我们所认识的同步代码一样,第四行的 exe(true) 并未得到执行。
