聊聊异步

javascript 语言中有三座大山:this 作用域 、原型对象以及异步。异步是相对比较复杂的概念,当然它也非常非常的重要,今天我们就来聊一聊异步。

javascript 是单线程语言,意味着同一时间只能做一件事,仅仅是这样就够了吗?

如果我们发起一个长时间的网络请求,它就那么傻傻的等着;整个代码就会被阻塞,那就玩完了,所以我们需要异步来保证整个流程的顺畅。

为了更加清楚的讲明白 javascript 中异步概念,我先抛出几个问题:

1. setTimeout、Promise、Ajax 这些执行代码中回调函数的调用顺序?
2. setTimeout 定时器中回调函数的真实的延时时间是多少?
3. Promise、async/await 的执行顺序?

在分析上述问题之前,先介绍几个概念。

「JS 引擎」javascript 引擎是一个专门处理 javascript 指令码的虚拟机器,一般会附带在网页浏览器中【维基百科】。以chrome 中为例,就是 V8 引擎。

「执行栈」一个记录函数调用的数据结构。当函数被调用时,会被 push 进栈顶;执行完返回时,从栈顶 pop 出。javascript 主线程中只有一个执行栈,负责顺序执行主线程中代码;当主线程中代码执行完,会去执行消息队列中的内容。简单理解就是同步代码执行时执行上下文存在的空间。 Eg. 我们可以在 chrome 中通过打断点来追踪 javascript 代码的执行过程

javascript 代码错误栈

「队列」javascript 代码在执行时会包含一个消息队列。当有外部的异步事件 (setTimeout、ajax 等请求) 时,相应的回调函数会按照先后顺序存放在消息队列中。简单理解就是异步事件返回结果时按先后顺序排列所形成的回调函数队列。

Tip:执行栈、堆(一个非结构化内存区域,用来存放变量、对象)、队列构成 js 并发模型


「任务」ES6 之前任务比较简单只有 setTimeout / ajax 这类 web api 生成的异步事件,所有的这些内容都会被存放到事件队列中,我们称之为异步任务。后来 ES6 中引入了 Promise 之后,异步任务之间存在差异,执行的优先级也有区别。分为两类:微任务和宏任务。

宏任务:setTimeout、setInterval 微任务:Promise、async / await

「事件循环」针对上述对于任务类型划分的不同,事件循环♻️也有所区别。(主要针对浏览器中事件循环)

  • 细分前 —— javascript 引擎遇到异步事件之后,会通过事件循环线程通知相应的 web 中工作线程执行异步事件;javascript 主线程继续向后执行,等到 web 中工作线程执行完之后,事件循环线程会将异步事件的回调函数添加到队列中;等到 javascript 主线程执行完之后,就依次执行队列中的回调函数。如此反复形成一个无限的循环被称为“事件循环” 。

  • 细分后 —— javascript 引擎遇到异步事件时,会区分事件类型决定是将它放到宏任务队列还是微任务队列。当 javascript 主线程执行完之后,会先去查看微任务队列是否还有未执行的事件。如果存在会依次执行微任务直至为空,然后再去执行宏任务队列中事件;如果微任务队列没有未执行事件,直接去宏任务队列中执行;形成一个新的执行栈。如此反复形成循环。

* 微任务永远是宏任务执行过程中产生的。 * 浏览器一次循环:宏任务 -> 微任务 -> GUI 渲染 ——>宏任务 -> 微任务 -> GUI 渲染 …….


大家先记住上面????的一些概念。接下来通过三个问题来梳理下:

【1】setTimeout、Promise、Ajax 这些执行代码中回调函数的调用顺序?

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

上述输出结果:

script start
script end
promise1
promise2
setTimeout

分析:

第一步:执行同步代码先输出 script start,遇到 setTimeout 将它交到 web api 的异步线程中处理,0ms后进入到消息队列作为下一次宏任务。javascript 主线程接着执行 Promise 遇到 then 之后生成对应的微任务推到微任务队列中,继续执行输出 script end
第二步:主线程执行完查看微任务队列,发现有两个微任务执行输出 promise1、promise2
第三步:微任务队列执行完之后就去查看消息队列,发现还有一个 setTimeout 的宏任务未执行,直接执行该宏任务输出 setTimeout

