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
由于是自执行,自动捕捉异常。
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/await
与之前写的 co
之间的区别。
参考资料
JavaScript Visualized: Event Loop
浏览器和 Node.js 的 EventLoop 为什么这么设计?