事件循环eventLoop

文章中难免有相互矛盾的地方,会逐渐完善,下面是关于 js 事件循环的一些内容

一、js 为何单线程

浏览器进程里有多个线程,比如定时器,http 请求等,但页面的渲染过程是单线程的,也就是说 ui 渲染和 j 解析是同一个线程,也就是同一时间只能执行二者其中一个。。。

js 用途是与用户互动以及操作 dom,为避免复杂性,从一诞生就是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

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

二、任务队列

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

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

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

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

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

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

注意:只有等到异步任务有了结果才会进入任务队列(task queu),而在有结果之前异步任务进入 task table 注册并开始计时。也就是说 js 代码从上向下执行,碰到异步任务就放到 task table 里,对于定时器如果时间到了就放进任务队列里,对于 ajax 请求,则等到返回响应才会进入任务队列

只要主线程空了(js 引擎存在 monitoring process 进程,会持续不断检查主线程执行栈是否为空),就会去读取”任务队列”,这就是 JavaScript 的运行机制。这个过程会不断重复。

再来通过一段伪代码了解一下这个概念 :

// eventLoop是一个用作队列的数组 //(先进,先出)
var eventLoop = [];
var event;
//“永远”执行
while (true) {
  // 一次tick
  if (eventLoop.length > 0) {
    // 拿到队列中的下一个事件
    event = eventLoop.shift();
    // 现在,执行下一个事件
    try {
      event();
    } catch (err) {
      reportError(err);
    }
  }
}

上面是一段伪代码,只用来说明概念,你可以看到,有一个用 while 循环实现的持续运行的循环,循环的每一轮称为一个 tick。 对每个 tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这 些事件就是你的回调函数。

三、事件及回调函数

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

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数(没有回调函数将无法向主线程反馈异步任务的结果),当主线程开始执行异步任务,就是执行对应的回调函数。

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

四、Event Loop

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

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

栈 (stack)分配固定大小内存(存放指针及基本数据类型),系统自动回收内存。堆 (heap)是动态分配内存大小, 不自动释放内存(引用数据类型的数据存放于此)。

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

var req = new XMLHttpRequest();
req.open("GET", url);
req.onload = function() {};
req.onerror = function() {};
req.send();

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

var req = new XMLHttpRequest();
req.open("GET", url);
req.send();
req.onload = function() {};
req.onerror = function() {};

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

再看下面代码

let data = [];
$.ajax({
  url: "www.js.com",
  data: data,
  success: () => {
    console.log("发送成功");
  }
});
console.log("代码执行结束");

上面代码执行的顺序:

  1. ajax 进入 Event Table,注册回调函数 success。
  2. 执行 console.log(‘代码执行结束’)。
  3. ajax 事件完成,回调函数 success 进入 Event Queue。
  4. 主线程从 Event Queue 读取回调函数 success 并执行。

node.js 的事件循环
参考:nodejs 的事件循环(官网)剖析 nodejs 的事件循环

  • EventLoop 是一种事件处理模型。
  • node.js 实现了单线程高效的异步 IO,这里的单线程指的是执行 js 代码部分的线程
  • 异步 IO 部分 node.js 是利用线程池去执行的

nodejs架构 上图是 nodejs 的架构,从上到下:

  1. 用户代码 js(也就是我们编写的应用程序,npm 包,nodejs 内置的模块等)
  2. binding 代码或第三方插件(js 或 c/c++代码,胶水代码能够让 js 调用 c/c++的代码,相当于一个桥联通 js 与 c/c++)
  3. 底层库(nodejs 的依赖库,包括 v8,libuv(c 语言实现的一套异步功能库,nodejs 的异步编程模型很大程度归功于 libuv 的实现),还包括其他依赖库 http-parser,openssl 加解密,c-ares 解析 dns,npm 包管理器)

而 nodejs 实现异步机制的核心便是 libuv,libuv 承担着 nodejs 与文件、网络等异步任务的沟通桥梁 libuv架构 上图中可以看到:nodejs 的网络 I/O、文件 I/O、DNS 操作、还有一些用户代码都是在 libuv 工作的。

