乐趣区

关于javascript:面试-JS-事件循环-event-loop-经典面试题含答案

原文地址

掘金

欢送 github star

思维导图

一、JS 异步编程基本概念

JS 之所以是单线程的是因为浏览器 (多线程) 只调配一个线程来执行 JS 代码,之所以只调配一个线程试因为浏览器思考到多线程操作会导致的一些问题,假如 JS 是多线程的,其中一个线程在 DOM 节点上增加内容,而另一个线程在这个节点上删除内容,那么浏览器该执行哪一个呢?所以 JS 的设计就是单线程的。然而单线程会造成很多的工作都须要期待执行,所以就引入了浏览器的事件循环机制。

过程和线程 Tip

过程中能够包含多个线程,比方关上一个页面,这个页面就占用了计算机的一个过程,页面加载时,浏览会调配一个线程去计算 DOM 树,一个去执行 JS 代码,其余的线程去加载资源文件等。

二、event loop

JS 主线程一直的周而复始的从工作队列中读取工作,执行工作,这种运行机制称为事件循环(event loop)。举荐看一个 2 分钟理解 event loop

宏工作和微工作

浏览器的事件循环(event loop)中分成宏工作和微工作。JS 中工作分成同步工作和异步工作。

1. 宏工作(macro task)

JS 中主栈执行的大多数的工作,例如:定时器,事件绑定,ajax,回调函数,node 中 fs 操作模块等就是宏工作

2. 微工作(micro task)

promise, async/await, process.nextTick等就是微工作。

思考:为什么要引入微工作,只有宏工作能够吗?

微工作的引入是为了解决异步回调的问题,假如只有宏工作,那么每一个宏工作执行完后回调函数也放入宏工作队列,这样会造成队列多长,回调的工夫变长,这样会造成页面的卡顿,所以引入了微工作。

思考,为什么 await 前面的代码会进入到 promise 队列中的微工作?

async/await 只是操作 promise 的语法糖,最初的实质还是 promise。举一个小栗子

async function async1() {console.log('async1 start');
    await async2();
    console.log('async1 end');
}
// 下面的代码等价于 ==>
async function async1() {console.log('async1 start');
    Promise.resolve(async2()).then(() => {console.log('async1 end')
    })
}

4. 宏工作和微工作的执行程序(很重要)

  1. 主栈队列就是一个宏工作,每一个宏工作执行完就会执行宏工作中的微工作,直到微工作全副都执行完,才开始执行下一个宏工作。
  2. __JS 中工作的执行程序优先级是:主栈全局工作 (宏工作) > 宏工作中的微工作 > 下一个宏工作。__,所以 promise(微工作) 的执行程序优先级高于setTimeout 定时器。
  3. 不能满目标将 .then 的回调放入微工作队列;因为没有调用 resolve 或者 reject 之前是不算异步工作实现的,所以不能将回调随便的放入微工作事件队列
  4. await 是一个让出线程的标记。await 前面的表达式会先执行一遍,将 await 前面的代码退出到 micro task中这个微工作是 promise 队列中微工作,而后就会跳出整个 async 函数来继续执行前面的代码。
  5. process.nextTick 是一个独立于 eventLoop 的工作队列,主栈中的宏工作每一次完结后都是先执行 process.nextTick队列,在执行微工作 promise.then()
  6. 每一个宏工作和宏工作的微工作执行完后都会对页面 UI 进行渲染。

热身 1 先看一个小栗子

// A 工作
setTimeout(() => {console.log(1)
}, 20)

// B 工作
setTimeout(() => {console.log(2)
}, 0)

// C 工作
setTimeout(() => {console.log(3)
}, 10)

// D
setTimeout(() => {console.log(5)
}, 10)

console.log(4)
/* 输入
*   4 -> 2-> 3 -> 5 -> 1
*/

