JavaScript异步编程

229 阅读8分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

Js是单线程的,天生异步

  • 线程是最小的执行单元,进程是最小的资源管理单元

  • 线程从属于进程,在软件运行的过程里面

  • Js是单线程语言,浏览器只会分配一个主线程给js,用来执行任务(函数),但是一次只能执行一个任务

  • 所有的同步任务都是在主线程上执行,形成一个执行栈

  • 主线程之外,还存在一个任务队列,只要存在异步任务,就会在任务队列里放置一个事件

  • 一旦执行栈里面同步任务带执行完毕,主线程就会去读取“任务队列”,看任务队列有哪些对应的异步任务,结束等待状态,进入执行栈,开始执行。

  • 主线程不断重复上面三个步骤

     主线程执行完毕以后,从事件队列中去读取任务队列的过程,我们称之为事件循环(Event Loop

Js设计成单线程原因

  • JS的单线程与它的用途有关
  • JS的主要作用:完成与用户的交互,以及DOM操作加入是多线程:一个线程在一个DOM上添加了内容,另外一个线程有删除了这个DOM节点。为避免复杂性,从一开始,JS就是单线程,在未来也不会改变
  • H5,web worker标准,它是允许JS创建多个线程,但是,子线程是完全受主线程控制的,而且子线程是不可以操作DOM的

同步任务与异步任务

  • 单线程:所有的任务需要排队,前一个任务结束了以后,才能执行下一个任务。

  • 如果前一个任务耗时很长,发起一个请求,网络很慢,后面的那个任务就需要一直等着。

  • IO的时候,(输入输出的时候),主线程不去管IO,挂起处于等待中的任务,先运行排在后面的任务,等IO设备返回了结果,再回过头来,把挂起等待的任务继续执行下去,于是所有的任务分为两种:同步任务,异步任务

  • 同步任务指的是:在主线程上排队执行的任务,只有前一个任务执行完毕以后,才能够去执行下一个任务。

  • 异步任务(类比做饭):不进入主线程,而是进入“任务队列”,只有任务队列通知主线程,某个异步任务可以执行了,这个任务才会进入主线程执行。

  • 两种最大的区别就是等

    (1)同步会阻塞后面的代码。如下永远不会执行console.log(1)。

    //永远不会执行console.log(1)
    while (true) {}
    console.log(1)
    

    (2)异步任务:所有的代码都在主线程执行,形成一个执行栈。主线程以外还存在一个任务队列(task queue),只要有了异步代码,就在“任务队列”放置一个事件,一旦执行栈所有的同步任务执行完毕以后,系统就会读取任务队列,对应的任务就会结束等待的状态,进入执行栈开始执行。

  • 执行顺序

    (1)最先执行的是:同步代码,执行完毕以后,立即出栈,让出主线程

    (2)同步代码执行完毕,立即出栈,此时主线程处于:空闲状态

    (3)主线程去读取任务队列,队列遵循的原则是先进先出,但是有个条件,触发条件相等,会遵循先进先出,如果触发条件不相同,则优先执行达到触发条件的代码,如settimeout中的第二个参数0秒,不是等待0秒执行,而是将其放到任务队列中,主线程一有空就立即执行(尽快执行)。

浏览器进程

  • js中的异步代码:定时器,ajax,响应事件

js是单线程,但浏览器是多线程,浏览器开设了其他的线程,去辅助js线程的运行。渲染引擎线程、javascript引擎线程、浏览器事件线程是是常驻线程,会常驻与浏览器内存中。

    js引擎线程称为主线程:它用于运行js代码,但其不包含异步代码,即运行同步代码。

    js异步代码一般交由浏览器事件线程、HTTP异步线程、定时器触发线程来执行。

image.png

调用栈

js引擎线程即主线程在执行同步代码的过程中,会生成一个执行栈(调用栈),通过进栈和出栈来处理函数的嵌套和调用。

    当v8在准备执行代码的过程中,会先将全局执行上下文压入执行栈中,之后开始执行主线程上的代码。

image.png

    在执行wait函数过程中,会把wait函数执行上下文压入执行栈中,当wait函数执行完毕,wait函数执行上下文会出栈。之后调用foo函数,过程与调用wait函数一致。

image.png

    当主线程上没有要执行的代码时,全局执行上下文出栈。

image.png

消息(任务)队列

任务队列:存在着两个队列,一个是宏任务队列,一个是微任务队列

宏任务队列:I/O,定时器,事件绑定,ajax,

微任务队列:promise里的then catch finally process nextTick ,async,await

image.png

    这一段代码中,当在主线程中遇到异步代码时,会把这些代码交给其各自处理的线程执行。定时器代码会交由定时器触发线程执行,ajax请求会交由HTTP异步线程执行,click响应事件会交由浏览器事件线程执行。这三段代码的回调函数AB,C会保存在它们各自的线程中,在未来某一时间执行。

这三个线程主要做的两件事情:

(1)执行主线程扔过来的代码,并且去执行这些异步代码;

(2)保存回调函数,异步代码执行成功之后,通知EventLoop轮询处理线程来取相应的回调函数。

    当异步代码执行完毕之后,他们的回调函数会保存到任务队列中。所以说,任务队列就相当于一个容器,存储着异步代码执行成功后返回的回调函数(来源于多个线程)。

image.png

事件循环

结合上文我们可以知道,js主线程处理同步代码,定时器线程等对个线程处理异步代码 ,任务队列保存异步代码执行成功的回调,

而事件循环就在其中扮演沟通协调的角色。

就是说,在主线程执行js代码 的过程中,当遇到了异步任务,就通过事件循环将其交给各自负责的异步线程处理,在异步线程中异步任务执行完毕后,就会通过事件循环将执行成功的回调函数放入到任务队列中。当执行栈为空时,就会查看任务队列,从队头取出一个 回调函数给主线程执行。这就是整个主线程、事件循环、异步线程、任务队列之间的关系。

要注意两点: (1)这个过程是循环往复的 (2)只有当主线程的同步代码 全都执行 完毕,才会去查看 任务队列。

image.png

宏任务

消息队列里的回调函数称之为宏任务。也可以说,宏任务就是消息队列中等待被主线程执行的事件。v8在执行宏任务时,会重新创建栈,随着宏任务中的函数别调用,栈也会随之变化,宏任务执行完毕,栈被清空销毁,主线程继续执行下一个宏任务。

微任务

有一个问题,用于主线程执行宏任务的时间颗粒度很粗,没有适用精密度或实时性比较好的场景。比如说消息队列中的函数A执行的时间比较久,那他一定就会影响他后面的回调函数B、C...的执行,而且这种影响是不可控的,因为不能确定完成当前宏任务需要多久的时间。

所以js引入了微任务这一概念,微任务在实时性和效率之间做了有效的权衡。v8中,每个宏任务会维护一个微任务队列,v8在执行一段代码的时候,会为这段代码创建一个环境对象,微任务队列就保存在这个环境对象中。

看下面这段代码:

(1)首先会执行同步代码console.log(count);,打印 1,继续往下执行;

(2)将定时器代码交由给定时器线程处理,继续往下执行,在这个过程中异步线程会执行异步代码,执行完毕后会将其回调函数放入消息队列中,等待主线程调用;

(3)调用foo函数,首先会执行 console.log(n);,打印2,然后遇到了异步代码,promise创建了一个微任务,这时就会创建一个微任务队列,将微任务的回调函数function() { console.log(3) }放到这个队列中。

(4)当foo函数执行完毕,foo执行上下文出栈,接下来v8就会检查微任务队列里面有没有微任务,有就会依次取出微任务执行。所以会执行console.log(3) ;打印3。微任务执行完毕,当前微任务也就执行完毕了。

(5)接下来,主线程就会去消息队列里去取宏任务,所以会执行console.log(4);,打印4。

(6)所有的任务都执行完毕,消息 队列、主线程和调用栈都会为空。

var count = 1;
console.log(count);

function foo() {
    var n = 2;
    console.log(n);
    Promise.resolve().then(function() {
        console.log(3)
    })
}

setTimeout(function() {
    console.log(4);
}, 1000);

foo();
// 输出顺序
// 1
// 2
// 3
// 4

image.png