非 I/O:

  • 定时器(setTimeout,setInterval)
  • microtask(promise)
  • process.nextTick
  • setImmediate
  • DNS.lookup

I/O:

  • 网络 I/O
  • 文件 I/O
  • 一些 DNS 操作

对于网络 I/O,各个平台的实现机制不一样,linux 是 epoll 模型,类 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 对这几种网络 I/O 模型进行了封装。

libuv 内部还维护着一个默认 4 个线程的线程池,这些线程负责执行文件I/O 操作、DNS 操作、用户异步代码。当 js 层传递给 libuv 一个操作任务时,libuv 会把这个任务加到队列中。之后分两种情况:

  • 线程池中的线程都被占用的时候,队列中任务就要进行排队等待空闲线程。
  • 程池中有可用线程时,从队列中取出这个任务执行,执行完毕后,线程归还到线程池,等待下个任务。同时以事件的方式通知 event-loop,event-loop 接收到事件执行该事件注册的回调函数。

注意:如果觉得 4 个线程不够用,可以在 nodejs 启动时,设置环境变量 UV_THREADPOOL_SIZE 来调整,出于系统性能考虑,libuv 规定可设置线程数不能超过 128 个。

官网解释 EventLoop
尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去

既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到 轮询 队列中等待时机执行。

五、定时器

w3c 更多标准
从 event loop 规范探究 javaScript 异步及浏览器更新渲染时机 除了放置异步任务的事件,”任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做”定时器”(timer)功能,也就是定时执行的代码。

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

console.log(1);
setTimeout(function() {
  console.log(2);
}, 1000);
console.log(3);

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

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

setTimeout(function() {
  console.log(1);
}, 0);
console.log(2);

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

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

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

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

requestAnimationFrame 是浏览器用于定时循环操作的一个接口,类似于 setTimeout,主要用途是按帧对网页进行重绘。

设置这个 API 的目的是为了让各种网页动画效果(DOM 动画、Canvas 动画、SVG 动画、WebGL 动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。代码中使用这个 API,就是告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。

requestAnimationFrame 的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz 或 75Hz),也就是说,每秒最多只能重绘 60 次或 75 次,requestAnimationFrame 的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个 API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了 CPU、GPU 和电力。

不过有一点需要注意,requestAnimationFrame 是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame 的动画效果会大打折扣

参考:

  1. 阮一峰-requestAnimationFrame
  2. 淘宝 FED-requestAnimationFrame
  3. 张鑫旭-requestAnimationFrame

六、Node.js 的 Event Loop

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

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

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

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B() {
    console.log(2);
  });
});

setTimeout(function timeout() {
  console.log("TIMEOUT FIRED");
}, 0);
// 1
// 2
// TIMEOUT FIRED

由于 process.nextTick 方法指定的回调函数,总是在当前”执行栈”的尾部触发,所以不仅函数 A 比 setTimeout 指定的回调函数 timeout 先执行,而且函数 B 也比 timeout 先执行。这说明,如果有多个 process.nextTick 语句(不管它们是否嵌套),将全部在当前”执行栈”执行。而 setImmediate 总是将事件注册到下一轮 Event Loop,多个 process.nextTick 语句总是在当前”执行栈”一次执行完,多个 setImmediate 可能则需要多次 loop 才能执行完。

注意:到这里应该对 Event Loop 有深刻理解了吧,所谓的 Event Loop 是周而复始读取任务队列的过程。而 process.nextTick 方法会在当前执行栈的尾部触发回调,也就是在当前执行栈的最后执行位置。而 setImmediate 也是在当前任务队列的尾部添加事件。(待完善)

宏任务和微任务

首先我们要知道宏任务和微任务都是异步任务

宏任务:包括整体代码 script,setTimeout ,setInterval,setImmediate 微任务:原生 promise(有些实现的 promise 将 then 方法放到宏任务),process.nextTick,MutationObserve