在主线程的主工作(宏工作)先自上而下执行,遇到 setTimeout 代码都是下一个(宏工作)。所以都会被退出到期待队列中,浏览器有专门监听期待队列中的代码,在主栈中的同步代码执行实现后,期待队列中的工作先到执行工夫的就先执行,如果期待工作队列中有两个同时到执行工夫的异步代码,那么先入队列的就先到主栈中执行。所以期待队列中 B 工作执行后到 C 工作到 D 工作 再到 A 工作。输入的后果就是4 -> 2-> 3 -> 1

热身 2 将下面的栗子改一下

// A 工作
setTimeout(() => {console.log(1)
}, 20)

// B 工作
setTimeout(() => {console.log(2)
}, 0)

// C 工作
setTimeout(() => {console.log(3)
}, 30)

console.log(4)
/* 输入
*   4 -> 2-> 1 -> 3
*/

这题的原理和下面一样,工作 A 的执行工夫比 C 工作先到了就先输入了 1 后输入 3。

三、思考题求输入程序(chrome 浏览器为准)

1. 来一道思考题,求输入后果

let xhr = new XMLHttpRequest()
xhr.open('post', 'api')
xhr.onreadystatechange = () =>{if(xhr.readyState == 2){console.log(2)
    }
    if(xhr.readyState == 4){console.log(4)
    }
}
xhr.send()
console.log(3)
/* 输入
*   3 2 4
*/

2. 再来一道思考题,在同步申请中上面代码输入的是什么

let xhr = new XMLHttpRequest()
xhr.open('get', 'xxx', false)
xhr.send()

xhr.onreadystatechange = () => {console.log(xhr.readyState)
}

没有输入,下面的两道题在 面试 | Ajax,fetch,axios 的超高频面试题 有解析。

3. 一道 Ajax 异步思考题,求输入后果。

let xhr = new XMLHttpRequest()
xhr.open('post', 'api')
xhr.onreadystatechange = () =>{console.log(xhr.readyState)
}
xhr.send()
/* 输入
*   2 -> 3 -> 4。*/

xhr.onreadystatechange 是异步的会退出到期待队列,主工作执行 xhr.send() 后 ajax 的状态码变成 1。主工作闲暇期待工作中的 xhr.onreadystatechange 开始监听到状态码变动,晓得状态码由2 -> 3 -> 4 后不再变动。如果不相熟 ajax 状态码的能够看看 面试 | Ajax,fetch,axios 的超高频面试题。

4. promise 热身题,求输入后果

console.log(1)
new Promise((resolve, reject) => {console.log(2)
    resolve()}).then(res => {console.log(3)
})
console.log(4)
/* 输入
* 1 -> 2 -> 4 ->3 
*/

解答:第一轮宏工作就是主栈中的同步工作,先输入 1,JS 代码执行到promise 立刻执行输入 2resolve.then() 中的代码放入到微工作队列,宏工作完结后输入 4,最初执行微工作队列输入3

4. setTimeout 和 Promise 的执行程序

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

new Promise(function (resolve, reject) {console.log(2);
    resolve();}).then(function () {console.log(3)
}).then(function () {console.log(4)
});

console.log(6);
// 2, 6, 3, 4, 1

解答:先开始主栈中的宏工作,遇到 setTimeout 后丢入宏工作队列期待,遇到 promise 立刻执行输入 2resolve() 异步的丢入微工作队列,最初输入6,第一个宏工作执行完结开始留下来的微工作,即 .then() 输入 3, 4。第一轮循环完结开始下一轮宏工作 setTimeout,输入1

5. setTimeout 和 Promise 的执行程序

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

new Promise(function (resolve, reject) {console.log(2)
    for (var i = 0; i < 10000; i++) {if (i === 10) {console.log(10)
        }
        i == 9999 && resolve();}
    console.log(3)
}).then(function () {console.log(4)
})
console.log(5);
// 2, 10, 3, 5, 4, 1

这道题的解法和下面雷同,都须要辨别宏工作和微工作。

