JS 异步编程

教程简介

作者: TigerChain
地址: https://www.jianshu.com/p/876e68fd6a1c
本文出自 TigerChain 简书 手把手教 Vue 系列

本节大纲

img

正文

js 是一门单线程、非阻塞、异步、并发语言。

一、同步和异步

1. 同步

同步指的是任务是一个接一个的去完成,上一个任务没有完成,下一个任务就不能开始,单线程和多线程都可以实现同步,但是单线程一定是同步的「一个线程只有执行完前面的任务,才能执行后面的」

img

2. 异步

异步是一个相对概念,多线程是异步的前提,一个线程是玩不了异步的

img

比如 Java 语言,声明一个 Thread 看起来只有一个线程,但是调用 start() 方法却异步执行了,请看图

img

执行结果

img

从结果来看 java 默认是有一个主线程的「main 线程,上面的 thrad 异步就是相对于 main 而言的」,所以根本不可能一个线程就能完成异步

那么到底 js 是如何实现异步的呢?说异步我们不得不说以下几个角色
JavaScript Engine、Web APIs、Message queue、Event loop

接下来一一介绍,首先登场的是 JS 引擎:

二、JavaScript Engine

JS 引擎有的也称为 JS 虚拟机,主要是负责解析和执行 js 的,它是浏览器所实现的,不同的浏览器有不同的实现方式「采用 c/c++ 实现」,这里以 V8 引擎为例来说明「其它的引擎都大同小异」

来看看引擎的简易图

img

由图可知,JS 引擎主要包括两个组件就是堆和栈

堆: 用就是用来分配内存的地方

栈: 也叫 调用栈/执行栈 就是方法调用和执行的地方「js 是单线程说的就是 call stack 」

这里顺便说一下,浏览器有渲染引擎和 js 引擎,浏览器是从上向下解析 html 标签的,当遇到 script 标签「js 代码」时会立即停止解析,直接执行 js 脚本,所以渲染引擎和 js 引擎是互斥的「js 操作 DOM 的会影响渲染」,这一个过程是同步的,所以加载一个耗时的 js 会导致界面卡死的根本原因就在这里。

来看看 stack 的执行机制

先来一段代码

img

这段代码本身没有什么好说的,非常简单的代码,我们看看 js 引擎在执行这段代码的时候 call stack 中的执行过程

img

call stack 由名子可以看出它是一个栈结构「那肯定遵循先进后出原则」,当一个方法调用的时候就入栈,执行完成以后就出栈

再来一段暴力代码

img

以上代码是一段暴力代码,就是一个死循环,我们来看看结果

img

由结果可行,call stack 栈大小被撑爆了,其实可以想像,不停的调用 hello 方法,入栈、入栈 … 入栈,肯定最后就放不下了

栈有多大?

由上面的死循环代码我们就可以尝试着算出 call stack 的大小参考 2ality.com

img

当然对不同的浏览器结果是不一样的,引擎实现的方式不一样,我测试在 chorome 如下「不同浏览器大家可自行测试一下」

img

show-cal-call-stack-size-result

我们清楚了当调用一个方法的时候 js 引擎会把方法压入 call stack ,当方法执行完毕以后出栈

三、Web APIs

由于 js 引擎中的 stack 同一时间只能干一件事情「单线程」,那么 call stack 肯定是玩不了异步,可是虽然 js 是单线程的,但浏览器却是多线程的,我们知道 js 有好多 API 有些不是核心 js 语言的一部分,比如 BOM DOM AJAX setTimeOut Canvas WegGl 等 api 浏览器可以在调用之外执行这些 api 「另起一个或多个线程跑这些 api」

img

这些 api 就可以独立于调用栈来执行自己的功能,但是有一个问题是如果这些 api 执行完以后该怎么办呢?有两种方案

  • 我们将 web api 完成的方法直接推送到调用栈
  • 我们采取一些机制来保存这些响应,在合适的时候推送给调用栈