知道了异步任务是如何进入任务队列(task queue),对于 setTimeout(fn,time)是从注册后 time 毫秒后才会进入任务队列,而 setInterval(fn, time)则是每隔 time 毫秒就会进入到任务队列。注意此时若 setInterval 的回调 fn 执行时间大于延迟时间 time,则就看不出来有时间间隔了。。。

先来看个简单的

setTimeout(() => {
  console.log("setTimeout1");
}, 0);
let p = new Promise((resolve, reject) => {
  console.log("Promise1");
  resolve();
});
p.then(() => {
  console.log("Promise2");
});
// 输出 Promise1,Promise2,setTimeout1

Promise 参数中的 Promise1 是同步执行的 其次是因为 Promise 的 then()是 microtasks,会在同步任务执行完后会去清空 microtasks queues, 最后清空完微任务再去宏任务队列取值

再来看看这个

function testQueue() {
  // part1
  Promise.resolve().then(() => {
    console.log("Promise1");
    setTimeout(() => {
      console.log("setTimeout2");
    }, 0);
    Promise.resolve().then(() => {
      console.log("Promise3");
    });
  });
  // part2
  console.log("begin");
  // part3
  setTimeout(() => {
    console.log("setTimeout1");
    Promise.resolve().then(() => {
      console.log("Promise2");
    });
  }, 0);
  // part4
  setTimeout(() => {
    console.log("s1");
    Promise.resolve().then(() => {
      console.log("p2");
    });
  }, 0);
}
testQueue();
// 浏览器环境
// begin,Promise1,Promise3,setTimeout1,Promise2,s1,P2,setTimeout2

// node环境
// begin,Promise1,Promise3,setTimeout1,s1,Promise2,p2,setTimeout2

浏览器环境执行过程:

  1. 第一轮,主线程有 part2 先执行即打印 begin,并注册一个微任务 part1,两个宏任务 part3,part4
  2. 第二轮,主线程为空了,开始执行微任务即 part1,在微任务执行过程中遇到的所有微任务都执行,即打印 Promise1,Promise3,并注册下一轮宏任务 setTimeout
  3. 第三轮,开始执行第一轮注册的宏任务,即先执行 part3,打印 setTimeout1,并执行遇到的所有微任务,即再打印 Promise2,继续执行第一轮注册的宏任务,即打印 s1,并执行遇到的微任务即打印 p2
  4. 第四轮,在上一步已经执行完了第一轮注册的宏任务,这次开始执行第二轮注册的宏任务,即打印 setTimeout2

node 环境执行过程:

  1. 第一轮,主线程有 part2 先执行即打印 begin,并注册一个微任务 part1,两个宏任务 part3,part4
  2. 第二轮,主线程为空了,开始执行微任务即 part1,在微任务执行过程中遇到的所有微任务都执行,即打印 Promise1,Promise3,并注册下一轮宏任务 setTimeout
  3. 第三轮,开始执行第一轮注册的宏任务,即先执行 part3 并打印 setTimeout1,并注册下一轮微任务 Promise2,然后再执行第一轮注册的宏任务 part4,即打印 s1,并注册下一轮的微任务 p2。
  4. 第四轮,在第二轮没有微任务,在第三轮注册两个微任务,因此依次执行并打印 Promise2,p2
  5. 第五轮,上一轮把所有微任务执行完了,最后打印 setTimeout2
var p = Promise.resolve();
p.then(function() {
  p.then(function() {
    console.log("C");
  });
  setTimeout(() => {
    console.log("D");
    p.then(() => {console.log('end')})
  });
  console.log("A");
});
p.then(function() {
  console.log("B");
});

// A B C D end

一个 Promise 决议后,这个 Promise 上所有的通过 then(..) 注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。这里,”C” 无法打断或抢占 “B”,这是因为 Promise 的运作方式。

事件循环队列类似于一个游乐园游戏:玩过了一个游戏之后,你需要重新到队尾排队才能再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。一个任务可能引起更多任务添加到同一个任务队列中,所以,理论上任务循环可能会无限循环。。。每次事件循环可理解为 tick,如果遇到任务则属于当前 tick(类似 setTimeout(fn,0)),只有事件才属于下一轮 tick