6. 求输入后果

console.log("start");
setTimeout(() => {console.log("children2")
    Promise.resolve().then(() =>{console.log("children3")
    })
}, 0)

new Promise(function(resolve, reject){console.log("children4")
    setTimeout(function(){console.log("children5")
        resolve("children6")
    }, 0)
}).then(res =>{         // flag
    console.log("children7")
    setTimeout(() =>{console.log(res)
    }, 0)
})
// start children4 children2 children3  children5  children7 children6
  1. 首先开始主工作中的第一轮宏工作,输入 start,遇到 setTimeout 不须要期待 0s 而是间接丢入宏工作队列(有人说须要期待 0s 再放入到工作队列是不对的,能够应用console.time/timeEnd 来测试),遇到 promise 立刻执行输入 children4,又遇到一个setTimeout 间接又丢入到宏工作队列,第一轮宏工作执行完,且没有微工作。问:下面的 .then() (正文的flag 处) 是第一轮宏工作循环的微工作吗?不是!因为 resolve 都没有执行,promise 的状态都还没有从pending 扭转,就不是第一轮的微工作。
  2. 开始下一轮的宏工作执行第一个进入的 setTimeout,输入children2, 第二轮宏工作完结,开始微工作执行promise 中的.then() 输入 children3。第二轮循环完结
  3. 接着又开始setTimeout 的宏工作,输入children5,微工作输入 children7。这里遇到一个宏工作 setTimeout,丢入宏工作队列。
  4. 又开始新 setTimeout 宏工作,输入 res children6

7. (头条)请写出上面代码的运行后果(不同的环境下输入有差别,上面以最新的 Chromium 为准)

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((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

这道题的难点在于是 promise2还是 async1 end 先输入。从全局宏工作之上而下执行时 await async2() 前面的代码 console.log('async1 end') 先进入 promise 中的微工作队列,最初.then() 中的console.log('promise2') 再进入到 promise 中的微工作队列。所以再开始下一轮宏工作循环之前先输入了 async1 end 再输入了 promise2。全局中的微工作执行实现开始下一轮宏工作setTimeout 最初输入 setTimeout

8. 将上一道题目变换一下,求输入(不同的环境下输入有差别,上面以最新的 Chromium 为准)

async function async1() {console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function 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

首先开始全局下的宏工作顺次输入 script start, async1 start, promise1, promise3, script end。其中 await async2();async2().then() 的代码先进入到 promise 的微工作队列,await async2(); 前面的代码再进入到 promise 的工作队列,console.log('promise4'); 最初进入到 promise 的工作队列。全局下的宏工作完结,开始全局下的微工作,promise 的微工作队列中依照队列的先进先出准则顺次输入,promise2,async1 end,promise4。全局微工作完结,开始下一轮的宏工作setTimeout,最终输入 setTimeout

9. 再来将下面的题目变换一下,求输入(不同的环境下输入有差别,上面以最新的 Chromium 为准)

async function async1() {console.log('async1 start');
    await async2();
    setTimeout(function() {console.log('setTimeout1')  // 这一部分代码会放入到 promise 的微工作队列中。},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

依照下面的解析,原理都是一样的,全局下的宏工作执行实现后,开始执行全局下的微工作 .then() 中的代码,最初开始下一轮宏工作的执行,下一轮宏工作是 setTimeout3 先执行,因为是setTimeout3 先退出下一个宏工作队列中的,再顺次退出setTimeout2, setTimeout1 到宏工作队列。所以输入的后果是setTimeout3, setTimeout2, setTimeout1

参考

《Javascript 忍者秘籍》第二版,事件循环篇

第 10 题:常见异步口试题,请写出代码的运行后果

完结

js 异步队列的题目就先到这里,如果感觉不过瘾的能够看看这篇文章的 [面试 | JS 你不得不懂的 异步编程 | promise 篇超高频面试题]() 面试题

退出移动版