前端面试死角—JavaScript执行机制

掌握JavaScript是前端开发者目前的必备技能,但代码敲得飞起却对它的执行机制一问三不知。了解JavaScript的执行机制对你的编码能力和代码理解将提高一个层次。

1. JavaScript执行机制?
很多人第一反应:单线程、自上而下。都没错,javascript是一门单线程语言,虽然在HTML5中提出了Web-Worker的概念,但它单线程的核心仍未改变。也可以说,javascript的“多线程”操作都是建立在单线程机制之上的。先简单看个场景:

console.log("场景开始")

setTimeout(() => {
  console.log("定时器结束")
},3000)

new Promise((resolve,reject) => {
  console.log("Promise开始")
  resolve()
}).then(() => {
  console.log("Promise结束")
})

console.log("代码执行结束")

/**
 * 你以为:
 * 场景开始
 * 定时器结束
 * Promise开始
 * Promise结束
 * 代码执行结束
 * 
 * 实际上:
 * 场景开始
 * Promise开始
 * 代码执行结束
 * Promise结束
 * 定时器结束
 */

上面注释中已经对代码块运行结果进行展示,诶?不是说好了单线程自上而下执行吗,怎么顺序跟想象中不一样?

2. JavaScript的同步和异步
由于javascript单线程的特点,意味着代码是需要排队的,轮到了才开始执行,那如果碰到计算量特别大、定时时间特别久、IO速度慢等情况,那后面的代码不久干等着了?于是便诞生了同步任务(synchronous)与异步任务(asynchronous)的机制。同步任务指在主线程上等待执行的任务;异步任务指不进入主线程,优先到隔壁的任务队列(task queue)中等待的任务。
前端面试死角—JavaScript执行机制_第1张图片
如图,主线程执行同步任务,异步任务会被放到任务队列中的Event Table中等待并注册回调函数,当达到调用函数的要求时,函数会被放到Event Queue中等待,只要主线程中的任务都已完成,队列清空,则会自动到Event Queue中按顺序将回调函数放入主线程执行。这样的执行顺序会不断循环,称之为事件循环(Event Loop) 这就是javascript的同步异步任务机制。
那主线程怎么时刻知道自己是否为空呢?js引擎存在monitoring process进程,会时刻检查主线程是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

同步:

console.log(1);
console.log(2);
console.log(3);
//结果:1 2 3

异步:

console.log(1);
setTimeout(function() {
    console.log(2);
},3000)
setTimeout(function() {
    console.log(3);
},0)
console.log(4);
//结果:1 4 3 2

这里注意,刚刚也提到,异步任务的回调函数进入Event Queue的前提是达成某种要求或完成任务,这里两个定时器虽然都是异步任务被先后放入Event Table中等待,但0毫秒比3000毫秒快,也就是说timeer2更快达到要求将回调函数放入Event Queue中,所以主线程优先调用timer2。(setTimeout(fn, 0)表示定时器在主线程任务栈清空后立刻执行并调用函数)

3. JavaScript的宏任务和微任务
上面讲完Event Loop规则,那么仅仅是这样吗?细心的朋友会发现刚第一个例子代码中,结果显示Promise比setTimeout优先执行,百思不得其解。其实在每一次的Event Loop中,还包含着宏任务和微任务的规则概念。
宏任务(macro-task):包含代码整体script、setTimeout、setInterval
微任务(micro-task):包含Promise,process.nextTick

先来看张图:
前端面试死角—JavaScript执行机制_第2张图片
原来,宏任务和微任务的回调函数会按达到要求的先后顺序进入不同的Event Queue。在我的理解,简单而言:首先是执行整体代码script(宏任务)→执行微任务队列→执行宏任务队列→执行微任务队列…
每一次的Event Loop都会严格按照这样的规则执行,这也决定了javascript的真实执行顺序。

console.log('1');
 
setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
},2000)
new Promise(function(resolve) {
    console.log('5');
    resolve();
}).then(function() {
    console.log('6')
})
 
setTimeout(function() {
    console.log('7');
    new Promise(function(resolve) {
        console.log('8');
        resolve();
    }).then(function() {
        console.log('9')
    })
})

这段代码,你的答案是什么?答案是:1 5 6 7 8 9 2 3 4。
步骤:
1、执行宏任务整体script打印出1 5;
2、执行微任务,此时微EventQueue中有Promise回调函数(P1),于是就执行P1,打印出6;
3、执行宏任务,此时宏EventQueue中存在两个setTimeout(T1和T2),因为T2的时延为0,所以优先执行T2,打印出7 8;
4、执行微任务,由于T2中存在Promise(P2),此时P2回调函数位于微EventQueue中,所以执行P2,打印出9;
5、执行宏任务,此时宏EventQueue中只剩下T1,执行T1,打印出2 3;
5、执行微任务,由于T1中存在Promise(P3),此时P3回调函数位于微EventQueue中,所以执行P3,打印出4;
6、宏EventQueue队列为空,程序执行结束。

4. 聊聊JavaScript的单线程
学到这里,不经疑问,既然这么多规则来处理“多线程”工作,为何javascript只能是单线程。原因很简单,javascript是浏览器脚本语言,作用就是实现用户操作交互和对Dom进行系列操作,这就决定了它只能单线程,毕竟如果它同时拥有多线程执行任务,那肯定会出现混乱,浏览器该执行谁?执行了不会冲突?所以为了保留单线程的特点,但又不能让代码性能被单线程耽误,于是便衍生了同步异步的解决方法,加上宏任务微任务的定义,js的执行机制便如此了。其实无非就是讲异步任务拿到任务表执行,并将函数按宏任务微任务区分丢进不同的任务队列中等待主线程的召唤,理解通透了也就不怕面试官提问多复杂的执行机制问题了。

你可能感兴趣的:(前端笔记,js,javascript,执行机制,队列,宏任务微任务)