关于事件循环我所知道的

127 阅读8分钟

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

640.gif

整段代码的执行过程可以分为四个部分:

  1. 执行栈
  2. WEB API
  3. 任务队列
  4. EventLoop 连接任务队列与执行栈

EventLoop 在其中的作用是:当执行栈中的任务全部执行完,检查任务队列中是否存在等待执行的任务,如果存在则取出队列中的第一个任务压入栈中。

微任务和宏任务用于区分执行的优先级

未命名文件.jpg

执行机制:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
  • 执行下一个宏任务

延伸问题收集

原生 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) {
        // ...
      })
    })
  })
})

项目和交互的复杂造成了回调会产生以下问题:

  1. 代码的简化和封装对开发人员的水平较高
  2. 通过封装抽象出模块和通用的类来保证代码是浅层的,在此过程中容易产生bug

由此产生了 promise 和 类promise(jquery 中的 deferred) 的方案。

promise

promisecallback 变成了一种扁平化的结构。

// 改造之前的代码
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'
})

相比 deferredpromise 由于是自执行,自动捕捉异常。

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");
  });

问题是

  1. return 1 时,控制台打印的顺序
  2. return Promise.resolve();时,控制台打印的顺序。

答案1:then1 → then1-1 → then2 → then1-2 → then3

按照第一个结论

  1. 第一次 resolve 后第一个 then 的回调进入微任务队列并执行,打印 then1
  2. 第二次 resolve 后内部第一个 then 的回调进入微任务队列,此时外部第一个 then 的回调全部执行完毕,需要将外部的第二个 then 回调也插入微任务队列。
  3. 执行微任务,打印 then1-1 和 then2,然后分别再将之后 then 中的回调插入微任务队列
  4. 执行微任务,打印 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(),再对代码进行优化。实现方法的自动执行。

  1. 自执行函数中 onFulfilled 调用 next
  2. next 调用 onFulfilled
  3. 形成自执行器,只有当代码全部执行完毕后才会终止
// 封装函数自动执行 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')

分析过程:

  1. 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

参考资料