同步和异步

同步代码意味着主线程要一步一步执行代码,遇到函数调用等操作的时候就要等待调用全部结束返回才继续执行下面的代码。

异步代码意味着主线程执行到异步代码后,无需等待结果产生,而是可以继续执行下面的内容。

JavaScript事件循环

js运行时的内存结构理论模型:

image-20221108112822614

栈是类似于C++的函数栈,堆中存储这对象,队列中是绑定了回调函数的消息。

js引擎驱动着一个主线程运行着事件循环:

image-20221108113125086

整个js脚本的执行会被作为第一个宏任务执行,然后每个事件循环中会从宏任务队列中选取一个执行,并且执行完所有产生的微任务。如果是在浏览器中,接下来会去进行页面渲染,一个循环就结束了,马上进入下一个循环……

JavaScript的单线程

单线程指的是对于程序员写的代码,运行在JS引擎中的时候是采用单个主线程运行的。但是setTimeOut、axios、ajax等操作还是会启用新线程处理的,不然js还是无法做到真正的同时执行代码。但是js保证了这些任务线程不会和主线程共用资源,资源分别隔离。

既然只有一个线程,js是如何实现异步的呢?重新来看异步的含义,表示执行异步操作的时候,不在乎其执行的结果是否产生就可以继续向下执行,而结果产生可以由新的任务线程执行来完成。例如setTimeOut函数中,会将要执行的函数先放入事件表中(浏览器或者nodejs等),然后会有单独的任务线程管理这个事件,如果延迟时间到了,就会把这个事件加入到任务队列中,等到当前事件循环结束执行下一个循环的时候,就会把任务中的这个消息取出,执行绑定的回调函数。

js总是在当前栈清空的时候才认为一个任务执行结束,开始检查队列准备执行下一个任务。因此setTimeOut函数的延迟即使为零,也会比当前脚本其他同步语句执行得晚。

Promise

js中的Promise是异步任务的一个重要实现方式。promise中会提供一个resolve函数和reject函数,resolve的含义是当前promise要兑现,返回一个结果出去;reject则表示当前promise执行的任务失败,抛出失败异常和结果。promise的声明就意味着,该函数可能不能立即返回结果,返回结果之后再调用回调函数完成后续操作。尽管如此,promise中的函数操作还是在调用的时候被立即执行,这和其结果什么时候返回不确定是不矛盾的。就算结果不能马上返回,主线程依然会继续执行下面的代码。

例如:

const myPromise = new Promise((resolve, reject) => {
    // 构造时,setTimeout操作会执行,但是该promise不会马上返回结果,而是会后来再返回结果'foo'
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

返回结果的时候,会触发通过then绑定的回调函数:

myPromise
  .then(value => { return value + ' and again'; })
  .then(value => { console.log(value) })
  .catch(err => { console.log(err) });

依照前面的描述,promise的回调会放在微任务队列中,也就是会在当前事件循环中就被全部执行,新产生的也会立刻准备执行。

async/await

都是语法糖。

实际上通过async声明函数后,调用该函数时就是new了一个promise对象。可以理解为async处声明了一个普通函数,调用的时候new Promise了一下

await只能在async声明的函数中使用。await则是把该函数的剩余语句都包装在await的promise的回调函数中了。当然await后面也可以不是promise,那就会立刻计算返回表达式的值(注意:这种情况后面的语句还是会被包装到then中,作为微任务加入队列。

可以看看await、async、普通函数组合起来会有什么效果。

await + async

async function main() {
    async function foo() {
        console.log("hello")
        return "foo"
    }
    ans = await foo()
    console.log(ans)
}
main()
console.log("over")

// 打印结果
// hello
// over
// foo

await + 普通函数。后面的语句也会被放在then中,over之后最后才会执行。

async function main() {
    function foo() {
        console.log("hello")
        return "foo"
    }
    ans = await foo()
    console.log(ans)
}
main()
console.log("over")

// 打印结果
// hello
// over
// foo

普通调用+async:

async function main() {
    async function foo() {
        console.log("hello")
        return "foo"
    }
    ans = foo()
    console.log(ans)
}
main()
console.log("over")

// 打印结果
// hello
// Promise { 'foo' }
// over

案例分析

"use strict";
const axios = require('axios');

async function get(url) {
    console.log(`begin: ${url}`);
    let response = await axios.get(url);
    console.log(`end: ${url} result: ${response.data}`);
}

// 异步请求
async function maina() {
    let begin_time = new Date().getTime();
    let all_request = []
    for (var i = 10; i >= 0; i--) {
        all_request.push(get('https://ustcsoc.cn/api/health'));
    }
    await Promise.all(all_request);
    let execute_time = new Date().getTime() - begin_time;
    console.log(`execute ${execute_time} ms`);
    // 或者
    //Promise.all(all_request).then(() => {
    //    let execute_time = new Date().getTime() - begin_time;
    //    console.log(`execute ${execute_time} ms`);
    //})
}

// 模拟同步
async function main() {
    let begin_time = new Date().getTime();
    for (var i = 100; i >= 0; i--) {
        await get('https://ustcsoc.cn/api/health');
    }
    const execute_time = new Date().getTime() - begin_time;
    console.log(`execute ${execute_time} ms`);
}

maina();

用异步请求完成对api的多次请求,最终执行时间等于模拟同步的1/10.