JS笔记--异步编程
回调函数
概念:回调函数是作为参数传递给另一个函数并在其父函数完成后执行的函数。
这是异步编程的最基本方法。
下面是三个回调函数的例子:
1 | function doSomething(msg, callback){ |
数组遍历的回调函数:
1 | array1.forEach(element => console.log(element)); |
jQuery异步请求的回调函数:
1 | $.get("/try/ajax/demo_test.php",function(data,status){ |
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。
事件监听
采用事件驱动模式。取决于某事件是否发生。
事件监听的回调函数:
1 | element.addEventListener("click", function(){ |
可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
Promise
JavaScript Promise | 菜鸟教程 (runoob.com)
以下是两种异步任务的书写方式:
写法一:
1 | setTimeout(function () { |
写法二 Promise:
1 | new Promise(function (resolve, reject) { |
容易看出Promise
将嵌套格式的代码变成了顺序格式的代码,程序流程更加清楚,而且有一整套的配套方法,可以实现许多强大的功能。
构造Promise
新建一个Promise对象:
1 | new Promise(function (resolve, reject) { |
Promise 的构造函数
起始函数
Promise 构造函数是 JavaScript 中用于创建 Promise 对象的内置构造函数。
Promise 构造函数接受一个函数作为参数,该函数是同步
的并且会被立即执行,所以我们称之为起始函数。
起始函数包含两个参数 resolve 和 reject,分别表示 Promise 成功和失败的状态。起始函数执行成功时,它应该调用 resolve 函数并传递成功的结果。当起始函数执行失败时,它应该调用 reject 函数并传递失败的原因。
promise对象
Promise 构造函数返回一个 Promise 对象,该对象具有以下三个方法:
- .then():可以将参数中的函数添加到当前 Promise 的正常执行序列,用于处理 Promise 成功状态的回调函数。.then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列。
- .catch():设定 Promise 的异常处理序列,用于处理 Promise 失败状态的回调函数。
- .finally():无论 Promise 是成功还是失败,都一定会执行的回调函数。
下面是一个使用 Promise 构造函数创建 Promise 对象的例子:
当 Promise 被构造时,起始函数会被同步执行:
1 | const promise = new Promise((resolve, reject) => { |
关于resolve()
和reject()
resolve() 中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给 then。如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作。
reject() 参数中一般会传递一个异常给之后的 catch 函数用于处理异常。
resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;
resolve 和 reject 并不能够使起始函数停止运行,别忘了 return。
关于then
catch
finally
三者序列位置可以更变,但最好按照then,catch,finally顺序编写。
三者都可以多次使用。finally与then一样会按顺序进行,但是 catch 块只会执行第一个,除非 catch 块里有异常。所以最好只安排一个 catch 和 finally 块。
then 块默认会向下顺序执行,return 是不能中断的,可以通过 throw 来跳转至 catch 实现中断。
注意:与其他异步操作的混合使用,视情况判断是否需要将其包裹在promise中:
如以下代码,需要promise,以确保在setTimeout
结束后再接着运行:
1 | function lucktest(){ |
异步函数(async/await)
async
是‘异步’的意思,而await
是‘等待’的意思。异步函数的原理与Promise原生API机制相同,但更便于阅读。
引例
下面将其与Promise做对比:
首先给出一个Promise函数:
1 | function print(delay, message) { |
再对Promise对象进行操作,实现功能:
1 | print(1000, "First").then(function () { |
我们也可以通过异步函数实现上一步代码的功能:
1 | async function asyncFunc() { |
这岂不是将异步操作变得像同步操作一样容易了吗!代码变得更加美观易读了!
异步函数的实现
async
作为关键字放到函数前面,用于表示函数是一个异步函数:
1 | async function async1() { |
async
函数返回的是一个promise对象,可以调用then方法获取到promise的结果值;如果function中返回的是一个值,async
直接会用Promise.resolve()
包裹一下返回。
1 | function getSomething() { |
await 用于等待一个异步任务执行完成的结果,并且await只出现在 async
函数中,但async
函数里不是必须有await;
当函数执行的时候,一旦遇到await就会先暂停,等到异步操作完成,再接着执行函数体内后面的语句;
await关键字的返回结果就是其后 promise执行的结果值,是resolved或者 rejected后的值。
为什么await关键词只能在async函数中用:
await操作符等的是一个返回的pomise结果,在异步编程中可以正常实现。正常程序不具备异步函数的特质,无法等待一个promise结果,一旦使用可能会造成堵塞,会抛出SyntaxError。
异步串行、并行
串行:等待前面一个await执行后接着执行下一个await,以此类推
并行:将多个promise直接发起请求(先执行async所在函数),然后再进行await操作
1 | async function asyncAwaitFn(str) { |
错误处理
在Promise中当请求reject的时候我们可以使用catch。为了保持代码的健壮性使用async、await的时候我们使用try catch来处理错误。
1 | async function catchErr() { |
事件循环
请看以下代码:
1 | console.log('1'); |
此代码的输出顺序为132。为什么顺序是这样的呢?
事件循环机制
js代码是从上往下一行行进行的,为了不造成堵塞,使用事件循环机制。
首先执行console.log('1');
,将其放入**调用栈
中,再执行代码,打印出’1’后将其从调用栈
**移出。
再将settimeout()
放入**调用栈
,判断为异步代码,将其从调用栈
移动至浏览器宿主环境
(调用Web API
) ,并在设定时间结束后移动至 任务队列(Callback Queue)
。当调用栈
所有同步代码执行完毕后从 任务队列
中取出异步任务至调用栈
**中执行,执行完毕后移除。这就是事件循环的基本流程。
所以代码会先执行settimeout()
后的内容,即console.log('3')
,再输出2。即顺序为132。
过程总结:
- 同步代码,调用栈执行后直接出栈
- 异步代码,放到宿主环境(Web API)中,等待时机,等合适的时候放入回调队列,等到调用栈空时eventLoop开始工作,轮询
宏任务与微任务
异步任务主要分为宏任务与微任务。
微任务执行时机比宏任务要早。且微任务在DOM渲染前触发,宏任务在DOM渲染后触发。
宏任务:有明确异步任务需要执行和回调;需要其他异步线程支持。
常见宏任务:setTimeout()
,setInterval()
,Ajax
,DOM事件
微任务:没有明确异步任务需要执行,只有回调;不需要其他异步线程支持。
常见微任务:Promise
,async/await
整体流程
- 先清空call stack中的同步代码
- 执行微任务队列中的微任务
- 尝试DOM渲染
- 触发Event Loop反复询问callbackQueue中是否有要执行的语句,有则放入call back继续执行
实例
- setTimeout与Promise
1 | console.log('1') |
黑马事件循环例题讲解:AJAX-Day04-10.事件循环经典面试题_哔哩哔哩_bilibili
- async与Promise
async
隐式返回 Promise 作为结果的函数。可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。
1 | console.log('script start') |
更多例子请参考:面试题:说说事件循环机制(满分答案来了)-腾讯云开发者社区-腾讯云 (tencent.com)
参考资料:异步函数async-CSDN博客 JS 异步编程的 5 种解决方案_js异步处理-CSDN博客
面试率 90% 的JS事件循环Event Loop,看这篇就够了!! !_面试率超高的js事件循环,看这篇就够了-CSDN博客