一、js如何执行
- 从前到后,一行一行执行
- 如果某一行执行报错,则停止下面代码执行
- 先把同步代码执行完,再执行异步
示例:
console.log('开始');
setTimeout(function() {
console.log('异步操作');
}, 2000);
console.log('结束');
输出结果为:
开始
结束
异步操作
可以看到,在执行异步操作的过程中,主线程不会等待异步操作结束,而是继续往下执行后续的代码,当满足条件时触发异步操作的回调函数。
异步代码不会阻塞后续代码的执行,它会在后台运行,并在特定条件满足时触发回调函数。常见的异步操作包括定时器、事件监听、AJAX 请求等
需要注意的是,JavaScript 采用事件循环(event loop)机制来处理异步代码的执行顺序。当主线程执行完同步代码后,会检查任务队列中是否还有异步任务,如果有,则按照优先级依次执行。当所有异步任务执行完毕后,事件循环会等待,直到有新的异步任务被添加到队列中才会继续执行。这就是 JavaScript 中异步执行的原理。
二、event loop过程
- 同步代码,一行一行放到Call Stack执行
- 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
- 时机到了,就移动到Callback Queue
- 如果Call Stack 为空(即同步代码执行完)Event Loop 开始工作
- 轮询查找Callback Queue,如有则移动到Call Stack 执行
- 然后继续轮询查找(永动机)
三、Promise
在JavaScript中,Promise是一种用于处理异步操作的对象。它可以表示一个尚未完成但最终会完成的操作,并提供了一种处理该操作结果的方式。
Promise有三种状态:pending(进行中)、resolved(已成功)和rejected(已失败)。最初处于pending状态,可以转换为resolved或rejected状态。一旦状态转变,就不会再改变。
Promise的主要用途是解决回调地狱(callback hell)问题,即多个异步操作依赖于前一个异步操作的结果,导致代码嵌套层级非常深。
以下是一个使用Promise的简单示例:
function getData() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
const data = 'Hello, World!';
if (data) {
resolve(data); // 将操作状态改为resolved,并将数据传递给下一个then方法
} else {
reject('Data not found'); // 将操作状态改为rejected,并传递错误信息给下一个catch方法
}
}, 2000);
});
}
getData()
.then(function(result) {
console.log(result); // 打印数据:Hello, World!
return result.length;
})
.then(function(length) {
console.log(length); // 打印长度:13
})
.catch(function(error) {
console.log(error); // 如果出错,则打印错误信息
});
在上面的代码中,通过getData函数返回一个Promise对象。在Promise的构造函数中,通过setTimeout模拟一个异步操作,并在一段时间后模拟完成操作。在操作完成时,使用resolve方法将状态改为resolved,并将数据传递给第一个then方法。如果出现错误,则使用reject方法将状态改为rejected,并将错误信息传递给catch方法。
通过链式调用then方法,可以在每个then方法中依次处理操作结果。如果其中一个then方法返回了新的Promise对象,它将作为下一个then方法的输入。
最后,通过catch方法捕获任何错误,并在控制台中打印错误信息。
使用Promise可以更清晰地表达异步操作的逻辑,避免了回调地狱的问题,从而提高代码的可读性和可维护性。
1. 三种状态
- Pending:初始状态,表示promise的操作还未完成。不会触发then和catch
- Resolved:表示promise的操作已经成功完成。会触发后续的then函数
- Rejected:表示promise的操作失败或发生错误。会触发后续的catch函数
举例:
pending状态:创建一个新的Promise实例时,初始状态是pending。例如:
const promise = new Promise((resolve, reject) => {
// do something
});
Resolved状态:当Promise实例成功地执行了resolve函数时,状态变为Resolved。例如:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success');
}, 2000);
});
promise.then((result) => {
console.log(result); // 输出:Success
});
rejected状态:当Promise实例执行了reject函数或者发生了错误时,状态变为rejected。例如:
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Something went wrong'));
}, 2000);
});
promise.catch((error) => {
console.error(error); // 输出:Error: Something went wrong
});
2. then和catch函数返回的状态
promise的then函数在正常情况下会返回一个新的promise对象,它的状态由then函数中的回调函数的返回值决定。如果回调函数返回一个普通的值,那么新的promise对象会变为已解决状态,并且其值就是回调函数的返回值。如果回调函数返回的是一个promise对象,那么新的promise对象的状态将跟随该promise对象的状态。catch函数也一样。
需要注意的是,如果then函数或catch函数中的回调函数本身抛出了异常,则新的promise对象的状态会变为已拒绝,并且该异常会被传递给下一个catch函数
。
const promise = new Promise((resolve, reject) => {
resolve('Hello');
});
promise.then((result) => {
throw new Error('Something went wrong');
}).catch((error) => {
console.error(error.message); // 输出:Something went wrong
});
上面这个示例会输出‘Something went wrong’,那么问题来了,在catch后面再加上一个catch回调,加上的这个catch会执行吗?
const promise = new Promise((resolve, reject) => {
resolve('Hello');
});
promise.then((result) => {
throw new Error('Something went wrong');
}).catch((error) => {
console.error(error.message); // 输出:Something went wrong
}).catch(() => {
console.error('会不会执行呢?');
})
答案是第二个catch不会执行,控制台依然只打印Something went wrong
这是因为第一个catch函数返回的状态是resolved,不会触发后续的catch函数。
3. 看几道题
示例1
Promise.reject().then(()=>{
console.log('1')
}).catch(()=>{
console.log('2')
}).then(()=>{
console.log('3')
})
执行这段代码会打印出以下内容:
2
3
这是因为Promise.reject()返回一个立即被拒绝的Promise对象。在第一个.then()中,由于Promise对象被拒绝,所以不会执行回调函数,然后会进入.catch(),打印出’2’。接着,第一个.catch()返回一个Promise对象,然后进入下一个.then(),打印出’3’。
注意:由于Promise.reject()直接触发了catch()方法,因此then()方法中的回调函数是不会执行的。
示例2
Promise.resolve().then(() => {
console.log("1");
throw new Error("error1");
}).catch(() => {
console.log("2");
}).then(() => {
console.log("3");
});
这段代码的执行完打印的顺序是:
1
2
3
首先,使用Promise.resolve()返回一个resolved状态的promise,然后执行.then()方法中的回调函数。在该回调函数中,首先打印"1",然后抛出一个错误"error1"。
接着,使用.catch()方法捕捉上一步中的错误。由于之前抛出了错误,所以会跳转到.catch()方法中的回调函数。在这个回调函数中,打印"2"。
最后,使用.then()方法执行回调函数,并打印"3"。
总结起来,即先执行.then()中的回调函数,再执行.catch()中的回调函数,最后执行下一个.then()中的回调函数。在使用Promise链式调用时,错误会被捕获并传递到接下来的.catch()中,不会中断链式调用。
示例3
Promise.resolve().then(()=>{
console.log('1')
throw new Error('error1')
}).catch(()=>{
console.log('2')
}).catch(()=>{
console.log('3')
})
执行完打印的顺序是:
1
2
原因是:首先,Promise.resolve()是一个已经完成的promise,所以then()方法会立即执行,并打印出1。然后,在then()方法的回调函数中,因为有一个throw语句,所以会抛出一个错误。接下来,catch()方法捕获到错误并执行相应的回调函数,打印出2。由于没有再出现错误,所以第二个catch()方法不会被执行。
四、async/await
1. 基本用法
async/await是JavaScript中处理异步操作的一种方式。它使得编写异步代码更加简洁和易于理解,同时解决了一些常见的异步编程问题。
使用async/await可以将异步操作的代码写成同步的风格,避免了回调地狱和链式调用的复杂性。它利用ES7(ECMAScript 2017)中引入的async函数和await表达式,使得异步代码的执行流程更加清晰。
举个例子,假设有一个需要异步获取数据的函数fetchData:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched!');
}, 2000);
});
}
在使用传统的Promise方法时,我们需要通过.then()来处理异步操作的结果:
fetchData()
.then(data => {
console.log(data);
// 执行其他操作...
})
.catch(error => {
console.error(error);
});
使用async/await,可以将上述代码改成如下形式:
async function fetchData() {
try {
const data = await fetchData();
console.log(data);
// 执行其他操作...
} catch (error) {
console.error(error);
}
}
在async函数内部,可以使用await关键字暂停函数的执行,等待Promise对象的状态变为resolved后再继续执行。通过使用try-catch语句,也能够轻松地捕获和处理异步操作可能抛出的错误。
总结一下,async/await
的主要作用有:
- 简化异步操作的代码结构,使其更易读和维护。
- 解决了回调地狱(callback hell)和链式调用的问题。
- 更容易捕获和处理异步操作的错误。
需要注意的是,使用async/await的代码需要在支持ES7的运行时环境中运行,或者通过Babel等工具进行转译。
2. async/await和Promise有什么关系
async/await 是 Promise 的一种语法糖,它建立在 Promise 的基础上,使异步的操作更加直观和易读。
通过使用 async 关键字来修饰一个函数,这个函数会自动返回一个 Promise 对象。在这个函数内部,可以使用 await 关键字来等待一个 Promise 对象的完成,并返回 Promise 的结果。
下面是一个使用 async/await 和 Promise 的示例:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched");
}, 2000);
});
}
async function displayData() {
try {
console.log("Fetching data...");
const result = await fetchData();
console.log(result);
console.log("Data displayed");
} catch (error) {
console.log(error);
}
}
displayData();
在上面的例子中,fetchData 函数返回一个 Promise 对象,并在 2 秒后通过 resolve 方法将结果设为 “Data fetched”。
displayData 函数使用了 async 关键字来标记为异步函数,并通过 await 等待 fetchData 函数的完成。
当 fetchData 函数完成后,返回的结果会赋值给 result 变量,并输出结果到控制台。
在 try-catch 块中,如果 Promise 被 reject,则会捕获到错误,并输出到控制台。
async/await 消除了 Promise 的嵌套回调,使异步代码的写法更加简洁和易读。同时,它可以结合其他 Promise 方法,如 Promise.all、Promise.race 等,来更好地管理和处理异步操作。
总结
1.执行async函数,返回的是Promise对象
2. await相当于Promise的then
3. try…catch可捕获异常,代替了Promise的catch
3. 看个题
async function async1() {
console.log('1')
await async2()
console.log('2')
}
async function async2() {
console.log('3')
}
console.log('4')
async1()
console.log('5')
执行的顺序是:
4
1
3
5
2
这其实主要是await async2() 这里,只要是await后面的就相当于是异步的回调,只不过是用同步的写法来表示异步而已
,如下:
五、宏任务macroTask和微任务microTask
- 宏任务:setTimeout, setInterval, Ajax, DOM事件
- 微任务:Promise async/await
- 微任务执行时机比宏任务要早
微任务是dom渲染前触发,宏任务是dom渲染后触发
看个例子文章来源:https://www.toymoban.com/news/detail-791428.html
// 事件循环:每次循环称为tick,每次tick的任务队列称为task queue
// 1. 执行全局script
// 2. 执行微任务
// 3. 执行宏任务
// 4. 渲染UI
// 5. 执行下一个tick
// 微任务是dom渲染前触发,宏任务是dom渲染后触发
// 事件循环的顺序:先执行同步代码,再执行微任务,再执行宏任务
console.log('1')
setTimeout(() => {
console.log('2')
Promise.resolve().then(() => {
console.log('3')
})
}, 0)
Promise.resolve().then(() => {
console.log('4')
setTimeout(() => {
console.log('5')
}, 0)
})
console.log('6') // 1 6 4 2 3 5
这段代码的执行顺序如下:文章来源地址https://www.toymoban.com/news/detail-791428.html
- 执行
console.log('1')
,打印出1
- 执行
setTimeout()
,将回调函数放入宏任务队列,并设置延迟为0秒 - 执行第一个
Promise.resolve().then()
,将回调函数放入微任务队列 - 执行
console.log('6')
,打印出6
- 执行第一个微任务队列中的回调函数,打印出
4
- 执行第二个
setTimeout()
,将回调函数放入宏任务队列,并设置延迟为0秒 - 执行宏任务队列中的回调函数,打印出
2
- 执行第二个微任务队列中的回调函数,打印出
3
- 执行第二个宏任务
setTimeout()
,打印出5
到了这里,关于前端常见面试题之异步(event loop, promise, async/await, 宏任务/微任务)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!