JS是一门单线程非阻塞的脚本语言,也就是说,所有的任务都是串行执行,同一时间只能做一件事。但当我们做一些比如:网络请求、定时器、事件监听等一些比较耗时的任务时,如果也让JS的单线程来做,那不仅仅执行效率低,页面也会出现卡死现象,用户体验会非常差。那如何解决呢,用事件队列。这也是为什么说,它是非阻塞的
虽然JS是单线程的,但JS的宿主环境(浏览器)是多线程的,浏览器会为这些耗时的任务开辟另外的线程,主要包括:Http请求线程、浏览器定时触发器、浏览器事件触发线程,来异步执行
说明:
所有的同步任务都会在JS线程上执行,函数的执行是通过进栈和出栈实现的,当我们调用 一个函数时,JS会生成一个与这个函数对应的执行环境(context),又叫执行上下文,这个执行 环境中存在着这个方法的私有作用域、上层作用域的指向、方法的参数、这个作用域中定义的变量和this对象,当调用的函数内又调用了其他函数时,因为JS是单线程的,同一时间只能执行一个函数,所以这些函数被排队在一个单独的地方,这个地方就是执行栈。
当一个脚本第一次执行的时候,JS引擎会将整段JS代码加入到执行栈,然后从头开始执行,当执行到一个函数,就会向执行栈中添加这个函数的执行环境,然后进入这个执行环境执行其中的代码,执行完毕后,JS会退出这个执行环境并把这个执行环境销毁,回到上一个执行环境,当遇到异步事件时,JS引擎会将这个事件挂起,执行栈继续执行,当这个异步事件返回结果后,会将这个事件加入到与当前执行栈不同的一个队列,我们称之为事件队列,被放入的事件不会立即执行其回调,而是要等到当前执行栈中所有的任务都执行完毕,JS线程处于闲置状态时,主线程才会去查找事件队列是否有任务,如果有,那么JS线程就会从中取出排在第一位的事件,并把这个事件对应的回调放入到执行栈中,然后执行其中的同步代码,如此反复,这样就形成了一个无限的循环,我们称之为事件循环。
实际上异步任务之间并不相同,因此他们之间也有优先级之分,所以任务队列被分成两种类型:
microtask queue 微任务队列,macrotask queue 宏任务队列
属于宏任务:
属于微任务:
一次事件循环流程,JS首先从宏任务队列中取出第一个宏任务加入到执行栈开始执行,如果在执行过程中又产生了宏任务,那么这个任务将在下次事件循环中才能执行,如果在执行过程中产生了微任务,那么当执行栈为空时,就会取出微任务队列中的全部任务,放入执行栈执行,如果在执行微任务时又产生了宏任务,则也要在下下次事件循环中才能执行,如果又产生了微任务,那么这个微任务将会在这次事件循环中执行,如此反复,就形成了事件循环
可以简单的理解一次事件循环流程:取出一个宏任务开始->执行栈执行->执行栈为空->微任务队列执行->微任务队列为空->完成
console.log("start");
setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
console.log("setTimeout1-Promise-start");
resolve("setTimeout1-Promise");
}).then(value => {
console.log(value);
console.log("Promise1-then1");
return value;
}).then(value => {
console.log(value);
})
}, 0);
new Promise(function (resolve) {
console.log("Promise1-start");
resolve("Promise1-resolve");
console.log("Promise1-end");
}).then(value => {
console.log(value);
console.log("Promise1-then1");
return value;
}).then(value => {
console.log(value);
console.log("Promise1-then2");
Promise.resolve("Promise2")
.then(value1 => {
console.log(value1);
console.log("Promise2-then1");
return value1;
}).then(value1 => {
console.log(value1);
console.log("Promise2-then2");
});
});
setTimeout(function () {
console.log("setTimeout2")
new Promise(function (resolve) {
console.log("setTimeout2-Promise-start");
resolve("setTimeout2-Promise");
}).then(value => {
console.log(value);
}).then(value => {
console.log(value);
})
}, 0);
console.log("end");
打印输出为:
start
Promise1-start
Promise1-end
end
Promise1-resolve
Promise1-then1
Promise1-resolve
Promise1-then2
Promise2
Promise2-then1
Promise2
Promise2-then2
setTimeout1
setTimeout1-Promise-start
setTimeout1-Promise
Promise1-then1
setTimeout1-Promise
setTimeout2
setTimeout2-Promise-start
setTimeout2-Promise
undefined
注意:
执行流程:
第一轮事件循环:
1:JS线程读取整段JS代码作为一个macrotask宏任务先被执行,形成相应的堆和执行栈
2:遇到console.log(“start”),输出start
3:遇到第一个setTimeout,交给浏览器异步模块处理,时间到达时,将回调函数作为macrotask宏任务,添加到macrotask queue宏任务队列
4:遇到Promise对象,立即执行,遇到内部的resolve,将回调函数作为microtask微任务,添加到microtask queue微任务队列
5:遇到第二个setTimeout,交给浏览器异步模块处理,时间到达时,将回调函数作为macrotask宏任务,添加到macrotask queue宏任务队列
6:遇到console.log(“end”),输出end
7:从macrotask queue宏任务队列中删除执行过的macrotask宏任务
8:执行栈为空,取出microtask queue微任务队列中的全部任务,放入执行栈执行
9:执行微任务队列中Promise的回调函数,在回调函数中又有新的微任务产生了,同时也一并执行
10:执行栈为空,microtask queue微任务队列为空,开始下一轮事件循环
第一轮console打印内容:
start
Promise1-start
Promise1-end
end
Promise1-resolve
Promise1-then1
Promise1-resolve
Promise1-then2
Promise2
Promise2-then1
Promise2
Promise2-then2
第二轮事件循环:
1:从宏任务队列中取出排在队头的任务,也就是第一轮中第三步的回调函数,将任务放到执行栈执行,输出setTimeout1,
2:在第一步的回调函数中遇到Promise,立即执行,遇到内部的resolve,将回调函数作为microtask微任务,添加到microtask queue微任务队列
3:第一步的宏任务执行完毕,此时执行栈为空,
4:从macrotask queue宏任务队列中删除执行过的macrotask宏任务
5:取出microtask queue微任务队列中的全部任务,放入执行栈执行
6:执行微任务队列中Promise的回调函数,在回调函数中又有新的微任务产生了,同时也一并执行
7:执行栈为空,microtask queue微任务队列为空,开始下一轮事件循环
第二轮console打印内容:
setTimeout1
setTimeout1-Promise-start
setTimeout1-Promise
Promise1-then1
setTimeout1-Promise
第三轮事件循环:
1:从宏任务队列中取出排在队头的任务,也就是第一轮中第五步的回调函数,将任务放到执行栈执行,输出setTimeout2,
2:在第一步的回调函数中遇到Promise,立即执行,遇到内部的resolve,将回调函数作为microtask微任务,添加到microtask queue微任务队列
3:第一步的宏任务执行完毕,此时执行栈为空,
4:从macrotask queue宏任务队列中删除执行过的macrotask宏任务
5:取出microtask queue微任务队列中的全部任务,放入执行栈执行
6:执行微任务队列中Promise的回调函数,在回调函数中又有新的微任务产生了,同时也一并执行
7:执行栈为空,microtask queue微任务队列为空,开始下一轮事件循环
第三轮console打印内容:
setTimeout2
setTimeout2-Promise-start
setTimeout2-Promise
undefined
总结:
1:JS事件循环总是从一个宏任务开始执行
2:一个事件循环过程中,只执行一个宏任务,但可执行多个微任务
3:执行栈中的任务产生的微任务会在当前事件循环内执行
4:执行栈中的任务产生的宏任务要在下一次事件循环才会执行
Promise出现主要是解决回调地狱的问题,让代码看起来更优雅,更易维护,但Promise一旦状态改变,就不会再变,状态会被凝固
new Promise(resolve => {
console.log("promise-start");
resolve("resolve");
console.log("promise-end");
}).then(value => {
console.log(value);
return "then";
}).then(value => {
console.log(value);
throw new Error("error");
}).catch(result => {
console.log(result);
})
打印输出:
promise-start
promise-end
resolve
then
Error: error
传入普通对象
let p = Promise.resolve("promise");
console.log(p);
p.then(value => {
console.log(value)
}
);
打印输出:
Promise {: "promise"}
promise
传入Promise对象
let promise = new Promise(function (resolve) {
console.log("promise");
});
let p = Promise.resolve(promise);
console.log(p);
console.log(promise===p);
p.then(value => {
//没有调用resolve方法所以不会打印东西
console.log(value);
})
打印输出:
promise
Promise {}
true
async:申明一个函数是异步的,返回是一个promise对象,但必须要等到内部所有await后面的表达式都执行完,状态才会发生改变
await:等待的意思,必须出现在async修饰的函数中,等待的可以是promise对象也可以是其他值,但如果是其他值,则会被转成一个立即resolve的Promise对象,await实际上是一个让出线程的标志,首先await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面JS执行栈的代码,等本轮事件循环 执行完以后再跳回到async函数中等待await。
个人理解
返回promise对象和非promise对象的区别是:
返回promise对象会把回调函数放到微任务队列的末尾,返回非promise对象,会马上把回调函数放到微任务队列中去。
注意:
await后面可以跟着promise对象,但不必写then方法,可直接拿到返回值
1:如果在async函数中return一个直接量,async会把这个直接量通过Promise.resolve封装成Promise对象,可通过then方法获取到这个值
async function testAsync() {
return "hello async";
}
let result = testAsync();
console.log(result);
result.then(value => {
console.log(value);
})
打印结果:
Promise {: "hello async"}
hello async
2:如果在async函数中return一个Promise.resolve,async会重新封装一个Promise对象并返回,也可通过then方法拿到Promise.resolve中的值
let promise = Promise.resolve("promise");
console.log(promise);
async function testAsync() {
return promise;
}
let result = testAsync();
console.log(result);
console.log(result === promise);
result.then(value => {
console.log(value);
})
打印结果:
Promise {: "promise"}
Promise {}
false
promise
3:如果在async函数中return一个new Promise,async也会重新封装一个Promise对象并返回,也可通过then方法拿到new Promise中resolve中的值
let promise = new Promise(resolve => {
resolve("promise");
});
console.log(promise);
async function testAsync() {
return promise;
}
let result = testAsync();
console.log(result);
console.log(result === promise);
result.then(value => {
console.log(value);
})
打印结果:
Promise {: "promise"}
Promise {}
false
promise
4:如果在async函数中没有返回值,async也会通过Promise.resolve封装成Promise对象返回,只是通过then方法获取的值为undefined
async function testAsync() {
console.log("hello async");
}
let result = testAsync();
console.log(result);
result.then(value => {
console.log(value);
})
打印结果:
hello async
Promise {: undefined}
undefined
console.log("script start");
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise1-then");
});
setTimeout(function () {
console.log("settimeout1");
}, 0);
async function async1() {
console.log("async1 start");
let resulet = await async2();
console.log(resulet);
console.log("async1 end");
}
async function async2() {
console.log('async2');
return "async2 return"
}
setTimeout(function () {
console.log("settimeout2");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise2");
resolve();
}).then(function () {
console.log("promise2-then");
});
console.log('script end');
打印输出:
script start
promise1
async1 start
async2
promise2
script end
promise1-then
async2 return
async1 end
promise2-then
settimeout1
settimeout2
Promise对象和async修饰的函数都是立即执行函数,所以上述代码放到执行栈中,首先打印出的是
script start
promise1
async1 start
async2
promise2
script end
因为async2由async修饰,所以是一个异步函数,而内部返回了一个直接量,所以会被Promise封装,并立即调用resolve方法,所以此时也会马上放到微任务队列中,紧接着就是Promise2中的resolve也会放到微任务队列,所以打印的顺序是
promise1-then
async2 return
async1 end
promise2-then
微任务执行完后再打印宏任务中的
settimeout1
settimeout2
如果把async2改成通过返回new Promise对象
async function async2() {
console.log('async2');
return new Promise(resolve => resolve("async2 return"))
}
则结果会是:
promise1-then
promise2-then
async2 return
async1 end
首先记住这个结论:如果在async中return了通过new的Promise对象,则这个里面的任务会排在微任务队列的末尾
我个人理解:
因为当async2函数内返回promise对象时,会先把promise对象放到微任务队列(只有对象内的resolve没有执行),等执行promise时,其实就是执行resolve,然后又放到微任务队列,所以返回promise对象的时候,它的任务会排在微任务队列末尾
https://www.cnblogs.com/cangqinglang/p/8967268.html
https://segmentfault.com/a/1190000015112913
https://www.cnblogs.com/hity-tt/p/6733062.html
https://segmentfault.com/a/1190000015057278?utm_medium=referral&utm_source=tuicool