这对上面的例子,首先会注册 A,B 任务,然后打印 A、B,执行 A 的时候注册新的任务 C,C 属于任务,因此为在当前 tick 执行,而 D 属于事件,需要在下一个 tick 执行

var p3 = new Promise(function(resolve, reject) {
  resolve("B");
});
var p1 = new Promise(function(resolve, reject) {
  resolve(p3);
});
p2 = new Promise(function(resolve, reject) {
  resolve("A");
});
p1.then(function(v) {
  console.log(v);
});
p2.then(function(v) {
  console.log(v);
});

// A B
// ,p1 不是用立即值而是用另一个 promise p3 决议,
// 后者本身决议为值 "B"。规定的行为是把 p3 展开到 p1,但是是异步地展开。
// 所以,在异步任务队列中,p1 的回调排在 p2 的回调之后

注意:把 p3 展开到 p1 是异步地展开

function testQueue() {
  Promise.resolve().then(() => {
    console.log("Promise1");
    Promise.resolve().then(() => {
      console.log("Promise3");
    });
  });
  console.log("begin");

  Promise.resolve().then(() => {
    console.log("p2");
  });
}
testQueue();
// 浏览器环境
// begin,Promise1,p2,Promise3

注意,虽然执行 Promise1 时,遇到微任务 Promise3,但只是在任务队里插入新的微任务,期前面还有 p2。。。因此先执行 p2

综上:在浏览器和 node 环境下,事件循环处理的机制不同。前者在执行宏任务或微任务过程中,如果遇到新注册的微任务则全部执行。而在 node 环境下,如果有同一循环下注册的宏任务,则先执行这些宏任务,然后再去执行微任务

node 环境下,还有 process.nextTick,是在当前执行栈的末尾执行,意味着要早于宏任务或微任务

function test() {
  setTimeout(() => {
    console.log("setTimeout");
  });

  process.nextTick(() => {
    console.log("process.nextTick");
  });

  new Promise((resolve, reject) => {
    console.log("resolved");
    resolve();
  }).then(() => {
    console.log("then");
    setTimeout(() => {
      console.log("then-setTimeout");
      Promise.resolve().then(() => {
        console.log("setTimeout-then");
      });
    });
    Promise.resolve().then(() => {
      console.log("then then");
    });
  });

  console.log("begin");
}
test();
// resolved,begin,process.nextTick,then,then then,setTimeout,then-setTimeout,setTimeout-then

执行过程:

  1. 第一轮,注册宏任务 setTimeout,主线程上 Promise 实例化立即执行即打印 resolved,并注册微任务,然后打印 begin,process.nextTick 在当前主线程尾部执行,因此再打印 process.nextTick
  2. 第二轮,上一轮是主线程,接下来该执行微任务队列了,即打印 then,然后遇见微任务,则一并执行即打印 then then,同时注册宏任务
  3. 第三轮,再执行第一轮注册的宏任务,即打印 setTimeout
  4. 第四轮,因为没有微任务,因此执行第二轮注册的宏任务,因此打印 then-setTimeout,然后没有其他宏任务了,因此打印 setTimeout-then

知道了上面的理论,再看看下面的