第 1 种方法显然不靠谱,如果 web api 执行完以后直接把结果给调用栈可以会影响正在执行的调用栈,所以浏览器采用第二种方法,使用消息队列来保存这些 web api 执行的响应以便在调用栈可以调用的时候推送给调用栈,这个保存消息的东西就是接下来我们要说的 Message Queue

四、Message Queue

Message Queue「消息队列也叫 Callback Queue」是用来保存 Web Api 调用完成以后的所有消息的回调函数,当调用栈「call stack」为空时「也就是调用栈中的方法执行完毕以后」Message Queue 中的回调方法「先进先出」会被添加到调用栈中去执行,但是浏览器是什么方式来把调用栈和 message Queue 联系起来的「什么机制把 Message Queue 中的回调方法给 call stack 当 call statck 为空的时候」,它就是 Event Loop

img

五、Event Loop

Event Loop 是把 call stack 和 Message Queue 联系起来的纽带和桥梁,Event Loop 是一个基于事件的并发模型,它时刻在监听着消息队列,如果有完成的消息它此刻还要关心 call stack 是否为空,如果为空则把 Messag Queue 中的回调结果推送给 call statck 回调方法执行

Event Loop 做两件事情

  • 监听 Message Queue「是否有消息」
  • 监听 call statck 「看是否为空,如果为空则推送结果」

img

这样就完成了 js 的非阻塞异步调用

六、代码来分析异步调用过程

1. 写一段如下代码

img

非常简单的一段代码,体现了 js 的异步过程

2. 分析过程

img

上图显示了上述代码的执行过程,简单的说一下吧,上述代码分为十个步骤

  • 当代码执行 console.log(‘大家好!’)的时候此方法入栈,这个没有什么好说的,前面说过了
  • 方法继续向下执行遇到 setTimeout()方法,这是一个 webapi 方法然后交给 3 去执行
  • 浏览器单独开一个线程去执行,然后调用栈不停止继续向下执行
  • 调用栈执行 console.log(‘欢迎关注’) 方法
  • web api 执行 setTimeout 方法完毕,指导结果给 Message Queue ,此是 webpai 就变成空的「图上没有体现,希望明白」
  • 此时调用栈中执行完 console.log(‘欢迎关注’) 以后此方法出栈,栈此时变成空的,Event Loop 监听着 Message Queue
  • Event loop 把 Message Queue 中的方法取出来,推给空的调用栈,此时 callback 入栈「调用栈」
  • 执行其中的方法体 console.log(‘TigerChain’)
  • 打印了 TigerChain 此方法出栈
  • 到此 callback 执行完毕,callback 出栈,调用栈变为空

以上过程只是一个针对简单代码的一个简单的分析,如果存在多个异步操作,则 Event Loop 不停的执行取出消息推入栈的操作直到完成

七、总结

到此我们把 js 非阻塞和异步的原理大概说了一下,相信大家应该有一个简单的知识和了解,大概总结一下

  • js 是非阻塞异步的单线程「单线程指的就是 call stack」
  • js 实现异步的方式是基于 Event Loop 的并发模型
  • 浏览器的 web api 不是 js 核心的部分,但是和 call stack 不冲突执行「浏览器另外开线程去执行」
  • web api 的执行结果不能直接给 call stack 先要通过 Message Queu 把结果存起来,等待 Event Loop 去处理
  • Event Loop 如果发现 call statck 为空时「此时就是推入 Message Queue 中的消息的最佳时机」取出消息队列中的消息推入给调用栈,异步结束

我们来看一张非常形象的 Event Loop 并发模型的图

img

图片来源 The Main Event… Loop 建议把这张图印在心里、印在心、印在心里「重要的事情说三遍」

js 的异步执行过程从上图非常形象的展现出来,从 statck 开始顺时针执行,遇到 webpai 方法让其去调用并把结果给 Message Queue , Event Loop 查看 stack 为空则取出 Message Queue 中的方法给 statck 搞懂此图就彻底了解了 Event Loop 的并发模型了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章