总结下前端的同步异步、事件循环问题,如有错误欢迎指正。
目录
一、setTimeout定时器函数
1.定义
2.基本语法
3.返回值
4.使用
1)异步执行
2)嵌套使用
3)事件循环
二、Promise
1.定义
2.状态
3.基本语法
1)resolve()
2)创建Promsie
3)处理Promise结果
(1)then回调(成功):
(2)catch(失败)
4.多个Promise组合使用
1)Promise.all
2)Promise.race
三、async/await
1.定义
2.async函数的特性
1)返回值是Promise
2)内部可以包含await关键字
3.await关键字的特性
1)暂停函数执行
2)只能在async函数内部使用
3)处理Promise结果
四、事件循环
1.定义
2.主要组成部分
1)执行栈
(1)作用
(2)原则
2)堆
3)任务队列(Task Queue)/消息队列(Message Queue)
(1)作用
(2)分类
3.执行流程
五、示例解析
1.例题一
1)输出结果
2)解析
2.例题二
1)输出结果
2)解析
3.例题三
1)输出结果
2)解析
一、setTimeout定时器函数
1.定义
是JavaScript中的一个定时器函数,主要作用是在指定的延迟时间(以毫秒为单位)后执行一次给定的函数或代码段。
2.基本语法
let timerId = setTimeout(function, delay, [arg1, arg2, ...]);
- function:要延迟执行的函数。
function myFunction() { console.log('Delayed message');}setTimeout(myFunction, 1000);
- delay:延迟的时间,单位为毫秒。
function myFunction() { console.log('Delayed message'); }setTimeout(myFunction, 1000);
- [arg1, arg2, ...]:可选参数。
function addNumbers(a, b) { console.log(a + b);
}setTimeout(addNumbers, 1000, 3, 5);
3.返回值
setTimeout函数会返回一个定时器的标识符(timerId)。这个标识符可用于在定时器执行之前取消定时器,通过clearTimeout函数来实现。
let timerId = setTimeout(function() { console.log('This message will not be shown.'); }, 3000);clearTimeout(timerId);
代码通过clearTimeout函数取消了设置的定时器,console.log将不会被输出。
4.使用
1)异步执行
setTimeout是一种异步操作方式。当代码执行到setTimeout函数时,不会阻塞后续代码的执行。
console.log('Start');
setTimeout(function() { console.log('Delayed');}, 2000);
console.log('End');
在以上函数中,输出顺序是Start、End,然后2秒后输出Delayed。因为setTimeout中的函数被放入了宏任务队列,等当前执行栈中的代码执行完后,才会在2秒后执行setTimeout中的函数。
2)嵌套使用
实现更复杂的定时效果,比如模拟一个倒计时。
function countdown(seconds) {if (seconds < 0) {return;}console.log(seconds);let timerId = setTimeout(() => {countdown(seconds - 1);}, 1000);
}
countdown(5);
这个函数会从5开始每秒递减数字,直到输出0,是一个简单的倒计时器。
3)事件循环
事件循环中,setTimeout中的函数会被放入宏任务队列。
二、Promise
1.定义
Promise在JavaScript中用于处理异步操作的结果。它是一个对象,用于包装一个还未完成的异步操作,可以在操作完成后进行相应的结果处理。
2.状态
- Pending(进行中):Promise的初始状态。当一个Promise被创建但异步操作尚未完成时,处于这个状态。
- Fulfilled(已成功):当异步操作完成时,Promise的状态变为Fulfilled。例如,一个fetch请求成功获取到数据后,对应Promise就会进入Fulfilled状态。
- Rejected(已失败):如果异步操作出现错误,Promise就会进入Rejected状态。比如,网络请求出现错误或者读取文件失败等情况。
3.基本语法
1)resolve()
(1)调用resolve()
当在一个Promise的执行器函数(传递给new Promise()的函数)中调用resolve()时,这个Promise就会从pending(进行中)状态转变为fulfilled(已成功)状态。表明这个Promise被解决了,并且任何与该Promise关联的then()方法中的成功回调函数将会被调用,同时将resolve()的参数作为成功回调函数的参数传递进去。
(2)没有调用resolve()的情况
保持pending状态:
如果没有调用resolve(),并且也没有调用reject(),Promise会一直保持pending状态。这种状态下,与该Promise关联的then()方法中的成功回调函数不会被调用,因为Promise尚未被成功解决。
可能导致的问题:
- 内存泄漏风险:如果有大量的Promise一直处于pending状态,并且这些Promise及其关联的对象(如回调函数)等没有被正确清理,那未完成的Promise所占用的内存空间就无法被释放,可能会导致内存泄漏。
- 程序逻辑阻塞:某些情况下,其他依赖于这个Promise结果的代码部分可能会被无期限地阻塞等待。
2)创建Promsie
let myPromise = new Promise((resolve, reject) => {// 异步操作if (/* 异步操作成功 */1) {resolve('成功的结果');} else {reject('失败的原因');}
});
3)处理Promise结果
(1)then回调(成功):
myPromise.then((result) => { console.log(result); });
当Promise进入Fulfilled状态时,then方法中的回调函数会被调用,并传入成功的结果,可进行一些后续操作,如UI显示等。
(2)catch(失败)
myPromise.catch((error) => { console.log(error); });
当Promise进入Rejected状态时,catch中的方法会被调用,用于处理错误。
4.多个Promise组合使用
1)Promise.all
Promise.all用于同时处理多个Promise。它接受Promise数组作为参数,并返回一个新的Promise。当数组中的所有Promise都进入Fulfilled状态时,新的Promise才会进入Fulfilled状态,并且返回一个包含所有成功结果的数组。
let promise1 = new Promise((resolve) => {resolve('promise1的结果');
});
let promise2 = new Promise((resolve) => {resolve('promise2的结果');
});
Promise.all([promise1, promise2]).then((results) => {console.log(results); // ['promise1的结果', 'promise2的结果']
});
2)Promise.race
和Promise.all类似,但它只关心第一个完成的Promsie(无论成功还是失败)。当第一个Promise完成后,返回的新Promise就会采用这个最先完成的Promsie的状态和结果。
let fastPromise = new Promise((resolve) => {setTimeout(() => {resolve('fastPromise的结果');}, 1000);
});
let slowPromise = new Promise((resolve) => {setTimeout(() => {resolve('slowPromise的结果');}, 2000);
});
Promise.race([fastPromise, slowPromise]).then((result) => {console.log(result); // 'fastPromise的结果'
});
三、async/await
1.定义
async/await是JavaScript中处理一步操作的一种更简洁、更易于理解的语法糖,它是基于Promise构建的。async用于定义一个异步函数,而await用于暂停异步函数的执行,直到一个Promise被解决(Fulfilled或Rejected)。
2.async函数的特性
1)返回值是Promise
一个async函数总是返回一个Promise。如果函数内部没有显式地返回一个Promise,JavaScript会自动将返回值包装在一个resolved的Promise中。
async function myAsyncFunction() {return "Hello";
}
以上代码等同于:
function myAsyncFunction() {return Promise.resolve("Hello");
}
2)内部可以包含await关键字
async函数的主要作用是可以在其中使用await来处理异步操作。不过,await只能在async函数内部使用。
3.await关键字的特性
1)暂停函数执行
当async函数执行到await关键字时,函数的执行会暂停,直到后面跟着的Promise被解决。
async function asyncFunction() {console.log("Before await");let result = await new Promise((resolve) => {resolve("Resolved");});console.log(result);console.log("After await");
}
asyncFunction();
调用函数中,会先输出"Before await",然后await关键字等待Promise的解决,resolve()
2)只能在async函数内部使用
await只能在async函数内部使用。如果在非async函数中使用await,会导致语法错误。这是因为await依赖于async函数提供的异步执行上下文。
3)处理Promise结果
await会获取Promise的解决值(fulfilled)或抛出异常(rejected)。
async function handlePromise() {try {let response = await fetch('https://example.com/api');let data = await response.json();return data;} catch (error) {console.error("An error occurred:", error);throw error;}
}
四、事件循环
1.定义
事件循环(Event Loop)是JavaScript的运行机制,用于处理异步任务。
它像一个永不停歇的循环,不断检查任务队列,并执行其中的任务,从而让JavaScript能够在单线程的环境下处理各种异步操作,如定时器、网络请求、用户事件等。
2.主要组成部分
1)执行栈
(1)作用
执行栈是JavaScript代码执行的地方,它是一个栈结构。当一个函数被调用,它的执行上下文会被压入执行栈。函数执行完毕后,其执行上下文会从栈顶弹出。例如,当执行
function a() { console.log('a'); } a();
时,a函数的执行上下文会被压入执行栈,执行console.log('a')后,函数执行完毕,执行上下文从栈顶弹出。
(2)原则
执行栈遵循先进后出的原则,当一个函数内部调用另一个函数时,新函数的执行上下文会被压入栈顶,等新函数执行完毕后才会弹出,然后继续执行原理函数的剩余部分。
2)堆
堆是用于存储对象和函数的内存空间。在JavaScript中,当创建一个对象或者一个函数时,这些数据会被存储在堆中。堆的内存分配和回收由JavaScript的垃圾回收机制来管理。
3)任务队列(Task Queue)/消息队列(Message Queue)
(1)作用
任务队列用于存放异步任务,如setTimeout、setInterval、Promise的then回调等。它是一个先进先出的队列结构。当异步任务可以执行时(比如setTimeout的延迟时间到了或者Promise的状态改变了),任务会被放入任务队列。
(2)分类
任务队列分为宏任务队列和微任务队列。宏任务队列主要存放如setTimeout、setInterval、I/O操作、script(整体代码块)等任务;微任务队列主要存放Promise的then回调、async/await的后续操作等任务。
3.执行流程
(1)首先,JavaScript引擎会执行全局代码(script),将其压入执行栈。执行过程中,遇到同步代码,就直接执行;如果遇到异步操作,会将异步操作的相关任务交给浏览器或Node.js的其他模块(如Web API模块)去处理,执行栈继续执行其他同步代码。
(2)异步操作完成时,其对应的任务会被放入任务队列。
(3)执行栈为空后,事件循环会先检查微任务队列。如果微任务队列中有任务,就将任务逐个取出并压入执行栈执行,直到微任务队列为空。
(4)微任务队列清空后,事件循环会检查宏任务队列。如果宏任务队列中有任务,就取出一个任务压入执行栈执行,这个任务执行完毕后,又回去检查微任务队列中是否有新任务(因为在宏任务执行过程中可能产生新的微任务),如此循环。
五、示例解析
1.例题一
先来个比较简单的:
async function async1() {console.log('async1 start');let result = await async2();console.log(result);console.log('async1 end');
}
async function async2() {console.log('async2');
}
console.log('start');
async1();
console.log('end');
1)输出结果
2)解析
(1)执行同步代码
首先执行同步代码console.log('start'),输出'start'。
(2)调用async1()
进入async1函数内部,console输出'async1 start'。
await关键字异步操作,等待async2返回一个Promise,并且把Promise的返回值赋值给了result变量。
(4)执行async2()
执行async2异步函数,进入后console输出'async2'。
async2执行完毕,返回一个Promise(因为没有显式地返回一个值,JavaScript会自动将其包装为一个已解决的Promise,值为undefined)
(6)回到async1函数
由于await的作用,async1函数暂停执行,等待async2返回的Promise被解决。
(7)执行同步代码
异步操作不会阻塞其他同步代码,按顺序执行console.log('end'),输出'end'。
(8)回到async1
async2返回的Promise被解决(实际上它立即被解决,因为没有异步操作等待,只是一个console),async1继续执行,将async2的返回值undefined赋值给result,输出'undefined',最后输出'async1 end'。
2.例题二
对例题一进行改变,得到如下:
async function async1() { console.log('async1 start');await async2().then((res) => {console.log(res);});console.log('async1 end');}async function async2() { console.log('async2');return new Promise((resolve, reject) => {console.log('async2 promise');setTimeout(() => {resolve('async2 resolve');}, 2000);});}async1();console.log('end');
1)输出结果
2)解析
(1)首先执行async1()
进入async1函数,直接console.log输出'acync1 start'。
遇到await async1(),开始执行async2()。
(2)执行async2()
先直接console.log输出'async2'。
接下来是创建一个Promise,这个Promise中直接输出'async2 promise'。
遇到setTimeout定时器,会将setTimeout中的函数放入宏队列。
此时async2()中的Promise的状态还没有变为fulfilled,所以async2()执行暂停并返回一个处于pending状态的Promsie。
(3)回到async1()
由于await的作用,async1函数暂停执行,等待async2返回的Promise被解决。
(4)执行console.log('end')
先执行其他同步代码,输出'end'。
(5)定时器触发
事件循环检查微任务队列中没有任务,检查宏任务队列中有一个setTimeout定时器的函数,在经过2000毫秒后,setTimeout的回调函数被执行,此时,在async2中创建的Promise调用resolve('async2 resolve'),Promise的状态变为fulfilled已解决。
(6)回到async1()
当async2返回的Promise被解决后,async1中的Promise的then回调函数被放入微队列任务中等待执行。
当执行栈为空时,事件循环检查微任务队列,执行then回调函数,输出res,res的值也就是之前resolve()中返回的值,输出'async2 resolve'。
接着继续执行async1中await之后的代码,输出'async1 end'。
3.例题三
对例题二再进行改变,得到如下:
async function async1() {console.log('async1 start');await async2().then((res) => {console.log(res);});console.log('async1 end');
}async function async2() {console.log('async2');new Promise((resolve, reject) => {console.log('async2 promise');resolve('async2 resolve');setTimeout(() => {console.log('settimeout');}, 0);}).then((res) => { console.log(res); })return await async3();
}
async function async3() {console.log('async3');return 'async3 resolve';
}
console.log('start');
async1();console.log('end');
1)输出结果
2)解析
(1)执行同步代码
console.log输出'start'。
(2)执行async1()
首先输出'async1 start',然后遇到await关键字等待async2执行。
(3)执行async2()
console输出'async2',然后用new创建一个Promise对象,这个Promise中输出'async2 promise',然后调用resolve('async2 resolve')将这个Promise的状态改为了fulfilled,这时将Promise的then回调函数放入微任务队列中等待执行,也就是将console.log(res)放入微队列。
接着遇到setTimeout定时器,将定时器的函数放入宏队列中,即将console.log('settimeout')放入宏队列中。
return await async3()中遇到await关键字,等待async3执行。
(4)执行async3()
在async3中直接console输出'async3',然后retuen 'async3 resolve',async函数中的return是一个Promise对象,这个对象返回值为'async3 resolve'。
(5)回到async2()
async2中获取到了async3返回的值,并且将该值return,async2函数也返回一个Promise对象,对象值是'async3 resolve'。
(6)回到async1()
这时await async2()获取到了async2函数的返回值,并且Promise状态为fulfilled已完成,所以会将Promise的回调函数then函数放入微任务队列中。
(7)执行console.log('end')
输出'end'。
(8)检查微任务队列
这时微任务队列里第一个是async2函数中的Promise的then回调函数(函数作用是console.log(res)),第二个是async1函数中async2执行完毕后的then回调函数(函数作用是console.log(res))。先进先出,按照顺序执行。先输出'async2 promise',再输出'async3 resolve'。这时async1的await执行完毕,async1函数继续执行,输出'async1 end'。
(9)检查宏任务队列
宏队列中只存放了async2函数中的setTimeout定时器,触发定时器,在0毫秒之后输出'settimeout'。