当前位置: 首页 > article >正文

JS 异步 ( 一、异步概念、Web worker 基本使用 )


文章目录

  • 异步
    • 代码异步执行概念
    • ES6 之前的异步
  • Web worker

异步

代码异步执行概念

通常代码是自上而下同步执行的,既后面的代码必须等待前面的代码执行完才会执行,而异步执行则是将主线程中的某段代码交由子线程去执行,当交给子线程后,主线程就会继续执行后面代码,而不用等待子线程执行完成,异步是程序语言并行执行的一种手段,通常将耗时的任务交由子线程同时处理,从而提升整体任务耗时。

不严谨的对比一下单线程同步和多线程异步的效率提升(不考虑 CPU 核数和时间片切换问题):

请添加图片描述

ES6 之前的异步


在 ES6 的 Web worker 出现之前,Javascript 确实也可以异步开发,但是要知道的是,那时的 Javascript 是单线程的,之所以能够使用多线程实现异步,其实是依靠 <浏览器内核结构> 的多线程,而不是 JS 本身具备多线程特性。

1. 浏览器内核结构

请添加图片描述

浏览器是多个进程共同配合工作的,所谓浏览器内核指的就是其中的渲染进程,进程主要结构如上图,
渲染进程中又有如下几个重要的线程(这些线程并不是JS的,而是浏览器渲染进程的):

线程功能
GUI 渲染线程 (渲染引擎)解析 HTML 和 CSS,从而构建 DOM
JS 执行线程 (JS 引擎)负责执行 Javascript 代码,内含 <任务队列> 和 <事件循环> 两个重要模块
事件触发线程 (DOM 监听)当事件发生后,将事件的回调函数添加到 JS 引擎的 <任务队列>
定时器线程 (Timer 监听)setTimeout、setInterval 等的计时,达到时间后,将计时器的回调函数添加到 JS 引擎的 <任务队列>
XmlHttpRequest 线程 (AJAX)XmlHttpRequest 的监听,当 XmlHttpRequest 对象状态变化时,将 Ajax 回调函数添加到 JS 引擎的 <任务队列>

GUI 线程和 JS 执行线程不能同时执行,遇见 Javascript 代码时,JS 执行引擎运行优先级更高

2. JS 执行线程的运行机制及与其他线程的搭配

请添加图片描述

(1) 主线程按顺序执行代码,当碰见 setTimeoutsetIntervalXmlHttpRequestDOM 事件 等 Api 时,会将其交给
对应的定时器线程、XmlHttpRequest 线程、事件线程处理,然后主线程继续执行后面的代码

(2) 定时器线程、XmlHttpRequest 线程、事件线程的任务触发后,会将回调函数添加至 JS 线程的任务队列中

(3) 主线程内的任务全部执行完成后,会调用事件循环器拉取任务队列中的任务到主线程,至此一个事件循环周期结束

3. 分析 setTimeout 等计时不准的问题

<script>
  (function async(){
    console.log('主线程执行1')
    setTimeout(function(){
      console.log('setTimeout 计时结束')
    }, 3000)
    console.log('主线程执行2')
    while(true){
      
    }
  }())
  // 输出:
  // 主线程执行1
  // 主线程执行2
</script>

预想结果是 3 秒后输出 <setTimeout 计时结束>,但在实际结果中,setTimeout 即使达到时间也没有执行,现在明
白了 <JS 执行线程的运行机制及与其他线程的搭配> 的原理,就可以解释这个现象了,因为主线程中有 while(true)
而主线程执行不完,就不会执行事件循环器拉取任务队列中 setTimeout 的回调函数,所以 setTimeout 的回调一直没有被调用


4. 微观队列

在 ES6 之后,<JS 执行线程的运行机制> 的整体流程有一点变化,事件循环器除了调用 <任务队列> 以外, 又多了
一个队列,为了区分,原队列改称为 <宏队列>,新队列称为 <微队列> ,<微队列> 中比较典型的就是状态为 fulfilled
或 rejected 的 Promise 对象的处理函数。在一次事件循环中优先执行 <微队列> 中的任务。

请添加图片描述

举例 - 事件循环器优先执行 <微队列> 中的任务:

<script>
  console.log("主线程任务1")
  // 定时器
  setTimeout(()=>{
    console.log("延迟零毫秒的定时任务1")
  },0)
  // 状态为 fulfilled 的 Promise
  Promise.resolve().then(()=>{
    console.log("状态为 fulfilled 的 Promise")
  })
  // 定时器
  setTimeout(()=>{
    console.log("延迟零毫秒的定时任务2")
  },0)
  console.log("主线程任务2")
  // 输出:
  // 主线程任务1
  // 主线程任务2
  // 状态为 fulfilled 的 Promise
  // 延迟零毫秒的定时任务1
  // 延迟零毫秒的定时任务2
</script>

复杂一点的举例(事件循环每次只获取一个任务):

<script>
  console.log("主线程任务1")
  // 定时器
  setTimeout(()=>{
    console.log("延迟零毫秒的定时任务1")
  },0)
  // 定时器
  setTimeout(()=>{
    // 状态为 fulfilled 的 Promise
    Promise.resolve().then(()=>{
      console.log("状态为 fulfilled 的 Promise 1")
    })
  },0)
  // 状态为 fulfilled 的 Promise
  Promise.resolve().then(()=>{
    console.log("状态为 fulfilled 的 Promise 2")
  })
  // 定时器
  setTimeout(()=>{
    console.log("延迟零毫秒的定时任务2")
  },0)
  console.log("主线程任务2")
  // 输出:
  // 主线程任务1
  // 主线程任务2
  // 状态为 fulfilled 的 Promise 2
  // 延迟零毫秒的定时任务1
  // 状态为 fulfilled 的 Promise 1
  // 延迟零毫秒的定时任务2
