一直没有深入了解过 JavaScript 的事件执行机制,直到看到了这篇文章: 《这一次,彻底弄懂 JavaScript 执行机制》 才发觉熟悉 JavaScript 的执行机制非常重要.
毕竟在跟进项目中偶尔需要排查为什么会出现函数执行顺序不一样的情况.
感谢作者浅显易懂的文字让我获益匪浅,以下是自己对 JavaScript 执行机制的理解,全是流水账.
文章主要叙述:
1:单线程和异步任务
2: 异步任务的分类
3:setTimeout 和 setInterval 的执行方式
单线程和异步任务
JavaScript 的特点就是单线程,也就是说,同一个时间只能做一件事.换句话说就是一行一行地按照顺序执行代码:
console.log(1);
let timeId = setTimeout(() => {
console.log(2);
},0);
console.log(3
);
运行上面的代码预想中的是 1,2,3.但实际上打印出来的是:1,3,2
从上面的运行结果来看 JavaScript 等所有的同步任务执行完之后,再去执行异步任务
这是因为虽然 JavaScript 是单线程操作,但如果不做一些处理遇到类似 setTimeout 之类的异步操作就会导致阻塞.
这里有一个疑问了,在主线程执行同步任务的过程中,异步任务就真的任何事情都不做了吗?
实际上:
除了主线程之外,还存在一个 "任务队列"(task queue).
异步任务指的是,不进入主线程,而进入 "任务队列"(task queue)的任务.
只要异步任务有了运行结果,就在 "任务队列" 之中放置一个事件.然后通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行.
等主线程执行完同步任务后就会回过头去查找任务队列中有哪些事件,事件对应的异步任务进入执行栈,开始执行.执行完该任务后再去查找任务队列中可执行的异步任务,直到任务队列清空.
所以这里执行顺序是这样的:
1:执行 console.log(1)
2:遇到 timeId 是异步任务,先放到任务队列
3:立即执行 console.log(3)
4:同步任务执行完毕,去查找任务队列里面的事件,发现 timeId 有运行结果了,执行 timeId.队列中没有其他的事件了,主线程运行完毕.
再来看段代码:
let timeId = setTimeout(() => {
function task() {
console.log('会2秒之后运行吗');
}
task();
},2000);
let oldTime = new Date();
function sleep(){
let newTime = new Date();
console.log('time:' + ( newTime.getSeconds() - oldTime.getSeconds()));
if( newTime.getSeconds() - oldTime.getSeconds() < 5) {
sleep();
}
}
sleep();
运行上面代码,发现 task 并没有在 2 秒之内执行而是在 5 秒之后才执行.
这是因为虽然 timeId 暂时被挂起,并且在 2 秒后有了运行结果后在 "任务队列" 之中放置一个事件通知主线程 timeId 可以执行了.
但因为主线程中的同步任务 sleep 要 5 秒之后才运行完毕,导致执行栈 5 秒后才去任务队列中执行等待中的 timeId 函数
(好比约了朋友去玩,出门前要换衣服,要约滴滴打车.用手机约好车之后就去换衣服,但是换衣服实在太久了,车都来了衣服还没换好.司机打电话说我到了,你快过来坐车吧.
我跟司机说,衣服没换好,你先等等吧.过了半小时之后终于换好了衣服(司机等得要砍人了)终于可以去坐车了)
总得来说其实 JavaScript 只有一个主线程,运行过程中碰到异步任务就先挂起.而有了运行结果表示准备好了可以执行的异步任务就进入执行栈中在同步任务后面去排队.
异步任务的分类:
macro-task(宏任务):setTimeout,setInterval,setImmediate,I/O(ajax),UI 交互事件 (onClick,onScroll...)
micro-task(微任务):Promise,process.nextTick,MutaionObserver
不同的 API 注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop(事件循环)将它们依次放入主线程中执行
进入整体代码 (宏任务) 后,开始第一次循环.接着执行所有的微任务.然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务
测试一下:
console.log(1);
let timeId = setTimeout(() => {
console.log(2);
let Promise02 = new Promise(resolve => {
console.log(3);
resolve();
}).then(() => {
console.log(4);
});
},0);
let Promise01 = new Promise(resolve => {
console.log(5);
resolve();
}).then(() => {
console.log(6);
});
console.log(7);
打印出来是:1,5,7,6,2,3,4
所以执行顺序实际上是这样的:
1:执行宏任务(整体代码)
2:执行微任务(Promise01 的 then 方法)
3:执行宏任务(timeId)
4:执行微任务(timeId 里面 Promise02 的 then 方法)
setTimeout 和 setInterval 的执行方式
setTimeout 和 setInterval 都是异步可以延时执行的方法.
setTimeout(fn,ms) 到了 ms 秒后 fn 会进入任务队列去等待执行,而 setInterval(fn, ms) 则是每到了 ms 秒都会有一个 fn 进入任务队列去排队等待执行,直到某个条件结束.
关于 setTimeout(fn,ms) 中的 ms 设置:
let timeId01 = setTimeout(() => {
console.log(1);
},1);
let timeId02 = setTimeout(() => {
console.log(2);
},0);
上面代码中 timeId01 的 timer 设置为 1,timeId02 的 timer 设置为 0.按理说 timeId02 的执行顺序比 timeId01 要优先,但实际上打印结果是:1,2
这是因为一般来说 timer 最小只能设置 4ms(嵌套层超过 5,并且设置的 timeout 的时间少于 4 才有效).但为了向实现看齐可最小设置 1ms,0ms 的时候会被设置为 1ms
其中setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行.
来源: http://www.bubuko.com/infodetail-2469609.html