Javascript的单线程和异步编程

运行时概念

下面的内容解释了一个理论上的模型。现代 JavaScript 引擎着重实现和优化了描述的几个语义。html

可视化描述

Stack, heap, queue

函数调用造成了一个栈帧。前端

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7));

 

当调用bar时,建立了第一个帧 ,帧中包含了bar的参数和局部变量。当bar调用foo时,第二个帧就被建立,并被压到第一个帧之上,帧中包含了foo的参数和局部变量。当foo返回时,最上层的帧就被弹出栈(剩下bar函数的调用帧 )。当bar返回的时候,栈就空了。node

对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域。git

队列

一个 JavaScript 运行时包含了一个待处理的消息队列。每个消息都有一个为了处理这个消息相关联的函数。github

事件循环时,runtime (运行时)老是从最早进入队列的一个消息开始处理队列中的消息。正因如此,这个消息就会被移出队列,并将其做为输入参数调用与之关联的函数。为了使用这个函数,调用一个函数老是会为其创造一个新的栈帧( stack frame),一如既往。ajax

函数的处理会一直进行直到执行栈再次为空;而后事件循环(event loop)将会处理队列中的下一个消息(若是还有的话)。编程

事件循环

之因此称为事件循环,是由于它常常被用于相似以下的方式来实现:api

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

若是当前没有任何消息queue.waitForMessage 会等待同步消息到达。浏览器

Javascript的单线程和异步编程

为何JavaScript是单线程?

JavaScript语言的一大特色就是单线程,也就是说,同一个时间只能作一件事。那么,为何JavaScript不能有多个线程呢?这样能提升效率啊。网络

JavaScript的单线程,与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?

因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。

单线程--异步

说到js的单线程(single threaded)和异步(asynchronous),不少同窗不由会想,这不是自相矛盾么?其实,单线程和异步确实不能同时成为一个语言的特性。js选择了成为单线程的语言,因此它自己不多是异步的,但js的宿主环境(好比浏览器,Node)是多线程的,宿主环境经过某种方式(事件驱动,下文会讲)使得js具有了异步的属性。往下看,你会发现js的机制是多么的简单高效!

浏览器--异步