</script>

Web worker


ES6 以前,JS 是单线程的,所谓异步也都是依赖于浏览器内核的多线程机制,而不是 JS 本身具有多线程特性,这就导致能支持的异步操作很少(定时器线程,事件线程,Ajax线程),ES6 以后新增的 Web worker 功能让 JS 真正的拥有了多线程特性,但是 Web work 创建的子线程有一些使用限制。

限制描述
同源限制子线程的 JS 脚本,必须和主线程的脚本文件同源
DOM限制不能对页面元素操作,包括不能使用弹出框等,可以理解为 JS 子线程无法使用渲染进程的渲染引擎

1. 基本语法

主线程脚本

<script>
  // 用来创建并启动一个 JS 子线程,参数为 JS 脚本文件 URL
  const work = new Worker("./worker.js")
  // 给子线程发送数据,参数任何类型都可以
  work.postMessage('发送给子线程的消息')
  // 监听子线程是否有消息返回,event 为事件对象,event.data 可以获取子线程返回的数据
  work.onmessage = (event)=>{
    console.log('接收到的子线程处理结果:' + event.data)
    // 关闭子线程
    work.terminate();
  }
</script>

子线程脚本文件 worker.js

console.log('子线程启动')
// 监听主线程是否有消息发送过来,event 为事件对象,event.data 可以获取主线程发送的数据
addEventListener('message', (event) => {
  console.log('子线程接到消息:' + event.data)
  // 向主线程发送数据
  postMessage('处理完成!')
  // 关闭子线程自身(和 terminate 功能一样,防止主线程调用后忘记关闭,所以此处也写一份 )
  close()
})

2. 同一文件内使用 Web worker

先说思路,主线程和子线程要分别写在不同的 script 标签对儿中,然后主线程读取子线程标签对儿中的内容,并将其创建成 Blob 类型(BlobFile 类型的父类,所以 Blob 也可以简单理解为文件类型),然后对该文件对象(Blob)生成 url,最后 worker 访问该 url

再说需要注意的东西:
(1) 子线程的 script 脚本要写在主线程 script 脚本之前,防止主线程中读取不到子线程的 script 标签
(2) 子线程的 script 标签的 type 属性,要给一个 type 规定的合法值以外的值(本人喜欢给 web-worker),如果是
合法值,就会被 JS 线程 ( JS 引擎 ) 识别,然后会直接运行其内容,而我们预想的执行时机是主线程调用后执行

<!-- 子线程脚本 -->
<script id="worker" type="web-worker">
  console.log('子线程启动')
  // 监听主线程是否有消息发送过来,event 为事件对象,event.data 可以获取主线程发送的数据
  addEventListener('message', (event) => {
    console.log('子线程接到消息:' + event.data)
    // 向主线程发送数据
    postMessage('处理完成!')
    // 关闭子线程自身(和 terminate 功能一样,防止主线程调用后忘记关闭,所以此处也写一份 )
    close()
  })
</script>
<!-- 主线程脚本 -->
<script>
  // 读取子线程脚本内容, 将其转换成二进制类型(Blob 是 File 类型的父类,所以 Blob 也可以理解为类文件类型)
  var blob = new Blob([document.querySelector("#worker").textContent]);
  // 针对 blob 文件生成 URL 
  var url = window.URL.createObjectURL(blob);
  // 用来创建并启动一个 JS 子线程,参数为生成的 URL
  const work = new Worker(url)
  // 给子线程发送数据,参数任何类型都可以
  work.postMessage('发送给子线程的消息')
  // 监听子线程是否有消息返回,event 为事件对象,event.data 可以获取子线程返回的数据
  work.onmessage = (event)=>{
    console.log('接收到的子线程处理结果:' + event.data)
    // 关闭子线程
    work.terminate();
  }
</script>

http://www.kler.cn/a/453828.html

相关文章:

  • 学习记录:配置mybatisplus的分页查询插件,mybatis-plus-jsqlparser 依赖
  • 7.桶排+计数+基数
  • AI驱动的网络安全运维:智能化时代的安全保障
  • uni-app开发订单列表页面
  • 设计模式之【观察者模式】
  • vulnhub靶场-matrix-breakout-2-morpheus攻略(截止至获取shell)
  • 【WebAR-图像跟踪】在Unity中基于Imagine WebAR实现AR图像识别
  • HUB、交换机、路由器和串口服务器
  • 广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
  • taro中实现带有途径点的路径规划
  • 使用Webpack构建微前端应用
  • Java前端基础——CSS
  • 靶机系列|VULNHUB|DC-1
  • Vue.js组件开发-使用vue-pdf显示PDF
  • RK3568平台开发系列讲解(中断及异常篇)Linux 中断系统中的重要数据结构
  • v语言介绍
  • deepin 安装 kafka
  • 传统网络架构与SDN架构对比
  • Qt笔记:网络编程UDP
  • 智慧交通-Android车牌识别接口-车牌识别系统