试图探寻JavaScript的异步设计

阅读本文,你将知道:html

  1. 同步和异步
  2. 阻塞和非阻塞
  3. JavaScript的异步实现方式
  4. 进一步深刻理解为何“JavaScript是异步事件驱动的单线程编程语言”

前言

在最初学习JavaScript的时候,就从各个地方得知JavaScript是一门单线程编程语言,可是使人疑惑的是,为何一个单线程语言可以同时执行HTTP请求同时渲染页面?为何代码的书写顺序和执行顺序并不一致?web

这就是异步带来的效果ajax

这里咱们必须达到以下的共识:编程

  • 浏览器是多线程的,常驻线程有:
    • 浏览器 GUI 渲染线程
    • JavaScript 引擎线程
    • 浏览器定时触发器线程
    • 浏览器事件触发线程
    • 浏览器 http 异步请求线程
  • JavaScript是单线程的

这里须要注意的是GUI渲染进程和JavaScript引擎进程是互斥的,由于若是这两个线程能够同时运行的话,JavaScript的DOM操做将会扰乱渲染线程执行渲染先后的数据一致性。浏览器

本文想要探讨的,是JavaScript线程里的异步设计,千万别和多线程混淆了。网络

想要了解更多关于浏览器多线程机制请参考:www.cnblogs.com/hksac/p/659…多线程

开始

一切得先从CPU开始讲起:异步

CPU的指令执行速度是远高于硬盘读取速度和主存读取速度的。而I/O操做就会涉及到硬盘存取和主存读取,常见的I/O操做有文件I/O,网络I/O。(I/O = Input / Output)。编程语言

因此,观察如下这一段伪代码:函数

var a = 2;

for(let i =0;i<10;i++){
    doSomeWork();
}

let buffer = openFile('./work.txt')

buffer.add('hello world');

复制代码

在CPU眼中,他会把代码当作这两部分:

绿色部分由于不涉及到I/O操做,因此CPU执行速度超快,可是当运行到红色部分时,倒是一个很是耗时的操做,而这段时间,CPU是处于一个'无所事事'的状态(DMA获取总线控制权以后一切I/O与CPU无关),由于文件若是没有读取进来,下面的工做也没法开展。

同步在这里的意思,即书写代码的顺序就是代码执行的顺序,若是JavaScript设计成同步的话,那么当执行到openFile这一行的时候,将会等待该I/O操做完成CPU才继续往下执行。

设想一下,当发送Ajax请求(网络I/O)的时候,整个页面被阻塞没法操做将会是多差的体验。

而诸如鼠标点击事件,滑动事件,失焦事件,在CPU看来,都是处理得特别慢的事件(虽然对咱们来讲是一瞬间的事情),若是将JavaScript设计成同步,也会特别浪费CPU性能。

阻塞和非阻塞关注的CPU在I/O发生时的工做状况

在上面这个读取文件的例子中

  • 若是在读取文件的同时,该线程被‘挂起’(能够理解为进程的阻塞态),CPU不在关注这个线程直到结果被返回,属于阻塞式
  • 若是在读取文件的同时,CPU会时不时关注并检查一遍结果是否返回,则属于非阻塞式

若是没法区分同步阻塞,请参考这里

异步则解决了代码被耗时任务阻止其往下执行的缺点

多线程异步有着比较好的解决方案:

  • 给涉及到I/O操做的部分新开一个线程执行
  • 主线程不等待继续往下执行
  • I/O线程执行完以后将结果写回公共区并通知主线程(也能够是主线程去轮询)
  • 主线程执行其回调

可是

JavaScript是一门单线程语言,自己没法提供多线程,那么是经过怎样的机制来实现异步的?

先给出答案:JavaScript经过事件循环和浏览器各线程协调共同实现异步

JavaScript认为任务分为两种,一种是全由CPU决定完成速度的任务,咱们称其为同步任务,一种是由多种因素(如硬盘读取速度,网速,点击反馈速度)决定完成速度的任务,咱们称其为异步任务。

举个简单的例子

  • 函数声明,for循环,变量声明,赋值操做等均可以属于同步任务
  • 读取文件,网络请求,网页事件都看作异步任务

JavaScript将全部的异步任务都会放进一个队列里面,在执行完全部的同步任务以后,会去队列中找到最早进入队列的异步任务执行。

仔细观察上图,结合本文在最开始提到的浏览器多线程设计:

  • JavaScript线程首先执行同步任务
  • 在执行完同步任务以后,会去异步任务队列的队头取出任务执行
  • 浏览器各个线程会在事件触发且完成事件以后将回调函数写入异步队列(先进先出队列)

由于诸如事件触发,http请求都是耗时没法直接肯定的任务,也就是说JavaScript线程没法得知异步的任务回调函数究竟何时会写入异步任务队列,那么这个地方,就须要一个机制,去时刻轮询这个任务队列,这就是事件循环(event loop)

如今咱们再看以下代码的执行顺序:

var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();
复制代码

真正的执行顺序是

var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();    // i am here
    req.onload = function (){};    
    req.onerror = function (){};    
复制代码

同理

while(1){
console.log('1')
}

setTimeOut(()=>{
	console.log('00000000000000000')
},1)
复制代码

也将一样永远不会输出00000000000000000

讨论下为何这样设计

由于JavaScript的工做环境是一个典型的异步应用场景:充斥着各类ajax事件和浏览器事件。各个事件的触发时间和获得反馈的时间都不得而知,若是设计成同步语言,将会带来极差的浏览器使用体验。 须要设计一个成一个生产者-消费者模型(也能够看作是观察者模式),来管理这样的异步任务。

浏览器须要作的事情太多了,一手须要负责渲染,一手须要负责http请求,一手还须要执行JavaScript,将JavaScript设计成单线程不只可以让浏览器更好地控制各个线程,同时对开发者来讲也更简单。多线程涉及到锁,临界区,冲突解决的学习成本仍是比较高的。

总结

再次来看一下这一句话:

JavaScript是异步事件驱动的单线程编程语言

异步:写代码顺序不必定是执行顺序,JavaScript线程先执行同步任务。 事件驱动:其余线程在各事件完成后将回调函数写入队列,都是以抽象事件做为触发机制的。 单线程:不能开多线程而是用eventloop来实现异步的。

JavaScript经过事件循环和浏览器各线程协调共同实现异步

JavaScript的异步设计很是优秀,这也让基于V8引擎的Node在服务端大方异彩,可以更加简单地开发出适合高密集I/O的web应用。

以后会基于JavaScript的EventLoop总结下关于Node的异步I/O(涉及到多线程)

若是喜欢,请关注

最新博客会最早更新在http://www.helloyzy.cn

相关文章
相关标签/搜索