关于事件循环我所知道的

简介: 关于事件循环我所知道的

image.png


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

image.png


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

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

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


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


image.png

执行机制:

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


延伸问题收集


原生 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 由于是自执行,自动捕捉异常。


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/await 与之前写的 co 之间的区别。

image.png

image.png


参考资料


JavaScript Visualized: Event Loop

面试官:说说你对JavaScript中事件循环的理解

浏览器和 Node.js 的 EventLoop 为什么这么设计?

原生 Promise 和手写 Promise 的区别是什么?

js 异步历史与 co.js 解读 作者:chentao

目录
相关文章
|
7月前
|
存储 Java API
ntyco协程的理解
ntyco协程的理解
86 0
|
2月前
|
JavaScript 数据库
事件循环
【10月更文挑战第28天】
38 3
|
4月前
|
存储 Linux 调度
协程(coroutine)的原理和使用
协程(coroutine)的原理和使用
|
1月前
|
存储 JavaScript 前端开发
事件循环的原理是什么
事件循环是一种编程机制,用于在单线程环境中处理多个任务。它通过维护一个任务队列,按顺序执行每个任务,并在任务之间切换,从而实现并发处理。在每个循环中,事件循环检查是否有新的任务加入队列,并执行就绪的任务。
|
14天前
|
前端开发 JavaScript API
前端:事件循环/异步
前端开发中的事件循环和异步处理是核心机制,用于管理任务执行、性能优化及响应用户操作,确保网页流畅运行。事件循环负责调度任务,而异步则通过回调、Promise等实现非阻塞操作。
|
4月前
|
存储 前端开发 JavaScript
事件循环机制是什么
【8月更文挑战第3天】事件循环机制是什么
38 1
|
7月前
|
前端开发 编译器 Linux
浅谈C++20 协程那点事儿
本文是 C++20 的协程入门文章,作者围绕协程的概念到协程的实现思路全方位进行讲解,努力让本文成为全网最好理解的「C++20 协程」原理解析文章。
|
数据采集 缓存 调度
协程小练习
协程小练习
|
7月前
|
前端开发 JavaScript UED