同时存在下面的代码 :

const XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; // node 中模拟浏览器环境,提供 ajax 请求所需对象
const fetch = url => {
  const obj = new XMLHttpRequest();
  obj.open('GET', url);
  obj.onreadystatechange = () => {
    if (obj.readyState === 4) {
      console.log('success!');
    }
  };
  obj.send();
};
setTimeout(() => {
  console.log('setTimeout');
}, 100);
fetch('http://xxx.xxx.xxx.xxx:xxxx/mock/5c823b3f95b2682cbab895de/staff/ui/manage/usercommissionlist');

上述执行结果不确定。如果网络请求响应的时间比 setTimeout 延迟长,那么先输入 success;否则先输出 setTimeout。 分析:

第一步:执行同步代码进行变量定义,遇到 setTimeout 之后交给 web api 的定时器线程执行,异步线程100ms后将回调函数推到消息队列;同时主线程会继续执行 fetch 操作,因为是异步所以被推到 web api 的 ajax 处理线程,等到网络有返回后将 onreadystatechange 后的回调函数放到消息队列中
第二步:主线程代码执行完毕之后发现微任务队列为空,接着去执行消息队列中第一个事件。这里 setTimeout 和 fetch的回调函数谁会被先推到消息队列呢?这个跟 fetch 的网络请求时间强相关,如果网络请求时间低于100ms,那么输出结果 success setTimeout,否则输出 setTimeout success

【2】setTimeout 定时器中回调函数的真实的延时时间是多少?

// 定义一个耗时操作
function syncCode(timeout) {
  const last = new Date().getTime();
  while (true) {
    const now = new Date().getTime();
    if (now - last > timeout) {
      break;
    }
  }
}
const XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; // node 中模拟浏览器环境,提供 ajax 请求所需对象
const fetch = url => {
  const obj = new XMLHttpRequest();
  obj.open('GET', url);
  obj.onreadystatechange = () => {
    if (obj.readyState === 4) {
      syncCode(300); // 执行一个300ms的同步代码
    }
  };
  obj.send();
};
setTimeout(() => {
  console.log('setTimeout');
}, 1000);
// 假设 fetch 网络请求耗时800ms
fetch('http://xxx.xxx.xxx.xxx:xxxx/mock/5c823b3f95b2682cbab895de/staff/ui/manage/usercommissionlist');
syncCode(900) // 执行一个900ms的同步代码

这里有个前提假设 fetch 的网络请求响应时间是800ms,那么从代码执行到最终输出 setTimeout 一共花了1200ms 。????

整个过程跟之前分析的类似。只不过要注意 fetch 操作中回调函数经过800ms后被推倒消息队列,setTimeout 操作经过1000ms被推到消息队列;与此同时主线程的代码执行900ms后才会去执行消息队列中代码。按时间线分析:

  • 800ms时 :fetch 回调被推到消息队列,setTimeout 在 web 的定时器线程中,主线程在执行同步代码(耗时900ms)

  • 900ms时 :主线程执行完同步代码,准备执行 fetch 中异步回调函数,这时 setTimeout 还在 web 中的定时器线程中

  • 1000ms时:主线程执行 fetch 中异步回调函数,这时 setTimeout 中回调函数被推到消息队列

  • 1200ms时:主线程执行完 fetch 中异步回调函数;准备执行消息队列中下一个回调函数 setTimeout

所以整体上输出 setTimeout 需要1200ms。(这里忽略细微的代码执行耗时)

【3】Promise、async/await 的执行顺序?

针对异步,经常也有一些 Promise 和 async 对比的延伸题目。大家搞清两点:

async/await 函数如何转化成 Promise 的函数
V8 引擎针对 async/await 语法进行的优化

