关于事件循环我所知道的

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

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

目录
相关文章
|
存储 编解码 Windows
AVI 格式的历史和演变
AVI 格式的主要功能使其成为存储和播放多媒体内容的多功能且实用的选择。 它对多种编解码器、各种分辨率和广泛兼容性的支持使 AVI 格式成为视频爱好者和专业人士的可靠选择。
399 0
|
Linux Docker 容器
.net Core WebApi发布到Docker并推送到阿里云容器服务
.net Core WebApi发布到Docker并推送到阿里云容器服务
1005 0
.net Core WebApi发布到Docker并推送到阿里云容器服务
|
9月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
7月前
|
存储 缓存 NoSQL
阿里云服务器实例规格选型:八大业务场景精准选型参考
阿里云提供了丰富多样的云服务器实例规格,以满足不同用户的多样化需求。然而,面对琳琅满目的实例选项,许多初次接触云服务器的用户往往感到无所适从。本文旨在帮助用户根据自身的业务场景和细分需求,精准选择最适合的阿里云服务器实例规格,确保资源的有效利用和成本的合理控制。
|
设计模式 中间件 程序员
【实战指南】深入了解23种设计模式
《深入了解23种设计模式:程序员必读指南》旨在帮助程序员理解和应用设计模式,以解决常见编程问题。书中介绍了设计模式的起源、目的及其在提高代码复用性、质量和团队沟通中的作用。涵盖创建型、结构型和行为型三大类共23种设计模式,每种模式均附有详细解析与C++实现示例,适合初学者和有经验的开发者学习参考。
288 92
|
12月前
|
机器学习/深度学习 自然语言处理 语音技术
Python在深度学习领域的应用,重点讲解了神经网络的基础概念、基本结构、训练过程及优化技巧
本文介绍了Python在深度学习领域的应用,重点讲解了神经网络的基础概念、基本结构、训练过程及优化技巧,并通过TensorFlow和PyTorch等库展示了实现神经网络的具体示例,涵盖图像识别、语音识别等多个应用场景。
409 8
|
存储 人工智能 Serverless
妙用AI助理帮您定方案、找细节
当您希望在繁琐的文档中迷失方向时,AI助理能为您提供清晰指引,助您轻松实现加速配置与获取核心代码参数,显著简化开发流程。无论是方案获取还是寻找细节,只需向AI助理提问,即可获得详细步骤与示例代码,大幅提升工作效率。点击右下角的AI助理,即刻体验便捷服务。
447 1
|
Python
经验大分享:Python函数返回值
经验大分享:Python函数返回值
299 0
|
Java Go Python
golang调用python实战
# 简介 ## go-python Python提供了丰富的[C-API](https://docs.python.org/2/c-api/)。而C和Go又可以通过cgo无缝集成。所以,直接通过Golang调用libpython,就可以实现Go调Python的功能了。但是过程比较复杂,而[go-python](https://github.com/sbinet/go-python)提供
3937 0
golang调用python实战
|
Web App开发 移动开发 HTML5
HTML中input标签的23种type类型
HTML中input标签的23种type类型