js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务造成一个任务队列排队等候执行,但前端的某些任务是很是耗时的,好比网络请求,定时器和事件监听,若是让他们和别的任务同样,都老老实实的排队等待执行的话,执行效率会很是的低,甚至致使页面的假死。因此,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的。下图说明了浏览器的主要线程。
浏览器线程
图片来自popAnt 画得太好,忍不住引过来 (http://blog.csdn.net/kfanning/article/details/5768776

任务队列

刚才说到浏览器为网络请求这样的异步任务单独开了一个线程,那么问题来了,这些异步任务完成后,主线程怎么知道呢?答案就是回调函数,整个程序是事件驱动的,每一个事件都会绑定相应的回调函数,举个栗子,有段代码设置了一个定时器

setTimeout(function(){
    console.log(time is out);
},50);

执行这段代码的时候,浏览器异步执行计时操做,当50ms到了后,会触发定时事件,这个时候,就会把回调函数放到任务队列里。整个程序就是经过这样的一个个事件驱动起来的。
因此说,js是一直是单线程的,浏览器才是实现异步的那个家伙。

主线程

js一直在作一个工做,就是从任务队列里提取任务,放到主线程里执行。下面咱们来进行更深一步的理解。
event loop
图片来自Philip Roberts的演讲《Help, I'm stuck in an event-loop》很是深入!
咱们把刚才了解的概念和图中作一个对应,上文中说到的浏览器为异步任务单独开辟的线程能够统一理解为WebAPIs,上文中说到的任务队列就是callback queue,咱们所说的主线程就是有虚线组成的那一部分,堆(heap)和栈(stack)共同组成了js主线程,函数的执行就是经过进栈和出栈实现的,好比图中有一个foo()函数,主线程把它推入栈中,在执行函数体时,发现还须要执行上面的那几个函数,因此又把这几个函数推入栈中,等到函数执行完,就让函数出栈。等到stack清空时,说明一个任务已经执行完了,这时就会从callback queue中寻找下一我的任务推入栈中(这个寻找的过程,叫作event loop,由于它老是循环的查找任务队列里是否还有任务)。

几个容易困惑的问题

  1. setTimeout(f1,0)是什么鬼
    这个语句最大的疑问是,f1是否是马上执行?答案是不必定,由于要看主线程内的命令是否已经执行完了,以下代码:

    setTimeout(function(){
    console.log(1);
    },0);
    console.log(2);
    这段代码的输出结果是2,1。由于执行setTimeou后,会当即把匿名函数放到callback queue里面等待主线程的召唤,但这个时候stack里面并非空的,由于还有一句console.log(2)。等到执行完console.log(2)后,才经过event loop把匿名函数放到stack里面。因此setTimeout(f1,0)这个语句并非没有意义,若是f1是很耗时的任务,那就应该把任务放到callback queue里面,等到主程序执行完后再执行。
  2. Ajax请求是否异步
    了解完上文内容,咱们就知道了,ajax请求内容的时候是异步的,当请求完成后,会触发请求完成的事件,而后把回调函数放入callback queue,等到主线程执行该回调函数时仍是单线程的。
  3. 界面渲染线程是单独开辟的线程,是否是DOM一变化,界面就马上从新渲染?
    若是DOM一变化,界面就马上从新渲染,效率必然很低,因此浏览器的机制规定界面渲染线程和主线程是互斥的,主线程执行任务时,浏览器渲染线程处于挂起状态。

如何利用浏览器的异步机制

咱们已经知道,js一直是单线程执行的,浏览器为几个明显的耗时任务单独开辟线程解决耗时问题,可是js除了这几个明显的耗时问题外,可能咱们本身写的程序里面也会有耗时的函数,这种状况怎么处理呢?咱们确定不能本身开辟单独的线程,但咱们能够利用浏览器给咱们开放的这几个窗口,浏览器定时器线程和事件触发线程是好利用的,网络请求线程不适合咱们使用。下面咱们具体看一下:

假设耗时函数是f1,f1是f2的前置任务。

  • 利用定时器触发线程

    function f1(callback){
    setTimeout(function(){
        // f1 的代码
        callback();
    },0);
    }
    f1(f2);

    这种写法的耦合度高。

  • 利用事件触发线程

    $f1.on('custom',f2);  //这里绑定事件以jQuery写法为例
    function f1(){
    setTimeout(function(){
        // f1的代码
        $f1.trigger('custom');
    },0);
    }

    这种方法经过绑定自定义事件,对方法一解耦,这样能够经过绑定不一样的事件,实现不一样的回调函数,但若是应用这种方法过多,不利于阅读程序。

  • 发布/订阅

    上面"事件",彻底能够理解成"信号"。

    咱们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其余任务能够向信号中心"订阅"(subscribe)这个信号,从而知道何时本身能够开始执行。这就叫作"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

    这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

    首先,f2向"信号中心"jQuery订阅"done"信号。

  • jQuery.subscribe("done", f2);

    而后,f1进行以下改写:

  • function f1(){
    
        setTimeout(function () {
    
          // f1的任务代码
    
          jQuery.publish("done");
    
        }, 1000);
    
      }

    jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引起f2的执行。

    此外,f2完成执行后,也能够取消订阅(unsubscribe)。

  • jQuery.unsubscribe("done", f2);
  •  

    这种方法的性质与"事件监听"相似,可是明显优于后者。由于咱们能够经过查看"消息中心",了解存在多少信号、每一个信号有多少订阅者,从而监控程序的运行。

  • Nodejs非阻塞式IO/事件驱动IO

异步的好处和适合的场景

  1. 异步的好处
    咱们直接经过一个例子对同步和异步进行对比,假设有四个任务(编号为1,2,3,4),它们的执行时间都是10ms,其中任务2是任务3的前置任务,任务2须要20ms的响应时间。下面咱们作下对比,你就知道怎么实现的非阻塞I/O了。
    同步和异步的对比
    图片来自Soham Kaman的文章
  2. 适合的场景
    能够看出,当咱们的程序须要大量I/O操做和用户请求时,js这个具有单线程,异步,事件驱动多种气质的语言是多么应景!相比于多线程语言,它没必要耗费过多的系统开销,同时也没必要把精力用于处理多线程管理,相比于同步执行的语言,宿主环境的异步和事件驱动机制又让它实现了非阻塞I/O,因此你应该知道它适合什么样的场景了吧!

参考:

https://www.cnblogs.com/woodyblog/p/6061671.html

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

相关文章
相关标签/搜索