JavaScript同步任务先于异步任务执行,那为什么图中异步任务b在a未结束运行的时候就开始执行?

  JavaScript的特点就是单线程,也就是说同一时间只能做一件事情,前面的任务没做完,后面的任务只能处于等待状态,(这就跟生活中的例子:排队买票一样,一个一个排队按顺序来)。这就产生了一个问题:为什么JavaScript不能是多线程的呢?多线程可以提高多核CPU的利用率,从而提高计算能力啊,这与浏览器的用途是息息相关的,也可以说是浏览器的用途直接决定了JavaScript只能是单线程。

  假如说JavaScript是多线程,我们可以试想一下,如果某一时刻一个线程给某个DOM节点添加内容,另一个线程在删除这个DOM节点,这个时候浏览器听那一个线程的?这样会让程序变得非常复杂,而且完全没有必要。同时这也说明JavaScript的单线程与它的用途有关,浏览器的主要用途是操作DOM、与用户完成互动、添加一些交互行为,如果是多线程将会产生非常复杂的问题。所以JS从一开始起为了避免复杂的问题产生,JavaScript就是单线程的,单线程已经成为JavaScript的核心,而且今后也不会改变。

  在多核CPU出现以来,这给单线程带来了非常的不便,不能够充分发挥CPU的作用。为了提高现代多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程,但是,这个本质并没有改变JavaScript单线程的本质。

  什么是单线程与多线程,这个问题值得我们思考?

    单线程:一个进程中只有一个线程在执行,如果把进程比作一间工厂,线程比作一个工人,所谓单线程就是这个工厂中只有一个工人在工作

    多线程:一个进程中同时有多个线程在执行,好比这个工厂中有多个工人在一起协同工作

    进程:CPU资源分配的最小单位,一个程序要想执行就需要CPU给这个程序分配相应的资源出来,用完之后再收回去。例如:内存的占用,CPU给这个程序提供相应的计算能力。

    线程:CPU调度的最小单位,一个程序可以理解为有N个任务组成的集合,某一时刻执行那个任务就需要线程的调度

    注意点:有图可知:工厂空间是共享的,说明一个进程中可以存在一个或多个线程,工厂资源是共享的,说明一个进程的内存空间可以被该进程中所有线程共享,多个进程之间相互独立,例如:听音乐的时候,不会影响到敲代码,歌词是不会出现在代码编辑器中的。

 多进程:在同一时间里,同一台计算机系统中允许两个或两个以上的进程处于运行状态,其实现在的计算机开机状态下就是多个进程在运行,打开任务管理器就可以看到那些进程在运行。多进程带来的好处是非常明显的,可以充分利用CPU的资源,而且电脑同时可以干很多件事还互相不影响。例如:在使用编辑器写代码的时候,还可以使用QQ音乐听歌。编辑器和QQ音乐之间完全不会影响。以Chrome为例:每打开一个tab就就相当于开启了一个进程,每个tab之间是完全不会影响。

  上面提到HTML5的Web Worker可以改善单线程的不便,了解Web Worker需要注意以下几点:

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

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

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

  在理解渲染原理之前,先了解浏览器内核构成是非常有必要的,具体内容看下面:

    浏览器工作方式:浏览器内核是通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果。

    浏览器内核是多线程的,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

    GUI渲染线程:

  • 主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等。
  • 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
  • 该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,主线程才会去执行GUI渲染,这也是为什么js解析脚本的时候会阻塞界面的渲染。
  • 该线程当然是主要负责处理 JavaScript脚本,执行代码。
  • 也是主要负责执行准备好待执行的事件(异步事件),即定时器计数结束,或者异步请求成功并正确返回时,I/O读取文件等等将依次进入任务队列,等待 JS引擎线程的执行。
  • 该线程与 GUI渲染线程互斥,当 JS引擎线程执行 JavaScript脚本时间过长,将导致页面渲染的阻塞。

    定时触发器线程

  • 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。
  • 主要负责将准备好的事件交给 JS引擎线程执行。例如:setTimeout定时器计数结束, ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将相应的事件依次加入到任务队列的队尾,等待 JS引擎线程的执行。

    异步http请求线程

  • 负责执行异步请求一类函数的线程,如: Promise,axios,ajax等。
  • 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。

  之前初步了解了浏览器的内核构成,提到了很多异步事件,那异步事件如何执行呢?这就跟任务队列有关了。现在来谈谈什么是任务队列,为什么需要任务队列?

  单线程也就意味着所有任务都需要排队,只有在前一个任务结束之后,才会执行后一个任务,否则后面的任务只能处于等待状态。如果某个任务执行需要很长的时间,后面的任务就都需要等待着,这会造成非常大的交互影响,给用户有一种页面加载卡顿的现象,影响用户体验!

  如果等待是因为CPU忙不过来也可以理解,大多数情况并不是CPU忙不过来的原因,而是文件I/O的读取,网络请求、鼠标点击事件等这些操作需要花费非常长的时间,只有等这些操作返回结果之后才能往下执行。

  为了解决这个问题,JavaScript的开发者也很快的意识到,脚本文件的执行,完全可以不用管那些非常耗时的I/O设备,异步请求,完全可以挂起等待中的任务,执行排在后面的位置,等I/O操作返回结果之后,再回过头执行挂起的任务。

  • 同步任务:在主线程(JS引擎线程)进行排队的任务,只有在前一个任务结束之后,才开始执行后一个任务。
  • 异步任务:不进入主线程,进入任务队列(task queue)的任务,只有在任务队列通知主线程任务队列中的某个任务可以进入主线程了,该任务才会进入主线程执行,也可以将异步任务理解为:有注册函数和回调两部分组成,注册函数负责发起异步过程,回调函数用来负责处理的结果。
  • 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
  • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"执行栈"中的所有同步任务执行完毕,系统就会去"任务队列"找找看,看看里面有哪些事件,那些对应的异步任务,已经结束等待状态,得到的相应的结果,得到结果的任务结束等待状态立即进入执行栈,开始执行。
  • 主线程不断重复上面的第三步的操作,也就形成了一个事件环(Event Loop)

  任务队列是一个事件队列(也可以理解为:消息队列),异步操作如:I/O设备完成一项任务,就在任务队列中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面的事件。

  任务队列中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列,等待主线程读取。

  所谓"回调函数"(callback),就是那些会被主线程挂起来的实现该任务的代码,主线程干开始运行的时候是不会执行的。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数代码。

  任务队列是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器功能“”,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

五、浏览器中的事件循环(Event Loop)

  主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

  为了更好的理解事件循环,请看下面的图:

   执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。

     浏览器端事件循环的异步队列有两种:macro(宏任务)队列、micro(微任务)队列,macro队列可以有多个,micro队列只能有一个。

   现在再拿之前的图来解释任务执行的流程,进一步加深理解:

  1. 一开始执行栈为空,执行栈可以理解为“先进后出”的栈结构,micro任务队列为空,macro任务队列中只有一个script 脚本,整体的js代码。
  2. 全局上下文(script整体代码)被推入执行栈,代码开始执行,判断是同步任务还是异步任务,如果是异步任务通过对一些接口的调用可以产生新的macro任务和micro任务,同步代码执行完毕,script脚本被移除macro任务队列,这个过程可以理解为macro-task的执行和出队。
  3. 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。

  4. 执行渲染操作,更新界面。
  5. 检查是否存在 Web worker 任务,如果有,则对其进行处理。
  6. 上述过程循环往复,直到两个队列都清空。

   总结一下:当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。

     1.代码开始执行,判断是同步任务还是异步任务,检测到有同步任务(属于宏任务)存在,先输出“同步任务”

     2. 同步任务执行完去查看是否有微任务队列存在,上面代码的微任务队列为:promise.resolve().then(),开始执行微任务,输出micro-task1

     3.执行微任务的过程中发现有宏任务setTimeout()存在,将其添加到宏任务队列,微任务执行完毕开始执行宏任务,由于macro-task2所在的宏任务早于macro-task1,因此先执行macro-task2所在的宏任务,输出macro-task2

     5.宏任务执行完毕,接着开始执行微任务输出:micro-task2

     6.微任务执行完毕,接着执行macro-task1所在的宏任务,输出:macro-task1

     7.执行完毕宏任务,此时的macro-task队列和micro-task队列已空,程序停止。

六、Node.js中的事件循环

  Node.js 不是一门语言也不是框架,它只是基于 Google V8 引擎的 JavaScript 运行时环境,同时结合 Libuv 扩展了 JavaScript 功能,使之支持 io、fs 等只有语言才有的特性,使得 JavaScript 能够同时具有 DOM 操作(浏览器)和 I/O、文件读写、操作数据库(服务器端)等能力,是目前最简单的全栈式语言。

  目前Node.js在大部分领域都占有一席之地,尤其是 I/O 密集型的,比如 Web 开发,微服务,前端构建等。不少大型网站都是使用 Node.js 作为后台开发语言的,用的最多的就是使用Node.js做前端渲染和架构优化,比如 淘宝 双十一、去哪儿网的 PC 端核心业务等。另外,有不少知名的前端库也是使用 Node.js 开发的,比如,Webpack 是一个强大的打包器,React/Vue 是成熟的前端组件化框架。

  Node.js通常被用来开发低延迟的网络应用,也就是那些需要在服务器端环境和前端实时收集和交换数据的应用(API、即时聊天、微服务)。阿里巴巴、腾讯、Qunar、百度、PayPal、道琼斯、沃尔玛和 LinkedIn 都采用了 Node.js 框架搭建应用。

  Node.js 编写的包管理器 npm 已成为开源包管理了领域最好的生态,直接到2017年10月份,有模块超过47万,每周下载量超过32亿次,每个月有超过700万开发者使用npm。

  当然了,Node.js 也有一些缺点。Node.js 经常被人们吐槽的一点就是:回调太多难于控制(俗称回调地狱)。但是,目前异步流程技术已经取得了非常不错的进步,从Callback、Promise 到 Async函数,可以轻松的满足所有开发需求。

  至于其他的特性这里附一篇很值得一看的文档:

  Node中的事件循环机制完全和浏览器的是不同的,Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。

  Node.js的运行机制如下:

    2.解析后的代码,调用Node API

    3.libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎

    4.V8引擎再将结果返回给用户

    libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。

,按照上面的顺序循环反复执行。

    timer阶段:这个阶段执行setTimeout或setInterval回调,并且是有poll阶段控制的。同样在Node.js中定时器指定的时间也不是非常准确,只能是尽快执行。

    I/O callbacks阶段:处理一些上一轮循环中少数未执行的I/O回调

    poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里

    注意点:上面的6个阶段都是不包括process.nextTick(),在日常的开发中我们使用最多的就是:timer  poll  check这三个阶段,绝大多数的异步操作都是在这三个阶段 完成的。

  timer阶段:这个阶段执行setTimeout或setInterval回调,并且是有poll阶段控制的。同样在Node.js中定时器指定的时间也不是非常准确,只能是尽快执行。

    1.这个阶段是至关重要的阶段,系统会做两件事情。一是回到timer阶段执行回调,二是执行I/O回调

    2.如果在进入该阶段的时候没有设置timer,会发生两件事情:

      2.1.如果poll队列不为空,会遍历回调队列并同步执行,直到队列为空或达到系统限制

      2.2.如果poll阶段为空,会发生下面两件事:

        2.2.1:如果有setImmediate()回调需要执行,poll阶段会停止,进入到check阶段执行回调

        2.2.2:如果没有setImmediate()回调需要执行,会等到回调被加入队列中并立即执行回调,这里同样有个超时限制防止一致等待下去

    3.如果设置了timer且poll队列为空,则会判断是否有timer超时,如果有的话回到timer阶段执行回调。

   3.先分析一段代码示例:

      2.执行micro-task任务:输出promise3,这一点跟浏览器的机制差不多

      5.1:setTimeout()设置在poll阶段为空闲时且定时时间到后执行,但它们在timer阶段执行

      5.3:实例分析:

回调,如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了,可以把setTimeout的第二个参数设置为:1 ,2,3,4....看看运行结果

      5.5:当二者写在I/O读取操作的回调中时,总是先执行setImmediate(),因为I/O回调是写在poll阶段,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了

   6.process.nextTick():这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。也就是说它指定的任务队列,都是在所有异步任务之前发生

  重点:浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

 浏览器和node环境下、micro-task队列的执行时机不同:

  浏览器端,微任务在事件循环的各个阶段执行。

  Node端,微任务是在macro-task执行完毕执行。

内容来源于网络如有侵权请私信删除

一年前,我写了一篇,谈了我对Event Loop的理解。

上个月,我偶然看到了Philip Roberts的演讲。这才尴尬地发现,自己的理解是错的。我决定重写这个题目,详细、完整、正确地描述JavaScript引擎的内部运行机制。下面就是我的重写。

进入正文之前,插播一条消息。我的新书出版了(,,),铜版纸全彩印刷,非常精美,还附有索引(当然价格也比同类书籍略贵一点点)。预览和购买点击。

2014年10月13日更新:本文已经做了较大修改,反映了我现在的认识。关于setTimeout的更多解释和示例,请参阅我正在写的。)

2014年10月11日更新:朴灵老师对本文做了,详细得指出了文中存在的错误说法,建议阅读。)

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

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

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

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。请看下面这个例子。


上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。


也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。


上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。


上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

请看下面的示意图(作者)。

根据上图,Node.js的运行机制如下。

(2)解析后的代码,调用Node API。

(3)负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:和。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via )。


上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。


令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。


Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。

10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!


另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个线程,即为JS引擎线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

1.1 单线程代码示例

1.2 浏览器的渲染进程是提供多个线程

也称为JS内核,负责处理JavaScript脚本。(例如V8引擎)
①JS引擎线程负责解析JS脚本,运行代码。
②JS引擎一直等待着任务队列中的任务的到来,然后加以处理。
③一个Tab页(renderer进程)中无论什么时候都只有一个JS线程运行JS程序。
归属于渲染进程而不是JS引擎,用来控制事件循环
①当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其他线程,如鼠标点击、Ajax异步请求等),会将对应任务添加到事件线程中。
②当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
注意:由于JS的单线程关系,所以这些待处理队列中的事件都是排队等待JS引擎处理,JS引擎空闲时才会执行。
①浏览器定时计数器并不是由JS引擎计数的。
②JS引擎时单线程的,如果处于阻塞线程状态就会影响计时的准确,因此,通过单独的线程来计时并触发定时。
③计时完毕后,添加到事件队列中,等待JS引擎空闲后执行。
注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
XMLHttpRequest在连接后通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调放入事件队列中,再由JS引擎执行。
负责渲染浏览器界面,包括:
②重绘(Repaint)以及回流(Reflow)处理。

  1. 一个进程包含一个或多个线程
  2. chrome为例,打开一个Tab(创建一个进程),又会包含多个线程(JS引擎线程、渲染线程)

“任务队列”是一个事件的队列(也可以理解成消息的队列)

任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务 指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务 指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
“任务队列”是一个先进先出(FIFO)的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的定时器功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。

3.1 执行栈和事件队列

//b、a、c复制代码

就算延时为 0ms,只是 timer 2 的回调函数会立即加入消息队列而已,回调的执行还是得等执行栈为空(JS引擎线程空闲)时执行。

其实 setTimeout 的第二个参数并不能代表回调执行的准确的延时事件,它只能表示回调执行的最小延时时间,因为回调函数进入消息队列后需要等待执行栈中的同步任务执行完成,执行栈为空时才会被执行。

浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。

有些地方会列出来UI Rendering,说这个也是宏任务
可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤



requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行

Promise中注意是回调,而new Promise在实例化的过程中所执行的代码都是同步进行的

  1. 一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。
  2. 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
  3. 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
  4. 执行渲染操作,更新界面
  5. 检查是否存在 Web worker 任务,如果有,则对其进行处理
  6. 上述过程循环往复,直到两个队列都清空

3.4 事件循环伪代码

四、事件循环(进阶)与异步

// 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行

直到 2 秒后,主线程中的任务才执行完成,这才去执行 macrotask 中的 setTimeout 回调任务。

  1. 函数前面async关键字的作用就2点:①这个函数总是返回一个promise。②允许函数内使用await关键字。
  2. 关键字await使async函数一直等待(执行栈当然不可能停下来等待的,await将其后面的内容包装成promise交给Web APIs后,执行栈会跳出async函数继续执行),直到promise执行完并返回结果。await只在async函数函数里面奏效。
  3. async函数只是一种比promise更优雅得获取promise结果(promise链式调用时)的一种语法而已。

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境

JS的运行环境主要有两个:浏览器、Node。
在两个环境下的Event Loop实现是不一样的,在浏览器中基于规范来实现,不同浏览器可能有小小区别。在Node中基于libuv这个库来实现。

  1. 解析后的代码,调用Node API
  2. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
  3. V8引擎再将结果返回给用户

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

浏览器和Node 环境下,microtask 任务队列的执行时机不同:

  1. Node端,microtask 在事件循环的各个阶段之间执行

先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务

  1. 从上往下执行代码,先执行同步代码,输出script start
  2. 接着往下执行,输出promise1,把 .then()放到微任务队列中。(注意Promise本身是同步的立即执行函数,.then是异步执行函数!)
  3. 接着往下执行, 输出script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
  4. 依次执行微任务中的代码,依次输出async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出setTimeout

这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务

  1. 从上往下执行代码,先执行同步代码,输出start
  2. 代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出children2,此时,会把 Promise.resolve().then放到微任务队列中。
  3. 宏任务①中的代码执行完成后,会查找微任务队列,于是输出children3;然后开始执行宏任务②,即第二个 setTimeout,输出children5,此时将.then放到微任务队列中。
  4. 宏任务②中的代码执行完成后,会查找微任务队列,于是输出children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出children6

  1. 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出3
  2. 遇到 p().then 会先放到微任务队列中,接着往下执行,输出end
  3. 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出2, 接着执行p().then, 输出4
  4. 微任务执行完成后,开始执行宏任务,setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时1不会输出。

  1. 一开始执行栈的同步任务(这属于宏任务) 执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出Promise1,同时会生成一个宏任务 setTimeout2
  2. 在执行宏任务setTimeout1时会生成微任务Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出Promise2
  3. 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是setTimeout2

由于在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,因此,如果它源源不断地产生新的microtask任务,就会导致主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样我们就无法进行UI渲染/IO操作/ajax请求了,因此,我们应该避免这种情况发生。在nodejs里的process.nexttick里,就可以设置最大的调用次数,以此来防止阻塞主线程。

我要回帖

更多关于 java异步执行不影响主流程 的文章

 

随机推荐