04-事件循环相关知识点

127 阅读5分钟

同步和异步的区别

  • 同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作
  • 异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容

异步编程的实现方式

  • 回调函数

    • 优点:简单、容易理解
    • 缺点:不利于维护,代码耦合高
  • 事件监听(采用时间驱动模式,取决于某个事件是否发生):

    • 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
    • 缺点:事件驱动型,流程不够清晰
  • 发布/订阅(观察者模式)

    • 类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅
  • Promise对象

    • 优点:可以利用then方法,进行链式写法;可以书写错误时的回调函数;

    • 缺点:编写和理解,相对比较难

    • Promise 的关键点在于callback 的两个参数,一个是 resovle,一个是 reject。还有就是 Promise 的链式调用(Promise.then(),每一个 then 都是一个责任人)

    • 可以把 Promise看成一个状态机。初始是 pending 状态,可以通过函数 resolve 和 reject,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化

    • 简单来说它就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息

    • 待定(pending):初始状态,既没有被完成,也没有被拒绝。

    • 已完成(fulfilled):操作成功完成。

    • 已拒绝(rejected):操作失败。

    • Promise.all(iterable) 将多个请求合并到一起

    • 简化版的Promise

image.png 创建 myPromise 实例时,用户会传入一个 constructor 回调,这个回调会立即执行。 resolvereject 会根据 constructor 回调的逻辑被调用,改变 Promise 的状态。 用户通过 then 方法传入回调函数

-   `constructor(resolve, reject)` 是用户定义的控制逻辑,用于告诉 `Promise` 什么时候成功或失败。
-   `onFullfilled(self.value)` 是在 `Promise` 成功时,执行用户定义的成功回调。
  • Generator函数

    • 优点:函数体内外的数据交换、错误处理机制
    • 缺点:流程管理不方便
  • async函数

    • 优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
    • 缺点:错误处理机制

什么是单线程,和异步的关系

  • 单线程 - 只有一个线程,只能做一件事

  • 原因 - 避免 DOM 渲染的冲突

    • 浏览器需要渲染 DOM
    • JS 可以修改 DOM 结构
    • JS 执行的时候,浏览器 DOM 渲染会暂停
    • 两段 JS 也不能同时执行(都修改 DOM 就冲突了)
    • webworker 支持多线程,但是不能访问 DOM
  • 解决方案 - 异步

事件循环EventLoop

  • js是单线程的,主要的任务是处理用户的交互

  • 用户的交互无非就是响应DOM的增删改,使用事件队列的形式,一次事件循环只处理一个事件响应,使得脚本执行相对连续,所以有了事件队列,用来储存待执行的事件

  • 那么事件队列的事件从哪里被push进来的呢?

  • 那就是另外一个线程叫事件触发线程做的事情了,他的作用主要是在定时触发器线程、异步HTTP请求线程满足特定条件下的回调函数push到事件队列中,等待js引擎空闲的时候去执行

  • 当然js引擎执行过程中有优先级之分,首先js引擎在一次事件循环中,会先执行js线程的主任务,然后会去查找是否有微任务microtask(promise),如果有那就优先执行微任务,如果没有,在去查找宏任务macrotask(setTimeout、setInterval)进行执行

  • 微任务

    • process.nextTick
    • Promise 构造函数中的代码是同步的,then 是微任务 等价于 await xx 之后的代码
    • Object.observe
    • MutationObserver
  • 宏任务

    • script
    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • UI rendering

一次 Event loop 顺序

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

setTimeout(function () { console.log("1"); }, 0);

async function async1() {
  console.log("2");
  const data = await async2();
  console.log("3");
  return data;
}

async function async2() {
  return new Promise((resolve) => {
    console.log("4");
    resolve("async2的结果");
  }).then((data) => {
    console.log("5");
    return data;
  });
}

async1().then((data) => {
  console.log("6");
  console.log(data);
});

new Promise(function (resolve) {
  console.log("7");
  // resolve(); // 调用 resolve,状态变为 fulfilled
}).then(function () {
  console.log("8");
});

### **事件循环中的任务顺序**
  1. 同步任务(立即执行):

    • console.log("2")(在 async1 中)
    • console.log("4")(在 async2 中)
    • console.log("7")(在 Promise 构造函数中)
  2. 微任务队列(按添加顺序执行):

    • console.log("5")async2then
    • console.log("3")async1await 后的代码)
    • console.log("6")console.log("async2的结果")async1.then 的回调)
  3. 宏任务队列(下一轮事件循环执行):

    • console.log("1")setTimeout 回调)

结果: 2 4 7 5 3 6 async2的结果 1

注意:由于没有调用 resolvethen 中的回调永远不会执行,因此不会打印 8

因为 resolve 没有被调用,Promise 的状态永远是 pending,不会变成 fulfilled 或 rejected。 .then 方法, 只有当 Promise 状态变为 fulfilled 时,then 中的回调才会执行

事件的各个阶段

  • 默认是冒泡
  • 1:捕获阶段 ---> 2:目标阶段 ---> 3:冒泡阶段
  • document ---> target ----> document
  • 由此,addEventListener的第三个参数设置为true和false的区别已经非常清晰了
    • true表示该元素在事件的“捕获阶段”(由外往内传递时)响应事件
    • false表示该元素在事件的“冒泡阶段”(由内向外传递时)响应事件
    • 阻止冒泡:在W3c中,使用stopPropagation()方法
    • 阻止捕获:阻止事件的默认行为,使用preventDefault()方法