能异步的JavaScript到底是不是单线程的?

2,259 阅读5分钟

答案当然肯定是单线程的啊!

但是JavaScript为什么要做成单线程的呢?

又是谁在处理setTimeout,setInterval,Promise等异步任务呢?

这些问题如果您已了然于胸,不妨继续读下去,看看我的理解和您的理解是否契合。

这些问题如果您还不清楚,可以跟着一起了解一下。

1. JavaScript为什么要做成单线程的呢?

关于这个问题,需要先了解一些前置知识。

1.1 什么是线程,进程又是什么?

进程(英语:process),是指计算机中已运行的程序。 ——维基百科

现在在阅读文章的你所使用的客户端就为你打开了一个进程[1]

线程(英语:thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。 ——维基百科

当你在上下滑动的时候就是线程[2]在运作。

如果你使用的是浏览器,则现在打开的每个Tab页都拥有自己的线程(IE除外,一个Tab页崩了就全崩了)。当你关闭浏览器的时候,进程则被杀死,又因为线程依附于进程而存在,则每个Tab也会随之关闭。

对于进程与线程阮一峰老师有更生动的例子

图片源自知乎
图片源自知乎

1.2 现在想一想为什么JavaScript是单线程的。

在阮一峰老师的例子中可以了解到,代表线程的工人可以去同一个房间,也就是不同线程使用同一块共享内存时,其他线程必须等它结束,才能使用这一块内存。

这个问题类比到浏览器中就是多个线程同时操作同一个DOM元素,这时就会产生问题,浏览器不知道该如何去渲染这个DOM元素,因此JavaScript被设计成了单线程。

为什么不能通过互斥锁[3]去解决这个问题呢?

简单回答就是为了避免复杂性。更细致的回答在这。

1.3 Web Workers API使JavaScript变成了多线程吗?

通过使用Web Workers,Web应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是UI线程)不会因此被阻塞/放慢。 ——MDN

这是MDN中对Web Workers API的介绍,这段介绍了提到了后台线程,主线程+后台线程不就是多线程了吗?其实不然,Web Workers API的使用有诸多限制:

  • 同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

  • DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。

  • 通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

  • 脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

  • 文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

在这些限制下,它的线程就不那么像一个线程了,所以Web Workers API并没有改变JavaScript单线程的本质。

2.谁在处理setTimeout,setInterval,Promise等异步任务呢?

先明确下什么是同步和异步:

一般而言,操作分为:发出调用和得到结果两步。发出调用,立即得到结果是为同步。发出调用,但无法立即得到结果,需要额外的操作才能得到预期的结果是为异步。同步就是调用之后一直等待,直到返回结果。异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务)。

看下这张图:

主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"事件队列"中加入各种事件(click,load,done)。图中callback queue,其实是event queue。图中的event queue应该有多个,不同的事件会有不同的event queue,当我们没有使用定时器时,则完全不用关心定时器事件这个队列。在线程执行到setTimeout,setInterval,Promise等时会设置对应的watcher,事件循环的过程中,会去调用该watcher,检查它的事件队列上是否产生事件(这个事件就是设置的回调函数),检查完所有watcher后,进入下一轮检查。

上面描述了Event Loop的过程,其中说到的检查对setTimeout,setInterval来说检查的时间,对Promise来说检查的是请求(也不一定是请求)是否返回,也就是有没有从pending变成reslove/reject状态。

总结

关于线程与异步举个例子,帮助大家理解一下。

早上闹钟叫起床的之后,往面包机里放两片面包烤上,然后去洗漱,洗漱完了也就可以吃面包了。

这个例子里人就是一个线程,闹钟就相当于setInterval,时间到了就必须起床,然后烤面包就是一个异步请求,当启动面包机的时候就是设置了一个watcher,之后就是执行同步事件—洗漱,等面包烤好了就开始执行回调—吃面包,当然也可以不设置回调,我只是烤着玩的,没打算吃。

小试题

    setTimeout(()=>{console.log(1)},0)

    new Promise(function (resolve) {
      setTimeout(()=>{resolve()},0)
    }).then(function () {
      console.log(2);
    });
    setTimeout(function () {
      console.log(" set1");
      new Promise(function (resolve) {
        resolve();
      }).then(function () {
        new Promise(function (resolve) {
          resolve();
        }).then(function () {
          console.log("then4");
        });
        console.log("then2 ");
      });
    });

    new Promise(function (resolve) {
      console.log("pr1");
      resolve();
    }).then(function () {
      console.log("then1");
    });

    setTimeout(function () {
      console.log("set2");
    });

    console.log(2);

    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then3");
    });

相关文章

    async function async1() {
        console.log( 'async1 start' )
        await async2()
        console.log( 'async1 end' )
    }
    
    async function async2() {
        console.log( 'async2' )
    }
    
    console.log( 'script start' )
    
    setTimeout( function () {
        console.log( 'setTimeout' )
    }, 0 )
    
    async1();
    
    new Promise( function ( resolve ) {
        console.log( 'promise1' )
        resolve();
    } ).then( function () {
        console.log( 'promise2' )
    } )
    
    console.log( 'script end' )

相关文章

参考资料

[1]

进程: https://zh.wikipedia.org/zh-cn/%E8%A1%8C%E7%A8%8B#Windows%E8%BF%9B%E7%A8%8B

[2]

线程: https://zh.wikipedia.org/zh-cn/%E7%BA%BF%E7%A8%8B

[3]

互斥锁: http://zh.wikipedia.org/wiki/%E4%BA%92%E6%96%A5%E9%94%81

本文使用 mdnice 排版