function test() {
  // part1
  setTimeout(() => {
    console.log("setTimeout1");
    Promise.resolve().then(() => {
      console.log("setTimeout1 Promise1");
    });
  }, 0);

  // part2
  Promise.resolve().then(() => {
    process.nextTick(() => {
      console.log("Promise1 nextTick");
    });
    console.log("Promise1");
    setTimeout(() => {
      console.log("setTimeout3");
      process.nextTick(() => {
        console.log("setTimeout3 Promise1 nextTick");
      });
    }, 0);
  });

  // part3
  process.nextTick(() => {
    console.log("nextTick");
  });

  // part4
  setTimeout(() => {
    console.log("setTimeout2");
    Promise.resolve().then(() => {
      console.log("setTimeout2 Promise2");
    });
  }, 0);
}
test();
// nextTick
// Promise1
// Promise1 nextTick
// setTimeout1
// setTimeout2
// setTimeout1 Promise1
// setTimeout2 Promise2
// setTimeout3
// setTimeout3 Promise1 nextTick
function testQueue() {
  console.log("1");

  setTimeout(function() {
    console.log("2");
    new Promise(function(resolve) {
      console.log("4");
      resolve();
    }).then(function() {
      console.log("5");
    });
    process.nextTick(function() {
      console.log("3");
    });
  });

  process.nextTick(function() {
    console.log("6");
  });

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

  setTimeout(function() {
    console.log("9");
    process.nextTick(function() {
      console.log("10");
    });
    new Promise(function(resolve) {
      console.log("11");
      resolve();
    }).then(function() {
      console.log("12");
    });
  });
}
testQueue();
// 因为有process.nextTick,因此需要node环境下打印
// procee.nextTick是当前执行栈的末尾,意味着若前面有其他级别更高的代码,则优先执行级别高的
// 1,7,6,8,2,4,9,11,3,10,5,12

// 原文答案
// 1,7,6,8,2,4,3,5,9,11,10,12

再来回顾一下浏览器环境

function test() {
  setTimeout(function() {
    console.log(1);
    new Promise(function(resolve) {
      console.log(3);
      setTimeout(function() {
        console.log(5);
      });
      resolve();
    }).then(function() {
      console.log(4);
    });
  });

  Promise.resolve().then(function() {
    console.log(9);
    setTimeout(function() {
      console.log(10);
    });
  });

  setTimeout(function() {
    console.log(2);
    new Promise(function(resolve) {
      console.log(6);
      setTimeout(function() {
        console.log(7);
      });
      resolve();
    }).then(function() {
      console.log(8);
    });
  });
}
// 9,1,3,4,2,6,8,10,5,7
async function async1() {
	console.log('async1 start');
	await async2();
	console.log('async1 end');
}
// 等价于
async function async1() {
	console.log('async1 start');
	Promise.resolve(async2()).then(() => {
    console.log('async1 end');
  })
}

//
let a = 0
let b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1

// 打印顺序
'1' 1
'2' 10
  1. 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generatorgenerator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  2. 因为 await 是异步操作,后面的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  3. 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。

多个script

<script>
  // 多个script标签也是按模块进行执行,不会交叉执行(定时器除外)
  // 最终打印1 3 4 5 6 s 2
  var p = new Promise( ( resolve, reject ) => {
    console.log( '1' );
    setTimeout( () => {
      console.log( 's' );
      Promise.resolve().then( () => {
        console.log( 2 );
      } )
    } )
    // 如果不改变状态,then方法则不会执行
    resolve()
  } )
  console.log( '3' )
  p.then( () => {
    console.log( '4' )
  } )
  p.then( () => {
    console.log( '5' )
  } )
</script>

<script>
  // 多个script标签加载,除了定时器,其他的代码也是按模块执行,
  // 也就是前一个script执行完才会执行这个
  console.log( '6' );
</script>
最近的文章

Promise解析

一直以来,自己都对 promise 的理解模模糊糊,接下来我想仔细研究一番1、Promise 含义Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 对象有以下两个...…

继续阅读
更早的文章

About git

参考:阮一峰-常用 git 命令操作参考:阮一峰-远程操作详解参考:阮一峰-工作流SSH为 Secure Shell 的缩写,是一种网络协议,用于计算机之间的加密登录,如果一个用户从本地计算机,使用ssh协议登录另一台远程计算机,我们可以认为这种登录是安全的,即使被中途截获,密码也不会泄露。ssh之所以安全,因为他采用公钥加密过程: 远程主机收到用户的登录请求,把自己的公钥发给用户, 用户使用这个公钥,将登陆密码加密后,发送给远程主机, 远程主机用自己的私钥,解密登录密码,如果密码正...…

继续阅读