const p = new Promise(function(resolve) {
  resolve('test');
});

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
(async () => {
  const result = await p;
  console.log(`Async:${result}`);
})();
p.then((result) => console.log(`Promise:${result}`));
console.log('script end');

上面????这段代码

// 在 node(v8.0.0)中输出结果:
script start
script end
Async:test
Promise:test
setTimeout

// 在 node(v10.15.3)中输出结果
script start
script end
Promise:test
Async:test
setTimeout

// 在 node(v12.2.0)中输出结果
script start
script end
Async:test
Promise:test
setTimeout

上述除了 Promise 和 async 执行顺序有区别之外,其他都一样。在分析 Promise 和 async/await 执行顺序不同之前先大概分析下过程:

// 其中:
async () => {
  const result = await p;
  console.log(`Async:${result}`);
}
// 可以换换成
() => {
  return RESOLVE(p).then((result) => console.log(`Async:${result}`))
}

第一步:执行同步代码定义 Promise 新对象,接着输出 script start,遇到 setTimeout 推到消息队列,然后执行 async 函数和 Promise 函数,推到微任务队列,最后输出 script end
第二步:执行完 script end 之后,本次主线程执行栈为空,查看并执行微任务队列。这里在 node 的 v8 版本和 v12 版本中执行顺序 async 在前 promise 在后;换到 node 的 v10 版本之后 promise在前 async 在后。这里存在不同,具体接下来分析。不管它们顺序如何都属于微任务队列;执行完微任务队列之后执行下一个宏任务
第三步:主线程执行宏任务也就是 setTimeout 注册的回调函数,输出 setTimeout 代码执行完成✅

我们接下来分析微任务队列中 async 和 Promise 执行顺序不同的原因。

const p = new Promise(function(resolve) {
  resolve('test');
});

(async () => {
  const result = await p;
  console.log(`Async:${result}`);
})();
p.then((result) => console.log(`Promise:${result}`));

其中 p 是一个 Promise,这个 Promise 已经被 resolve 为确定结果了。V8 引擎的主线程在执行的时候遇到 async 会将 p 进行一层 Promise 包裹,即使 p 是一个已经确定执行结果的 Promise。将后续的 console.log(`Async:${result}`) 推到微任务队列,继续执行 p.then() 的操作。由于 p 已经执行完所以直接执行后续的回调函数输出

Promise:test
Async:test

那么为啥 node(v8.0.0) 中执行会不一致呢?这是因为 Node.js 8 中引入了一个 bug,在某些时候会让 await 直接跳过一些微任务,导致这里 await 后的回调直接执行。后续 Node 10 中又修复了这个 bug。Node 10 中修复完这个 bug 之后的结果跟我们想象中的可能不太一样。我们理解 async / Promise 只是写法上不一样都是产生微任务,遇到结果确认的 Promise 应该直接执行,顺序输出 Async:test 和 Promise:test。

这就跟 V8 引擎针对 await 做的处理有关系。

1. 首先 async/await 函数中永远返回的是一个 Promise;所以 V8 会创建一个所谓的 implicit_promise,用于对结果进行一层 Promise 封装
2. await 后的值会被转为 Promise,不管你本身是不是 Promise
3. await 后面执行的处理函数会绑定到这个 Promise上,用于 await 后的 Promise 有结果了恢复主函数执行
4. 最后挂起异步函数并返回 implicit_promise

类似这样的处理

其中所做的优化主要是:

1. 如果 await 后的 p 是一个 Promise 就不重复再包裹,只针对非 Promise 进行 Promise 包裹。对于 await 的值已经是Promise的情况避免了再次创建 Promise,同时从最少三个微任务减少到只有一个;类似于 Node 8 的 bug 所做的
2.干掉了 throwaway。这个可用可不用,当时只是为了满足 performPromiseThen 中操作规范而已

这就是 Node 12 部分所做的优化,所以执行顺序为 Async:test 和 Promise:test 。

转载至:https://zhuanlan.zhihu.com/p/66015359

作者:jsdchenye

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值