回调函数

概念:回调函数是作为参数传递给另一个函数并在其父函数完成后执行的函数。

这是异步编程的最基本方法。

下面是三个回调函数的例子:

1
2
3
4
5
6
7
8
function doSomething(msg, callback){
alert(msg);
if(typeof callback == "function")
{callback();}
}
doSomething("回调函数", function(){
alert("匿名函数实现回调!");
});

数组遍历的回调函数:

1
array1.forEach(element => console.log(element));

jQuery异步请求的回调函数:

1
2
3
$.get("/try/ajax/demo_test.php",function(data,status){
alert("数据: " + data + "\n状态: " + status);
});

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。


事件监听

采用事件驱动模式。取决于某事件是否发生。

事件监听的回调函数:

1
2
3
element.addEventListener("click", function(){ 
alert("Hello World!");
});

可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。


Promise

JavaScript Promise | 菜鸟教程 (runoob.com)

深入理解Promise - 掘金 (juejin.cn)

以下是两种异步任务的书写方式:

写法一:

1
2
3
4
5
6
7
8
9
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 3000);
}, 4000);
}, 1000);

写法二 Promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve();
}, 1000);
}).then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Second");
resolve();
}, 4000);
});
}).then(function () {
setTimeout(function () {
console.log("Third");
}, 3000);
});

容易看出Promise嵌套格式的代码变成了顺序格式的代码,程序流程更加清楚,而且有一整套的配套方法,可以实现许多强大的功能。


构造Promise

新建一个Promise对象:

1
2
3
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
if (Math.random() < 0.5) {
resolve('success');//如果异步操作成功,则调用 resolve 函数并传递成功的结果
} else {
reject('error');//如果异步操作失败,则调用 reject 函数并传递失败的原因
}
}, 1000);
});

//使用 then 方法处理 Promise 成功状态的回调函数,使用 catch 方法处理 Promise 失败状态的回调函数
promise.then(result => {
console.log(result);
}).catch(error => {
console.log(error);
});

关于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function lucktest(){
console.log("luck test");
new Promise(function (resolve,reject) {
setTimeout(()=>{
console.log("检测中。。。");
resolve();
},1000);
}).then(()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
x=Math.floor(Math.random()*100);
console.log('运气值为'+x);
if(x>50){resolve();}
else{reject();}
},1000)})
})
//注意下方then和catch中resolve的位置,包含在异步操作setTimeout内。若放在外部则会函数因为异步操作略过setTimeout的等待时间直接进行resolve()。
.then(()=>{return new Promise((resolve,reject)=>{setTimeout(()=>{console.log('运气不错!');resolve()},1000)})})
.catch(()=>{return new Promise((resolve,reject)=>{setTimeout(()=>{console.log('运气不太好');resolve()},1000)})})
.finally(()=>{setTimeout(()=>console.log('end'),500)})
}

异步函数(async/await)

async是‘异步’的意思,而await是‘等待’的意思。异步函数的原理与Promise原生API机制相同,但更便于阅读。

引例

下面将其与Promise做对比:

首先给出一个Promise函数:

1
2
3
4
5
6
7
8
function print(delay, message) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log(message);
resolve();
}, delay);
});
}

再对Promise对象进行操作,实现功能:

1
2
3
4
5
print(1000, "First").then(function () {
return print(4000, "Second");
}).then(function () {
print(3000, "Third");
});

我们也可以通过异步函数实现上一步代码的功能:

1
2
3
4
5
6
async function asyncFunc() {
await print(1000, "First");
await print(4000, "Second");
await print(3000, "Third");
}
asyncFunc();

这岂不是将异步操作变得像同步操作一样容易了吗!代码变得更加美观易读了!

异步函数的实现

async作为关键字放到函数前面,用于表示函数是一个异步函数:

1
2
3
4
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}

async函数返回的是一个promise对象,可以调用then方法获取到promise的结果值;如果function中返回的是一个值,async直接会用Promise.resolve()包裹一下返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getSomething() { 
return "something";
}

async function testAsync() {
return Promise.resolve("hello async");
}

