理解JavaScript的执行机制
一直没有深入了解过JavaScript的事件执行机制,直到看到了这篇文章:《这一次,彻底弄懂JavaScript执行机制》 才发觉熟悉JavaScript的执行机制非常重要。
毕竟在跟进项目中偶尔需要排查为什么会出现函数执行顺序不一样的情况。
感谢作者浅显易懂的文字让我获益匪浅,以下是自己对JavaScript执行机制的理解,全是流水账。
文章主要叙述:
1:单线程和异步任务
2: 任务更精细的分类
3:setTimeout 和 setInterval 的执行方式
同步任务和异步任务
JavaScript的特点就是单线程,也就是说,同一个时间只能做一件事。换句话说就是一行一行地按照顺序执行代码
1:同步任务指的是直接进入主线程执行的任务。
2:异步任务指的是,不进入主线程、而进入"任务队列"(task queue)等待同步任务全部执行完毕主线程执行栈为空时再去任务队列查找等待被调用的函数
console.log(1); let timeId = setTimeout(() => { console.log(2); },0);
console.log(3);
运行上面的代码打印出来的是:1,3,2
这里执行顺序是这样的:
1:执行console.log(1)
2:遇到timeId是异步任务,先放到任务队列
3:立即执行console.log(3)
4:同步任务执行完毕,去查找任务队列里面的事件,发现timeId有运行结果了,执行timeId。队列中没有其他的事件了,主线程运行完毕。
也就是常说的JS事件循环:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue(任务队列)。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环) 直到任务队列清空。
那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
再来看段代码:
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(宏任务):包括整体代码script、setTimeout、setInterval、setImmediate、I/O(ajax)、UI交互事件(onClick,onScroll...)
micro-task(微任务):Promise、process.nextTick、MutaionObserver
不同的API注册的异步任务会依次进入自身对应的队列中,然后等待Event Loop(事件循环)将它们依次放入主线程中执行
- 进入整体代码script(宏任务)后开始第一次循环。接着执行当前所有的微任务。
- 然后再次从宏任务开始,找到其中一个任务队列执行完毕再执行当前宏任务中所有的微任务。
- 继续执行以上一步过程,直到所有任务执行完毕
测试一下:
console.log(1); let timeId01 = 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); }); let timeId02 = setTimeout(() => { console.log(8); let Promise03 = new Promise(resolve => { console.log(9); resolve(); }).then(() => { console.log(10); }); }, 0) console.log(7);
打印出来是:1,5,7,6,2,3,4,8,9,10
执行顺序实际上是这样的:
1:执行宏任务(整体代码)// 1,5,7
2:执行微任务(Promise01的then方法)// 6
3:执行宏任务(timeId)//2,3
4:执行微任务(timeId里面Promise02的then方法)// 4
5:执行宏任务(timeId02)// 8,9
6:执行微任务(timeId02里面Promise03的then方法)// 10
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)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。