1.Javascript 中的同步任务和异步任务
同步任务: 可以立即执行不需要等待,例如 var a = 123
异步任务: 不能立即执行,需要等待一段时间后才能执行,例如 xhr网络请求 和 setTimeout定时器。
执行流程如下:
-
执行整个脚本,解析到同步和异步任务时分别放进不同的执行场所,同步任务放进主线程,异步任务放进事件表(Event Table)中并注册函数。
-
当指定的事情执行完成时,事件表(Event Table)会将这个函数移入事件队列(Event Queue)中等待执行。
-
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
不断重复整个过程,形成Event Loop(事件循环)。
举一个例子:
console.log('111') //同步任务 1
setTimeout(()=>{ //异步任务 1
console.log('222')
},3000)
setTimeout(() => { // 异步任务 2
console.log('333')
}, 1000)
console.log('444') //同步任务 2
//答:111 444 333 222
对这个过程进行解析:
- 将同步任务1和2 放入主线程中,将异步任务1和2放入Event Table,并注册setTimeout的回调函数。
- 执行 主线程中的任务 按照从上到下的先后顺序 先输出 111 后 输出 444
- 当setTimeout定时结束后返回回调函数进入 Event Queue等待执行。
- 主线程执行完毕后从Event Queue读取setTimeout的回调函数放入主线程中并执行,因为两个异步任务执setTimeout执行时间不同,所以返回回调函数先后顺序也有所不同。
2.为什么异步任务中要设置队列(事件监听器)?
对于http、xhr等网络请求,执行的时间是不确定的,针对于这种情况设置了消息队列(事件监听器)。
事件监听器可以监听异步任务的状态,如果可以执行回调就会将相应的任务放入事件队列中。
举个例子:
假设两个网络请求
console.log('111') // 同步任务1
ajax().then(() => { // 异步任务1
console.log('222')
})
ajax().then(() => { // 异步任务2
console.log('333')
})
console.log('444') // 同步任务2
发起请求的两个异步任务,通过事件监听器来监听,如果异步任务2先返回回调函数,异步任务1后返回回调函数,那么就先执行异步任务2后执行异步任务1,答案则是:111 444 333 222。
再举个例子:
假设两个Promise
console.log("111");
const newPromise = new Promise((resolve, reject) => {
resolve("hello world");
}).then((res) => {
console.log(222);
});
const newPromise1 = new Promise((resolve, reject) => {
resolve("hello world");
}).then((res) => {
console.log(333);
});
console.log("444");
Promise是立即执行的同步函数,因此到resolve,都是同步的。但是.then()是异步的,他之间的回调会被放入微任务队列中等待执行。微任务和宏任务相关的知识在下面会讲。最后的结果是111 444 222 333
3.什么是宏任务和微任务?
一般情况,我们将异步任务又分为宏任务和微任务。
宏任务:由宿主(Node、浏览器等)发起,在微任务后运行,并会触发新一轮的 netxtTick()。
具体事件:
- script (可以理解为外层同步代码)
- setTimeout(定时器) / setInterval(计数器)
- UI rendering / UI事件
- postMessage,MessageChannel5. setImmediate,I/O(Node.js)
微任务:由JS引擎发起,在宏任务前运行,不会触发新一轮的 netxtTick()。
具体事件:
- Promise (then catch finally)
- MutaionObserver
- Object.observe(已废弃;Proxy 对象替代)
- process.nextTick(Node.js)
4.执行顺序
- 说法1:执行同步代码 ==> 检查微任务并执行 ==> 执行宏任务1 ==> 检查微任务并执行 ==> 执行宏任务2 ==> 检查微任务并执行 ==> 执行宏任务3 …
- 说法2:异步任务有宏任务和微任务两种,先将宏任务添加到宏任务队列中,将宏任务里面的微任务添加到微任务队列中,所有同步任务执行完后执行异步任务,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将宏任务从队列中调入主线程执行。这个过程一直循环执行(事件循环 Event loop)
一张比较易懂的图,可以配合着理解
2024-8-25补充:如果一个微任务在执行过程中创建了另一个微任务,新的微任务会被添加到当前微任务队列的末尾,并且会在下一轮事件循环内的宏任务执行前执行(也就是说在微任务进行过程中,如果出现了新的微任务,那么新的微任务就加在当前微任务队列末尾,在当前轮次进行处理(排队,赶个末班车))
4.async与await
4.1 async
async修饰符:async修饰的函数,默认返回 new Promise对象的resolve内容(若被async修饰的函数无返回值,则最终无返回值)。如此调用 async修饰的函数,可以直接使用then获取resolve值,用catch获取reject值
4.2 await(难点)
async方法内部,当程序执行到await方法时,会阻塞await方法后面的程序,进入await方法内部并执行到return前,然后跳出该async方法,执行与该async方法并列的同步任务。
await之后如果不是promise,await会阻塞后面的代码,会先执行async外面的同步代码,等外面的同步代码执行完成在执行async中的代码。
如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
例题1
async function async1() {
console.log("async1 start"); //2
await async2();
console.log("async1 end"); // 6
}
async function async2() {
console.log("async2"); //3
}
console.log("script start"); //1
setTimeout(function () {
console.log("setTimeout"); //8
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1"); //4
resolve();
}).then(function () {
console.log("promise2"); //7
});
console.log("script end"); //5
解释:(下面异步宏任务和异步微任务略写为宏任务与微任务)
- 先按顺序执行同步任务 “script start”
- 遇见setTimeout,属于宏任务,放入到宏任务队列中
- 进入async1函数, 执行同步任务(语句),打印"async1 start"
- 遇见 await之后的async2 进入方法内部,直行到return前(因为async2里没有return,所以就全部执行完),打印"async2",然后await async2()后面的语句都被阻塞,放入到微任务队列中,继续执行与async2方法并列的同步任务
- 进入Promise,同步打印"promise1"
- 遇到.then,放入微任务队列中
- 执行同步任务"script end"
- 同步任务执行结束后,开始执行所有微任务,按顺序输出"async1 end",“promise2”
- 微任务执行结束后,输出下一轮宏任务"setTimeout"
例题2
async function test1() {
console.log("start test1"); //1
console.log(await test2()); //7
console.log("end test1"); //8
}
async function test2() {
console.log("test2"); // 2
return await "return test2 value";
}
test1();
console.log("start async"); //3
setTimeout(() => {
console.log("setTimeout"); //9
}, 0);
new Promise((resolve, reject) => {
console.log("promise1"); //4
resolve();
}).then(() => {
console.log("promise2"); //6
});
console.log("end async"); // 5
解释:
- 执行同步任务"start test1"
- 先执行 await test2(),进入方法内部,执行到return前,打印"test2",然后挑出该async方法,阻塞后面的程序(个人理解:这一行因为没有return回来,所以也还没有执行完毕,所以 console.log(await test2()) 也没有打印)
- 继续执行同步任务"start async"
- 遇见setTimeout,放入宏任务队列
- promise里面同步执行"promise1"
- 遇见promise.then,放入微任务队列
- 执行同步任务"end async"
- test1外面的同步代码执行结束后,回到test1中,console.log(await test2())执行完成后返回Promise { “return test2 value”},是promise对象,推到微任务队列中
- 开始按顺序执行微任务队列,打印"promise2"、“return test2 value”、“end test1”
- 微任务队列完成后,执行下一轮宏任务,打印 “setTimeout”
2024.2.25补充:又看到两个挺不错的例题,补充进来
例题三
console.log('1')
setTimeout(function() {
console.log('2')
process.nextTick(function() {
console.log('3')
})
new Promise(function(resolve) {
console.log('4')
resolve()
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6')
})
new Promise(function(resolve) {
console.log('7')
resolve()
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9')
process.nextTick(function() {
console.log('10')
})
new Promise(function(resolve) {
console.log('11')
resolve()
}).then(function() {
console.log('12')
})
})
解释
例题四
new Promise(function (resolve) {
console.log('1')// 宏任务一
resolve()
}).then(function () {
console.log('3') // 宏任务一的微任务
})
setTimeout(function () { // 宏任务二
console.log('4')
setTimeout(function () { // 宏任务五
console.log('7')
new Promise(function (resolve) {
console.log('8')
resolve()
}).then(function () {
console.log('10')
setTimeout(function () { // 宏任务七
console.log('12')
})
})
console.log('9')
})
})
setTimeout(function () { // 宏任务三
console.log('5')
})
setTimeout(function () { // 宏任务四
console.log('6')
setTimeout(function () { // 宏任务六
console.log('11')
})
})
console.log('2') // 宏任务一
解释
这道题挺恶心的,套来套去,不过难度不算大,动笔写一下画个流程图就行,答案就是1 2 3 … 11
2024-2-29 例题五
const async1 = async () => {
console.log("async1"); //2
setTimeout(() => {
console.log("timer1"); // 宏任务1
}, 2000);
await new Promise((resolve) => {
console.log("promise1"); //3 await 阻塞后面的代码
});
// 因为没有Promise状态一直处于pending,没有变成resolve,所以下面两行不会执行
console.log("async1 end");
return "async1 success";
};
console.log("script start"); //1
async1().then((res) => console.log(res));
console.log("script end"); // 4
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then((res) => console.log(res)); //5:1
setTimeout(() => {
console.log("timer2"); //宏任务2 虽然timer2排在timer1后面,但是timer2只要一秒。因此还是先输出timer2
}, 1000);
输出结果:
script start
async1
promise1
script end
1
timer2
timer1
2024-8-24 例题六
console.log(1);
setTimeout(() => {
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise((resolve) => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
});
});
process.nextTick(() => {
console.log(6);
});
new Promise((resolve) => {
console.log(7);
resolve();
}).then(() => {
console.log(8);
});
setTimeout(() => {
console.log(9);
process.nextTick(() => {
console.log(10);
});
}, 0);
new Promise((resolve) => {
console.log(11);
resolve();
}).then(() => {
console.log(12);
});
解释
console.log(1);
// 宏1
setTimeout(() => {
console.log(2);
process.nextTick(() => {
console.log(3); // 微2-1
});
new Promise((resolve) => {
console.log(4);
resolve();
}).then(() => {
console.log(5); // 微2-2
});
});
// 微1
process.nextTick(() => {
console.log(6);
});
new Promise((resolve) => {
console.log(7);
resolve();
}).then(() => {
console.log(8); // 微2
});
// 宏2
setTimeout(() => {
console.log(9);
process.nextTick(() => {
console.log(10);
});
}, 0);
new Promise((resolve) => {
console.log(11);
resolve();
}).then(() => {
console.log(12); // 微3
});
console.log("111");
const newPromise = new Promise((resolve, reject) => {
resolve("hello world");
}).then((res) => {
console.log(222); // 微4
});
const newPromise1 = new Promise((resolve, reject) => {
resolve("hello world");
}).then((res) => {
console.log(333); // 微5
});
console.log("444");
答案:1,7,11,111,444,6,8,12,222,333(第一个宏任务),2,4,3,5(第二个宏任务),9,10(第三个宏任务)
这边需要注意的是,每一个宏任务执行结束时都会检查当前是否有可执行的微任务,如果有的话就都执行掉。所以说4后面紧跟着5
2024-8-25
Promise.resolve(1)
.then(2) // 微1
.then(Promise.resolve(3))
.catch(4)
.then((res) => console.log(res));
输出1
解释:
- Promise.resolve(1): 这是一个立即被 resolved 的 Promise,值为 1。
- 第一个 .then(2):通常,.then 接收两个参数:onFulfilled 和 onRejected,这两个参数通常是函数。
如果传递的不是函数,比如这里传递的是数字 2,会被忽略,即默认地会直接传递上一个 Promise 的 resolved 值。因此,这个 .then 不会改变 Promise 的状态或值,Promise 仍然 resolved,值仍然是 1。 - 第二个 .then(Promise.resolve(3)):同样地,.then 的参数需要是函数,而 Promise.resolve(3) 也是一个 Promise 对象,因此它会被忽略。Promise 继续保持 resolved 状态,值仍然是 1。
- .catch(4): catch 方法用于捕获前面链条中的任何错误,但是前面的链条中没有抛出错误,因此不会进入catch回调里面,Promise 依然保持 resolved 状态,值仍然是 1。
- 最后一个 .then((res) => console.log(res)): 这个 .then 终于接收到一个函数作为参数。因此它会将前面的 resolved 值 1 作为参数传递给这个函数。最后,console.log(res) 会输出 1。
2024-9-11
async function async1() {
console.log("async1 start");
let result = await async2();
console.log(result);
await new Promise((resolve) => {
console.log("no resolve");
});
console.log("async1 end");
}
async function async2() {
console.log("async2");
return 1;
}
console.log("script start");
async1();
console.log("script end");
输出: script start, async1 start, async2, script end, 1, no resolve
解释:
这里最需要注意的就是await 一个async函数和 await一个Promise的区别。
1. 关于async函数,他是会隐式地包装一个Promise.resolve,因此即使你没有return一个值,他还是返回了一个Promise.resolve(undefined),而且状态是fulfilled。因此他之后的代码还是可以执行;
2. 关于Promise,如果他里面没有使用resolve或者reject函数,那么他的状态就一直是pending,就不会执行await之后的代码(像这题里面, async1 end就没有打印出来)
2024-9-16
有几个概念需要注意一下
- 在Promise里面直接return一个数,那么因为没有使用resolve或者reject,返回的不是一个Promise,因此也不能被下面的.then,.catch给捕获到;
async function P1(){
console.log('P1');
await new Promise((resolve,reject)=>{
console.log('P1-1');
return 12
})
console.log('P1 end');
}
P1();
// p1 end不会被输出
- 在.then()或者.catch()里面直接返回一个数,那么会被隐式地包装为Promise.resolve(aa),可以继续进行链式调用
new Promise((resolve, reject) => {
resolve("Initial value");
})
.then(data => {
console.log(data); // 输出: "Initial value"
return "Returned value"; // 普通值
})
.then(result => {
console.log(result); // 输出: "Returned value"
});
- 在async函数里面返回一个数,也会被隐式地包装为Promise.resolve(aa)
async function fetchData() {
return "Data fetched"; // 返回普通字符串
}
fetchData().then(data => {
console.log(data); // 输出: "Data fetched"
});