JavaScript的任务分为两种同步和异步:
- 同步任务: 在主线程【执行栈(Call Stack)】上排队执行的任务,只有一个任务执行完毕,才能执行下一个任务,
- 异步任务: 不进入主线程,而是放在【任务队列】中,若有多个异步任务则需要在队列中排队等待,当执行栈有空之后,不断取出消息队列中的任务执行;
上面提到了队列和执行栈,下面就先来看看这两个概念:
一、JavaScript的事件
1)执行栈:一个存储函数调用的栈结构,遵循先进后出的原则。它主要负责跟踪所有要执行的代码。 每当一个函数执行完成时,就会从堆栈中弹出(pop)该执行完成函数;如果有代码需要进去执行的话,就进行 push 操作。以下图为例:
b站峰华前端工程师的视频
2)任务队列: 也就是消息队列,它用来保存异步任务,遵循先进先出的原则。它主要负责将新的任务发送到队列中进行处理。
JavaScript在执行代码时,会将同步的代码按照顺序排在执行栈中,然后依次执行里面的函数。当遇到异步任务时,就将其放入任务队列中,等待当前执行栈所有同步代码执行完成之后,就会从异步任务队列中取出已完成的异步任务的回调并将其放入执行栈中继续执行,如此循环往复,直到执行完所有任务。
二、任务队列的种类
任务队列其实不止一种,根据任务种类的不同,可以分为微任务(micro task)队列和宏任务(macro task)队列。常见的任务如下:
宏任务
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微任务
- process.nextTick (Node独有)
- Promise
- async/await
- Object.observe
- MutationObserver
console.log("script start");
setTimeout(() => {
console.log("setTimeout --- outer1");
setTimeout(() => {
console.log("setTimeout--inner");
}, 400);
}, 0);
setTimeout(() => {
console.log("setTimeout --- outer2");
Promise.resolve().then(() => {
console.log("promise -- inner");
}, 450);
}, 0);
console.log("script end");
JS任务栈 | script start, script end |
---|---|
宏任务 | setTImout-outer1, setTimeout-outer2, setTimout-inner |
微任务 | promise-inner |
JS任务栈 setTImout-outer1, setTimeout-outer2, setTimout-inner
宏任务 promise-inner
微任务
三、nodeJs中的EventLoop
单从API层面上来理解,Node新增了两个方法可以用来使用:
- 微任务的process.nextTick
- 宏任务的setImmediate
但其事件循环的方式和浏览器不一致,不要弄混。
nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别如下:
timers:执行setTimeout() 和 setInterval()中到期的callback。
I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
idle, prepare:队列的移动,仅内部使用
poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
check:执行setImmediate的callback
close callbacks:执行close事件的callback,例如socket.on(“close”,func)
与浏览器的区别:
setImmediate(() => {
console.log('timer1')
Promise.resolve().then(function () {
console.log('promise1')
})
})
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function () {
console.log('promise2')
})
}, 0)
输出:
timer1 或 timer2
promise1 promise2
timer2 timer1
promise2 promise1
1)setTimeout 和 setImmediate
只打印setTimeout()和setImmediate()结果
setImmediate((_) => console.log("setImmediate"));
setTimeout((_) => console.log("setTimeout"));
结果 :setTimeout和setImmediate谁先出现不异地昂
原因:
这是因为setTimeout的第二个参数默认为0。但是实际上,Node 做不到0毫秒,最少也需要1毫秒,根据官方文档,第二个参数的取值范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f,0)等同于setTimeout(f, 1)。
【疑惑:setTimeout是到点才把callback放到宏任务中还是,直接放到宏任务中,到点才执行?】
扩展:
setTimeout()和setImmediate() 后面存在代码的问题
setImmediate((_) => console.log("setImmediate"));
setTimeout((_) => console.log("setTimeout"));
let countdown = 100
while (countdown--) {
console.log("countdown", countdown);
}
结果:setTimeout setImmediate
原因:进入timer试setTimeout一定存在并可以执行
2)process.nextTick()
process.nextTick这个名字有点误导,它是在本轮循环执行的,而且是所有异步任务里面最快执行的。
Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列。所以,下面这行代码是第二个输出结果。
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
Promise.resolve().then(() => console.log(4));
process.nextTick(() => console.log(3));
(() => console.log(5))();
结果:
5
3
4
1
2
Tips:
只有在nextTickQueue执行完毕之后,microTaskQueue才会执行
四、注意点
1)事件通过 UI触发 或手动触发
<html>
<head>
<title>测试任务执行顺序</title>
<style>
.outer {
width: 200px;
height: 200px;
background-color: brown;
cursor: pointer;
}
.inner {
width: 100px;
height: 100px;
background-color: blanchedalmond;
cursor: pointer;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
<script>
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");
function onClick() {
console.log("click", this);
setTimeout(() => {
console.log("setTimeout", this);
}, 0);
Promise.resolve().then(() => {
console.log("promise", this);
});
}
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);
// inner.click(); -> dispathEvent() ->
</script>
</body>
</html>
UI触发:click -inner -> promise - inner -> click -outer -> promise -outer -> setTimeout -inner -> setTimeout -outer
click触发:click - inner -> click -outer -> promise -inner -> promise -outer -> setTimeout -inner -> setTimeout -outer
原因:
click触发是直接在主线程中调用dispatchEvent。
疑惑?inner -> outer的冒泡,是否会区分UI触发还是dispatchEvent触发?
猜测:会区分
原因:UI触发是新建一个宏任务,将inner.onClick() 和 outer.onClick()分别放入;代码触发则是直接操作;
我们认为dispatchEvent的方法,一个是每次冒泡都产生一个宏任务,另一个是每次都同步执行任务;
实验代码:
function inner() {
console.log("click inner");
Promise.resolve().then(() => {
console.log("inner - click");
});
}
function outer() {
console.log("click outer");
Promise.resolve().then(() => {
console.log("outer - click");
});
}
function dispatch() {
inner()
outer()
}
setTimeout(dispatch); // click inner -> click outer -> inner -click -> outer -click 同步执行;
2)setTimeout是何时放入宏任务中
以下setTimeout任务是在100ms之后,将() => {console.log(“setTimeout”);} 放入宏任务中;
setTimeout(() => {
console.log("setTimeout - 100");
}, 100);
setTimeout(() => {
console.log("setTimeout - 120");
}, 120);
console.log('setTimeout - end');
问题:js是同步执行的代码,setTimeout()加入消息队列中去了,在不断加入其他Task,如何做到delay执行setTimeout的回调嘞?
setTimeout原理解析
js中存在两个消息队列,一个是执行任务队列,一个是执行延时任务队列;
setTimeout会创建一个Task,放到执行延时任务队列中;
在任务队列执行执行完毕之后,延时任务队列会计算当前到执行时间的任务有哪些,然后进行执行。
- 题目: 手写setTimeout
function mySetTimeout(callback, timeout) {
let currTime = Date.now();
while (Date.now() - currTime < timeout) {
console.log("wait", Date.now());
}
callback.apply(this);
}
mySetTimeout(() => {
console.log("execute my setTimeout");
}, 10);
https://zhuanlan.zhihu.com/p/60505970 【setTimeout源码解析】
nodejs关联了底层定时器,我们直接手写的方法,会阻塞主线程,失去了所谓异步的效果;
- 延展问题:使用setInterval实现setTimeout
function mySetInterval(callback, time) {
setTimeout(() => {
callback();
mySetInterval(callback);
}, time);
}
mySetInterval(() => {
console.log("muSetInterval");
}, 10);
问题:不会停止 --> clearSetInterval()
3)async/await
当执行到await时,等同于函数暂停执行,直到await等待的Promise状态改变了,才会回到这个函数执行。【类似生成器】
async function async1() {
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2 start");
return Promise.resolve().then(() => {
console.log("async2 end1");
});
}
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");
执行结果:
async2 start
Promise
script end
async2 end1
promise1
promise2
async1 end
setTimout
https://juejin.cn/post/6992167223523541023#heading-5
五、经典题目
1)为什么要引入微任务,只有宏任务可以吗?
给紧急任务一个插队的机会,否则新入队的任务永远被放在队尾。区分了微任务和宏任务后,本轮循环中的微任务实际上就是在插队,这样微任务中所做的状态修改,在下一轮事件循环中也能得到同步。
2)为什么 await 后面的代码会进入到promise队列中的微任务?
async/await 只是操作 promise 的语法糖,最后的本质return promise。