终于搞懂了JS的事件循环、同步/异步、宏任务/微任务、运行机制(附笔试题)

513 阅读6分钟
JS是单线程语言
进程,即资源分配的最小单位,拥有独立的堆栈空间和数据存储空间
线程,即程序执行的最小单位,一个进程可以包括多个线程
如果多进程,同时操作DOM,那么后果就不可控了
例如:对于同一个按钮,不同的进程赋予了不同的颜色,到底该怎么展示
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务(synchronous)
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务(asynchronous)
异步任务指的是,不进入主线程,而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
事件循环(Event Loop)
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
宏任务(macrotask)
当前调用栈中执行的代码成为宏任务。
包括:script 中代码、setTimeoutsetInterval、I/O、UI render
微任务(microtask)
当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件
包括:process.nextTick、MutationObserver、Promise.then 
运行机制
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
Promise
Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的

new Promise(function(resolve) {
    console.log('promise');  // 此处立即执行
    resolve();
}).then(res=>{
    console.log('rosolve')  // 加入到微任务(microtask中)中
})
Async/Await
  而在async/await中,在await出现之前,其中的代码也是立即执行的。
  很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。
  await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
  因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask
  
async function async1() {
	console.log('async1 start'); //立即执行
	await async2(); //立即执行await后面的表达式
	console.log('async1 end'); //将await后面的代码加入到微任务(microtask中)中
}
题一
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

async function async2() {
	console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');
/*
    script start
    async1 start
    async2
    promise1
    script end
    async1 end
    promise2
    setTimeout
*/

解析:
    1.事件循环从宏任务(macrotask)队列开始,宏任务队列中,只有一个script(整体代码)任务,执行整个代码块
    2.遇到2个定义的async函数,继续往下走,遇到console语句,直接输出 'script start'
    3.script任务继续向下执行,遇到async1()函数,async函数中在await之前的代码是立即执行的,所以输出 'async1 start',然后执行async2()函数,输出 'async2',将 ‘console.log('async1 end')’分配到microtask队列中
    4.script任务继续往下执行,遇到PromisePromise中的代码是被当做同步任务立即执行的,所以输出 'promise1',然后执行 resolve,将 'promise2' 分配到microtask队列中
    5.script任务继续往下执行,最后只有一句输出了'script end',至此,全局任务就执行完毕了
    6.每次执行完一个宏任务之后,会去检查是否存在 microtask;如果有,则执行 microtask 直至清空 microtask Queue。因此在script任务执行完毕之后,开始查找清空微任务队列
    7.microtask队列中,Promise 队列中有两个任务async1 end和promise2,因此依次输出async1 end 、promise2,所有的 microtask 执行完毕之后,表示第一轮的循环就结束了
    8.第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束
题二
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');

/*
    script start
    async1 start
    promise1
    promise3
    script end
    promise2
    async1 end
    promise4
    setTimeout
*/
解析:
    和题一不同的是async2()函数内部也写成了PromisePromise中的代码是被当做同步任务立即执行的,所有会输出 'promise1''promise2'则被分配到microtask队列中,因此输出完 'script end'后,会相继输出 promise2, async1 end ,promise4,setTimeout
题三
sync function async1() {
    console.log('async1 start');
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改如下:
	setTimeout(function() {
		console.log('setTimeout2')
	},0)
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

/*
    script start
    async1 start
    promise1
    script end
    promise2
    setTimeout3
    setTimeout2
    setTimeout1
*/
解析:
    在输出为promise2之后,接下来会按照加入setTimeout队列的顺序来依次输出,通过代码我们可以看到加入顺序为3 2 1,所以会按321的顺序来输出。
题四
async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')

/*
    script start
    a1 start
    a2
    promise2
    script end
    promise1
    a1 end
    promise2.then
    promise3
    setTimeout
*/
解析:
    1.  Promise.resolve().then(() => {
          console.log("promise1");
        });
        a1();
    
    'promise1'也是被分配到microtask队列中的,而且是在a1()函数执行前,先分配的,所以在 'script end'输出后,会先输出 'promise1' 然后在依次输出
    
    2.  let promise2 = new Promise((resolve) => {
          resolve("promise2.then");
          console.log("promise2");
        });
    这里会直接输出"promise2", 'resolve'不同于'await',不会阻止后面的执行
    
参考文章