文章目录
- 异步
- 代码异步执行概念
- 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) 主线程按顺序执行代码,当碰见 setTimeout
、setInterval
、XmlHttpRequest
、DOM 事件
等 Api 时,会将其交给
对应的定时器线程、XmlHttpRequest 线程、事件线程处理,然后主线程继续执行后面的代码
(2) 定时器线程、XmlHttpRequest 线程、事件线程的任务触发后,会将回调函数添加至 JS 线程的任务队列中
(3) 主线程内的任务全部执行完成后,会调用事件循环器拉取任务队列中的任务到主线程,至此一个事件循环周期结束
3. 分析 setTimeout 等计时不准的问题
<script>javascript">(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>javascript">console.log("主线程任务1")// 定时器setTimeout(()=>{console.log("延迟零毫秒的定时任务1")},0)// 状态为 fulfilled 的 PromisePromise.resolve().then(()=>{console.log("状态为 fulfilled 的 Promise")})// 定时器setTimeout(()=>{console.log("延迟零毫秒的定时任务2")},0)console.log("主线程任务2")// 输出:// 主线程任务1// 主线程任务2// 状态为 fulfilled 的 Promise// 延迟零毫秒的定时任务1// 延迟零毫秒的定时任务2
</script>
复杂一点的举例(事件循环每次只获取一个任务):
<script>javascript">console.log("主线程任务1")// 定时器setTimeout(()=>{console.log("延迟零毫秒的定时任务1")},0)// 定时器setTimeout(()=>{// 状态为 fulfilled 的 PromisePromise.resolve().then(()=>{console.log("状态为 fulfilled 的 Promise 1")})},0)// 状态为 fulfilled 的 PromisePromise.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>javascript">// 用来创建并启动一个 JS 子线程,参数为 JS 脚本文件 URLconst work = new Worker("./worker.js")// 给子线程发送数据,参数任何类型都可以work.postMessage('发送给子线程的消息')// 监听子线程是否有消息返回,event 为事件对象,event.data 可以获取子线程返回的数据work.onmessage = (event)=>{console.log('接收到的子线程处理结果:' + event.data)// 关闭子线程work.terminate();}
</script>
子线程脚本文件 worker.js
javascript">console.log('子线程启动')
// 监听主线程是否有消息发送过来,event 为事件对象,event.data 可以获取主线程发送的数据
addEventListener('message', (event) => {console.log('子线程接到消息:' + event.data)// 向主线程发送数据postMessage('处理完成!')// 关闭子线程自身(和 terminate 功能一样,防止主线程调用后忘记关闭,所以此处也写一份 )close()
})
2. 同一文件内使用 Web worker
先说思路,主线程和子线程要分别写在不同的 script
标签对儿中,然后主线程读取子线程标签对儿中的内容,并将其创建成 Blob 类型(Blob
是 File
类型的父类,所以 Blob
也可以简单理解为文件类型),然后对该文件对象(Blob
)生成 url,最后 worker
访问该 url
再说需要注意的东西:
(1) 子线程的 script
脚本要写在主线程 script
脚本之前,防止主线程中读取不到子线程的 script
标签
(2) 子线程的 script
标签的 type
属性,要给一个 type
规定的合法值以外的值(本人喜欢给 web-worker),如果是
合法值,就会被 JS 线程 ( JS 引擎 ) 识别,然后会直接运行其内容,而我们预想的执行时机是主线程调用后执行
<!-- 子线程脚本 -->
<script id="worker" type="web-worker">javascript">console.log('子线程启动')// 监听主线程是否有消息发送过来,event 为事件对象,event.data 可以获取主线程发送的数据addEventListener('message', (event) => {console.log('子线程接到消息:' + event.data)// 向主线程发送数据postMessage('处理完成!')// 关闭子线程自身(和 terminate 功能一样,防止主线程调用后忘记关闭,所以此处也写一份 )close()})
</script>
<!-- 主线程脚本 -->
<script>javascript">// 读取子线程脚本内容, 将其转换成二进制类型(Blob 是 File 类型的父类,所以 Blob 也可以理解为类文件类型)var blob = new Blob([document.querySelector("#worker").textContent]);// 针对 blob 文件生成 URL var url = window.URL.createObjectURL(blob);// 用来创建并启动一个 JS 子线程,参数为生成的 URLconst work = new Worker(url)// 给子线程发送数据,参数任何类型都可以work.postMessage('发送给子线程的消息')// 监听子线程是否有消息返回,event 为事件对象,event.data 可以获取子线程返回的数据work.onmessage = (event)=>{console.log('接收到的子线程处理结果:' + event.data)// 关闭子线程work.terminate();}
</script>