EventLoop 事件循环 面试
EventLoop 解决什么问题?
JS 是单线程、只有一个调用栈的程序语言。通过 浏览器/Node 的 EventLoop 实现异步,解决运行 JS 代码运行阻塞问题。
注意:EventLoop 是浏览器/Node 提供的能力
浏览器 EventLoop
可以将事件循环理解为是一层调度逻辑,作用是等到执行栈清空就将任务队列中的任务压入栈中。
像定时器、网络请求这类异步任务就是在别的线程执行完之后,通过任务队列通知主线程进行最后的回调处理。
举个例子:
const foo = () => console.log('First')
const bar = () => setTimeout(()=>console.log('Second'), 500)
const baz = () => console.log('Third');
bar()
foo()
baz()
整段代码的执行过程可以分为四个部分:
- 执行栈
- WEB API
- 任务队列
- EventLoop 连接任务队列与执行栈
EventLoop 在其中的作用是:当执行栈中的任务全部执行完,检查任务队列中是否存在等待执行的任务,如果存在则取出队列中的第一个任务压入栈中。
微任务和宏任务用于区分执行的优先级
执行机制:
- 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
- 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
- 执行下一个宏任务
延伸问题收集
原生 Promise 和手写 Promise 的区别是什么? 引用自 Cat Chen 的回答
早前浏览器中 polyfill 的
Promise无法使用 microtask,只能使用 task 来模拟。原生
Promise.resolve(callback)和setTimeout(callback, 0)一起调用,无论如何Promise.resolve的回调函数都会先执行。浏览器 polyfill 出来的
Promise则是哪一个先被调用,哪一个的回调函数就会先执行。Node polyfill 出来的
Promise是两者按任意顺序一起执行,Promise的回调函数都会先触发。原因是 Node 很早就有
process.nextTick(callback)这个东西,和后来才出现的queueMicrotask几乎一样。
JS 异步历史整理
摘自:js 异步历史与 co.js 解读 作者:chentao
- callback
- deferred(promise)
- generator + promise
- async/await
callback
如果一个函数无法立即返回 value,而是经过一段不可预测的行为时间之后(副作用)才能得到 value。就得通过 callback 获得 value。
function sideEffect () {
const value = 1
setTimeout(() => {
return value
})
}
console.log(sideEffect()) // undefined
function sideEffect (callback) {
const value = 1
setTimeout(() => {
// ...
callback(value)
})
}
sideEffect(value => {
console.log(value) // 1
})
延伸出来 callback 回调地狱。
getData(function (a) {
getMoreData(a, function (b) {
getMoreData(b, function (c) {
getMoreData(c, function (d) {
// ...
})
})
})
})
项目和交互的复杂造成了回调会产生以下问题:
- 代码的简化和封装对开发人员的水平较高
- 通过封装抽象出模块和通用的类来保证代码是浅层的,在此过程中容易产生bug
由此产生了 promise 和 类promise(jquery 中的 deferred) 的方案。
promise
promise 将 callback 变成了一种扁平化的结构。
// 改造之前的代码
getData()
.then(getMoreData)
.then(getMoreData)
.then(getMoreData)
.then(function (d) {
// ...
})
为什么选择 promise?
// deferred
deferred.promise.then(v => console.log(v))
setTimeout(() => {
deferred.resolve('sign')
}, 500)
// promise
const p = new Promise(resolve => {
setTimeout(() => {
resolve('sign')
}, 500)
})
p.then(v => console.log(v))
deferred 不会捕捉到错误,必须用 try catch 然后通过 deferred.reject 触发。
deferred.promise.catch(reason => console.log(reason))
setTimeout(() => {
throw 'error'
})
相比 deferred ,promise 由于是自执行,自动捕捉异常。
Promise 总结
第一个结论
链式调用中,只有前一个 then 的回调执行完毕后,跟着的 then 中的回调才会被加入至微任务队列。
虽然 then 是同步执行,并且状态也已经变更。
但这并不代表每次遇到 then 时我们都需要把它的回调丢入微任务队列中,而是等待 then 的回调执行完毕后再根据情况执行对应操作。
// 第一个结论
Promise.resolve()
.then(() => {
console.log("then1");
Promise.resolve().then(() => {
console.log("then1-1");
});
})
.then(() => {
console.log("then2");
});
// then1 → then1-1 → then2
第二个结论
同一个 Promise 的每个链式调用的开端会首先依次进入微任务队列。
let p = Promise.resolve();
p.then(() => {
console.log("then1");
Promise.resolve().then(() => {
console.log("then1-1");
});
}).then(() => {
console.log("then1-2");
});
p.then(() => {
console.log("then2");
});
// then1 → then2 → then1-1 → then1-2
let p = Promise.resolve().then(() => {
console.log("then1");
Promise.resolve().then(() => {
console.log("then1-1");
});
}).then(() => {
console.log("then2");
});
p.then(() => {
console.log("then3");
});
// then1 → then1-1 → then2 → then3
// then 每次都会返回一个新的 Promise
// 此时的 p 已经不是 Promise.resolve() 生成的,而是最后一个 then 生成的
// 因此 then3 应该是在 then2 后打印出来的。
面试题
Promise.resolve()
.then(() => {
console.log("then1");
Promise.resolve()
.then(() => {
console.log("then1-1");
return 1
// return Promise.resolve();
})
.then(() => {
console.log("then1-2");
});
})
.then(() => {
console.log("then2");
})
.then(() => {
console.log("then3");
})
.then(() => {
console.log("then4");
});
问题是
return 1时,控制台打印的顺序return Promise.resolve();时,控制台打印的顺序。
答案1:then1 → then1-1 → then2 → then1-2 → then3
按照第一个结论
- 第一次 resolve 后第一个 then 的回调进入微任务队列并执行,打印 then1
- 第二次 resolve 后内部第一个 then 的回调进入微任务队列,此时外部第一个 then 的回调全部执行完毕,需要将外部的第二个 then 回调也插入微任务队列。
- 执行微任务,打印 then1-1 和 then2,然后分别再将之后 then 中的回调插入微任务队列
- 执行微任务,打印 then1-2 和 then3 ,之后的内容就不一一说明了
答案2:then1 → then1-1 → then2 → then3 → then1-2
当 Promise resolve 了一个 Promise 时,会产生一个 NewPromiseResolveThenableJob,这是属于 Promise Jobs 中的一种,也就是微任务.并且该 Jobs 还会调用一次 then 函数来 resolve Promise,这也就又生成了一次微任务
generator + promise
promise 链式调用的语法还是不够同步。
const getData = () => {
return new Promise(resolve => resolve(1))
}
const getMoreData = value => {
return value + 1
}
getData()
.then(getMoreData)
.then(getMoreData)
.then(getMoreData)
.then(value => {
console.log(value) // 4
})
// 通过 generator 改造后
const gen = (function* () {
const a = yield 1
const b = yield a + 1
const c = yield b + 1
const d = yield c + 1
return d
})()
const a = gen.next()
const b = gen.next(a.value)
const c = gen.next(b.value)
const d = gen.next(c.value)
console.log(d.value) // 4
但是需要手动调用 gen.next(),再对代码进行优化。实现方法的自动执行。
- 自执行函数中
onFulfilled调用next。 next调用onFulfilled- 形成自执行器,只有当代码全部执行完毕后才会终止
// 封装函数自动执行 gen.next()
function co (fn, ...args) {
return new Promise((resolve, reject) =>{
const gen = fn(...args);
function next (result) {
// part3
}
function onFulfilled (res) {
// part1
}
function onRejected (err) {
// part2
}
onFulfilled()
})
}
/**
* part1
* 自动调用 gen.next()
* 然后调用 next() 将结果传入到 generator 对象内部
*/
function onFulfilled (res) {
let result
try {
result = gen.next(res)
next(result)
} catch (err) {
return reject(err)
}
}
/**
* part2
* 发生错误调用 gen.throw()
* 这可以让 generator 函数内部的 try/catch 捕获到
*/
function onRejected (res) {
let result
try {
result = gen.throw(err)
next(result)
} catch (err) {
return reject(err)
}
}
/**
* part3
* 接受到结果后再次调用 onFulfilled
* 继续执行 generator 内部的代码
*/
function next (result) {
let value = result.value
if (result.done) return resolve(value)
// 如果是 generator 函数,等待整个 generator 函数执行完毕
if (
value && value.constructor &&
value.constructor.name === 'GeneratorFunction'
) {
value = co(value)
}
// 转为 promise
Promise.resolve(value).then(onFulfilled, onRejected)
}
测试代码:
const ret = co(function * () {
const a = yield 1
const b = yield a + 1
const c = yield b + 1
const d = yield c + 1
return d
})
ret.then(v => console.log(v)) // 4
const fn = v => {
return new Promise(resolve => {
setTimeout(() => resolve(v), 200)
})
}
// 结合 promise
const ret = co(function * () {
const a = yield fn(1)
console.log(a) // 1
const b = yield fn(a + 1)
console.log(b) // 2
const c = yield fn(b + 1)
console.log(c) // 3
const d = yield fn(c + 1)
console.log(d) // 4
return d
})
ret.then(v => console.log(v)) // 4
// error 的处理 错误都能被捕捉
const ret = co(function * () {
try {
throw 'errorOne'
} catch (err) {
console.log(err) // errorOne
throw 'errorTwo'
}
})
ret.catch(err => console.log(err)) // errorTwo
async/await
Generator 函数的语法糖。为了使异步操作变得更方便。async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
相对于 Generator 的改进主要集中集中在:
- 内置执行器
- 更好的语义化
- Promise 的返回值
本质也是 Promise 的语法糖:Async 函数返回了 Promise 对象。下面两种方法是等效的:
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
正常情况下,await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值,不管await后面跟着的是什么,await都会阻塞后面的代码。
async function f() {
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
// await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码
async function fn1() {
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2() {
console.log('fn2')
}
fn1()
console.log(3)
// 1,fn2,3,2
面试题
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')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
分析过程:
- 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
- 遇到定时器了,它是宏任务,先放着不执行
- 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
- 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
- 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
- 继续执行下一个微任务,即执行 then 的回调,打印 promise2
- 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout