同步与异步模式简介

我们知道,Javascript语言的执行环境是 单线程 (single thread)的。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种: 同步 (Synchronous)和 异步 (Asynchronous)。

同步模式 就是后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;

异步模式 则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行队列上的后一个任务,而是执行回调函数;后一个任务则是不等前一个任务的回调函数的执行而执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

"异步模式" 非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端, "异步模式" 甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

异步任务队列

可能有人告诉你,Javascript内部存在着先进先出的异步任务队列,仅仅用以存储异步任务,与同步任务分开管理。

进程执行完全部同步代码后,每当进程空闲、触发回调或定时器到达规定的时间,Javascript会从队列中顺序取出符合条件的异步任务并执行之。

我们简单验证一下,

var timeout1 = setTimeout(function() {
  console.log(2);
}, 0);
console.log(1);
var timeout2 =setTimeout(function() {
  console.log(3);
}, 0);

上面的代码我们都知道输出是 1 2 3,因为setTimeout是异步任务,而timeout1又比timeout2先注册,所以最终输出了这个结果。

然而,仅仅通过以上代码我们确定不了同步任务究竟是不是会优先于异步任务执行,因为setTimeout有一个最小的时间间隔限制,在这个时间间隔里语句console.log(1)完全可以执行完毕,我们要想办法让同步代码占用更长时间。

定时器最小时间间隔:在苹果机上的最小时间间隔是10ms,在Windows系统上的最小时间间隔大约是15ms。Firefox中定义的最小时间间隔是10ms,而HTML5规范中定义的最小时间间隔是4ms

再阅读下面代码,

setTimeout(function() {
  console.log(1);
}, 0);
console.log(2);
let end = Date.now() + 1000*5;
while (Date.now() < end) {
console.log(3);
end = Date.now() + 1000*5;
while (Date.now() < end) {
console.log(4);

输出顺序:2 3 4 1。从上面的输出结果我们可以确定,异步代码是在所有同步代码执行完毕以后才开始执行的。

那我们刚刚对js异步任务队列的理解方式是对的吗?底层机制会是这样的吗?

事实上,我们上述对于异步队列的理解和解释都是非常浅层和感性的(并且是错误的),虽然跟着上述的理解方式我们可以解释很多代码行为,但实际的机制却远没有这么简单,异步模式作为Javascript的重中之重,有很多设计细节是我们未知的,我们应当更加理性和学术地去探究学习。

再看一段比较复杂的代码,说出它的输出顺序:

setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
console.log(6);
setTimeout(function(){
    console.log(7);
},0);
console.log(8);

你认为上述代码输出结果是什么呢?讲出理由。

浏览器环境输出结果:输出顺序为,3 4 6 8 5 2 7,跟你事先认为的结果一样吗?为什么结果会这样?

除了注册顺序以外,还有什么因素影响着每个异步任务在异步队列中的顺序呢?

我们先一起了解下事件循环任务队列两个概念,再回来解答这个问题。

线程、事件循环和任务队列

Javascript是单线程的,但是却能执行异步任务,这主要是因为 JS 中存在事件循环(Event Loop)和任务队列(Task Queue)

事件循环:JS 会创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为Tick。每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次Tick会查看任务队列中是否有需要执行的任务。

任务队列:异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如onclicksetTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的webcore来执行的,webcore包含下图中的3种 webAPI,分别是DOM Bindingnetworktimer模块。

  • DOM Binding 模块处理一些DOM绑定事件,如onclick事件触发时,回调函数会立即被webcore添加到任务队列中。
  • network 模块处理Ajax请求,在网络请求返回时,才会将对应的回调函数添加到任务队列中。
  • timer 模块会对setTimeout等计时器进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。

主线程:JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。

ES5规范中对于事件循环的定义

翻开规范《ECMAScript® 2015 Language Specification》,找到事件循环 6.1.4 Event loops

规范中中提到,一个浏览器环境,只能有一个事件循环,而一个事件循环可以多个任务队列,每个任务都有一个任务源(Task source)。

相同任务源的任务,只能放到一个任务队列中。

不同任务源的任务,可以放到不同任务队列中。

又举了一个例子说,客户端可能实现了一个包含鼠标键盘事件的任务队列,还有其他的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它。这样就能保证流畅的交互性,而且别的任务也能执行到了。同一个任务队列中的任务必须按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行。

结论:一个事件循环可以有多个任务队列,队列之间可有不同的优先级,同一队列中的任务按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行。

重新看回开始的代码:

setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
console.log(6);
setTimeout(function(){
    console.log(7);
},0);
console.log(8);

输出结果是,3 4 6 8 5 2 7。为什么setTimeout会后于promise.then执行呢,原因或许就是它所处的任务队列优先级较低。

不同任务队列的优先级

那么接下来,我们探究一下不同任务队列的优先级。

实际上,对于任务队列的优先级的定义,Promise/A+ 规范中有作详细的解释。

图灵社区 : 阅读 : 【翻译】Promises/A+规范

我们都知道,一个Promise的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。

而上面的Promises规范就规定了,实践中要确保onFulfilledonRejected异步执行,且应该在then方法被调用的那一轮事件循环以后的新执行栈中执行

意思就是,当我们调用resolve()reject()的时候,触发promise.then(...)实际上是一个异步操作,这个promise.then(...)并不是在resolve()reject()的时候就立刻执行的,而也是要重新进入任务队列排队的,不过能直接在当前的事件循环新的执行栈中被取出执行(不用等下次事件循环)。

知道这个以后,我们再看一段代码,这个代码包含常用的大部分异步操作,我们将借此得出不同任务队列的优先顺序:

(其中setImmediate()process.nextTick()是node的语句)

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
console.log(6);
process.nextTick(function(){
    console.log(7);
console.log(8);

NodeJs环境输出

其中3 4 6 8是同步输出的。 因为注册顺序:1 > 2 > 5 > 7,而输出顺序是7 > 5 > 2 > 1

所以可以很容易得到,优先级 :process.nextTick > promise.then > setTimeout > setImmediate

process.nextTick()属于idle观察者,setImmediate()属于check观察者.在每一轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者.

而实际上,上述的Promises规范早已提到异步队列优先级规定的详细定义和解释了,并不需要我们一个一个去测试。

在JS引擎中,我们可以按性质把任务分为两类,macrotask(宏任务)和 microtask(微任务)。

浏览器JS引擎中:

  • macrotask(按优先级顺序排列): script(你的全部JS代码,“同步代码”), setTimeoutsetIntervalsetImmediateI/O,UI rendering
  • microtask(按优先级顺序排列):process.nextTick,Promises(这里指浏览器原生实现的 Promise), Object.observeMutationObserver
  • JS引擎首先从macrotask queue中取出第一个任务,执行完毕后,将microtask queue中的所有任务取出,按顺序全部执行;
  • 然后再从macrotask queue(宏任务队列)中取下一个,执行完毕后,再次将microtask queue(微任务队列)中的全部取出;
  • 循环往复,直到两个queue中的任务都取完。

所以,浏览器环境中,js执行任务的流程是这样的:

  1. 第一个事件循环,先执行script中的所有同步代码(即 macrotask 中的第一项任务)
  2. 再取出 microtask 中的全部任务执行(先清空process.nextTick队列,再清空promise.then队列)
  3. 下一个事件循环,再回到 macrotask 取其中的下一项任务
  4. 再重复2
  5. 反复执行事件循环…

NodeJS引擎中:

  1. 先执行script中的所有同步代码,过程中把所有异步任务压进它们各自的队列(假设维护有process.nextTick队列、promise.then队列、setTimeout队列、setImmediate队列等4个队列)
  2. 按照优先级(process.nextTick > promise.then > setTimeout > setImmediate),选定一个  不为空 的任务队列,按先进先出的顺序,依次执行所有任务,执行过程中新产生的异步任务继续压进各自的队列尾,直到被选定的任务队列清空。
  3. 重复2...

也就是说,NodeJS引擎中,每清空一个任务队列后,都会重新按照优先级来选择一个任务队列来清空,直到所有任务队列被清空。

现在,你可以根据这个流程再看回前面的代码,其实一切都很容易理解了…

以上,就是Javascript任务队列的顺序机制。

同步与异步模式简介我们知道,Javascript语言的执行环境是单线程(single thread)的。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就... 简单SQLite支持的队列,可使用setImmediate()在Node.js中运行许多短任务 如果您有大量正在运行的小型任务,则该库将允许它们通过node.js的主事件线程按顺序执行,而不会阻塞/使其他node.js事件处于饥饿状态。 该库的目的是提供一种简单的方法: 按FIFO顺序连续执行一次任务队列 维护一个磁盘队列(使用SQLite),该队列在崩溃/重新启动后仍然存在 确保node.js事件循环可以使用setImmediate()返回每个作业调用之间的,从而防止阻塞。 通过承诺实现异步 工作单元或任务作为简单的json对象存储在队列中。 每个任务应以足够的速度完成,以免阻塞您的node.js事件循环。 如果无法充分分解任务,则应考虑使用多线程Worker实现。 但是,如果您有大量正在运行的小型任务,则该库将允许它们通过node.js的主事件线程执行
什么是事件循环 尽管JavaScript是单线程的,但通过尽可能将操作放到系统内核执行,事件循环允许Node.js执行非阻塞I/O操作。 由于现代大多数内核都是多线程的,因此它们可以处理在后台执行的多个操作。 当其中一个操作完成时,内核会告诉Node.js,以便可以将相应的回调添加到 轮询队列 中以最终执行。 我们将在本主题后面进一步详细解释。 事件循环解释 当Node.js启动时,它初始化事件循环,处理提供的输入脚本(或放入 REPL ,本文档未涉及),这可能会进行异步API调用,调度计时器或调用 process.nextTick() , 然后开始处理事件循环。 下图显示了事件循环操作顺序
Event Loop事件循环机制 事件循环机制(Event Loop)是全面了解javascript代码执行顺序绕不开的一个重要知识点。 为什么会有事件循环机制javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。 首先等来了解js为什么是单线程的吧! 首先要明白线程的概念? 进程是资源分配的最小单位,线程是进程的一部分,是cpu调度的基本单位; 了解浏览器 浏览器中,每次打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有js引擎线程、ui渲染线程、http请求线程。 这里面还可深入了解js引擎线程、ui渲染线程、http请
栈和队列是web开发中最常用的两种数据结构。绝大多数用户,甚至包括web开发人员,都不知道这个惊人的事实。如果你是一个程序员,那么请听我讲两个启发性的例子:使用堆栈来组织数据,来实现文本编辑器的“撤消”操作;使用队列处理数据,实现web浏览器的事件循环处理事件(单击click、悬停hoover等)。 等等,先想象一下我们作为用户和程序员,每天使用栈和队列的次数,这太惊人了吧!由于它们在设计上有普遍性和相似性,我决定从这里开始为大家介绍数据结构。 在计算机科学中,栈是一种线性数据结构。如果你理解起来有困难,就像最初非常困惑的我一样,不妨这样认为:一个栈可以对数据按照顺序进行组织和管 Promise和异步观察器库以及手动滚动的回调程序和库通常需要一种机制来将回调的执行推迟到下一个可用事件之前。 (请参阅 。) asap函数会asap执行任务,但不会在返回之前执行任务,仅等待当前事件和先前计划的任务的完成。 asap ( function ( ) { // ... } ) ; 该CommonJS软件包提供了一个asap模块,该模块可导出一个函数,该函数将尽快执行任务功能。 ASAP努力将事件安排在产生IO,重排或重制图纸之前发生。 每个事件都接收一个独立的堆栈,父框架中仅包含平台代码,并且事件按其安排的顺序运行。 ASAP提供了一个快速事件队列,该队列将执行任务直到其为空,然后再屈服于JavaScript引擎的基础事件循环。 当一个任务被添加到以前为空的事件队列中时,ASAP会安排一个刷新事件,希望该事件在JavaScript引擎有机会执行IO任务
Promise在前端中主要用于处理异步调用,其基本使用方式通过阮一峰大佬的文档一下就可以入手,但是最近我看了一篇文章wecTeam中,作者深山蚂蚁的《高级进阶:深度揭秘Promise注册微任务和执行过程》一文,让我对Promise的执行顺序有了更深的了解,与此同时我也有了一个疑问,通过这篇文章与大家探讨。 1. promise的异步主要发生在微任务队列中 2. 第一个then的回调监听最新Pr...
JavaScript 代码执行顺序 1. js的执行顺序,先同步后异步 2. 异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列 (微任务优先级高于宏任务的前提是:同步代码已经执行完成。) 3. Promise 里边的代码属于同步代码,.then() 中执行的代码才属于异步代码 微任务包括 process.nextTick ,promise ,MutationObser
虽然大家知道async/await,但是很多人对这个方法中内部怎么执行的还不是很了解 await做了什么处理 从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。 很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈后面的代码。等本轮事件循环执行完了之后又会跳回到async函数中等待await
584866489: 修改后,重启报错: ERROR Failed to compile with 1 error 1:30:10 PM error in ./node_modules/element-ui/packages/scrollbar/src/main.js Module parse failed: Unexpected token (65:6) JavaScript任务队列的顺序机制(事件循环) 前端.火鸡: 觉得说的不对的可以看一下自己的node的版本 当产品说elementUI的datepicker要加上时区…… MainMay: 第一步复制出来datepicker报错 98% after emitting CopyPlugin ERROR Failed to compile with 1 error 17:01:06 error in ./node_modules/element-ui/packages/scrollbar/src/main.js Module parse failed: Unexpected token (65:6) You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders | }, this.$slots.default); | const wrap = ( | ref="wrap" | style={ style } @ ./node_modules/element-ui/packages/scrollbar/index.js 1:0-35 4:0-9 5:16-25 5:32-41 8:15-24 @ ./node_modules/cache-loader/dist/cjs.js??ref--12-0!./node_modules/babel-loader/lib!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/components/dat e-picker/src/basic/time-spinner.vue?vue&type=scrip 当产品说elementUI的datepicker要加上时区…… 循环22222: 问下上面的步骤