async function test() {
const v1 = await getSomething();
//await getSomething() 等待 getSomething 函数的执行结果。由于 getSomething 函数直接返回一个字符串,所以会立即得到结果,不需要等待异步操作
//运行v1后再运行v2
const v2 = await testAsync();
//await testAsync() 等待 testAsync 函数返回的 Promise 对象执行完毕。由于 testAsync 函数是一个 async 函数,它会返回一个 resolved(已完成)的 Promise 对象,其中包含字符串 "hello async"。
console.log(v1, v2);
}
test();

await 用于等待一个异步任务执行完成的结果,并且await只出现在 async 函数中,但async函数里不是必须有await;

当函数执行的时候,一旦遇到await就会先暂停,等到异步操作完成,再接着执行函数体内后面的语句;

await关键字的返回结果就是其后 promise执行的结果值,是resolved或者 rejected后的值。

为什么await关键词只能在async函数中用:

await操作符等的是一个返回的pomise结果,在异步编程中可以正常实现。正常程序不具备异步函数的特质,无法等待一个promise结果,一旦使用可能会造成堵塞,会抛出SyntaxError。

异步串行、并行

串行:等待前面一个await执行后接着执行下一个await,以此类推

并行:将多个promise直接发起请求(先执行async所在函数),然后再进行await操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function asyncAwaitFn(str) {
return await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(str)z
}, 1000);
})
}

const serialFn = async () => { //串行执行
console.log(await asyncAwaitFn('string 1'));
console.log(await asyncAwaitFn('string 2'));
console.timeEnd('serialFn ')
}

serialFn();

const parallel = async () => { //并行执行
//多个异步进行
const parallelOne = asyncAwaitFn('string 1');
const parallelTwo = asyncAwaitFn('string 2')

//直接打印
console.log(await parallelOne)
console.log(await parallelTwo)
console.timeEnd('parallel')
}
parallel()

错误处理

在Promise中当请求reject的时候我们可以使用catch。为了保持代码的健壮性使用async、await的时候我们使用try catch来处理错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function catchErr() {
try {
const errRes = await new Promise((resolve, reject) => {
setTimeout(() => {
reject("http error...");
}, 1000);
);
//平常我们也可以在await请求成功后通过判断当前status是不是200来判断请求是否成功
// console.log(errRes.status, errRes.statusText);
} catch(err) {
console.log(err);
}
}
catchErr();

事件循环

请看以下代码:

1
2
3
console.log('1');
setTimeout(()=>{console.log('2');},0);
console.log('3')

此代码的输出顺序为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

整体流程

  1. 先清空call stack中的同步代码
  2. 执行微任务队列中的微任务
  3. 尝试DOM渲染
  4. 触发Event Loop反复询问callbackQueue中是否有要执行的语句,有则放入call back继续执行
事件循环整体流程

实例

  1. setTimeout与Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log('1')
setTimeout(()=>{
console.log('2')
const p =new Promise(resolve => resolve(3))
p.then(result => console.log(result))
},0)
const p = new Promise(resolve=> {
console.log('8')
setTimeout(()=>{
console.log('4')
},0)
resolve(5)
})
p.then(result=> console.log(result))
const p2 = new Promise(resolve => resolve(6))
p2.then(result =>console.log(result))
console.log('7')
//1 8 7 5 6 2 3 4

黑马事件循环例题讲解:AJAX-Day04-10.事件循环经典面试题_哔哩哔哩_bilibili

  1. async与Promise

async隐式返回 Promise 作为结果的函数。可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')
}, 0)

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

更多例子请参考:面试题:说说事件循环机制(满分答案来了)-腾讯云开发者社区-腾讯云 (tencent.com)


参考资料:异步函数async-CSDN博客 JS 异步编程的 5 种解决方案_js异步处理-CSDN博客

面试率 90% 的JS事件循环Event Loop,看这篇就够了!! !_面试率超高的js事件循环,看这篇就够了-CSDN博客

事件循环机制(Event Loop)的基本认知 - 掘金 (juejin.cn)