【运维SaaS开发前端经验分享】深入解析JS的异步机制

318 阅读7分钟

JavaScript定义

JavaScript 是一种单线程编程语言,这意味着同一时间只能完成一件事情。也就是说,JavaScript 引擎只能在单一线程中处理一次语句。

  • 优点:单线程语言简化了代码编写,因为你不必担心并发问题,但这也意味着你无法在不阻塞主线程的情况下执行网络请求等长时间操作。
  • 缺点:当从 API 中请求一些数据。根据情况,服务器可能需要一些时间来处理请求,同时阻塞主线程,让网页无法响应。

异步运行机制

CallBack,setTimeOut,ajax 等都是通过事件循环(event loop)实现的。

1.什么是Event Loop?

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

2.流程整体示意图

在这里插入图片描述

3.总结异步运行到整体机制

主线程在运行的时候,将产生堆(heap)和栈(stack),栈中的代码会调用各种外部API,它们将在”任务队列”中根据类型不同,分类加入到相关任务队列中,如各种事件等。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,根据任务队列的优先级依次执行那些事件所对应的回调函数。这就是整体的事件循环。

4.任务队列的优先级

微任务队列中的所有任务都将在宏队列中的任务之前执行。也就是说,事件循环将首先在执行宏队列中的任何回调之前清空微任务队列。

举例:

console.log('Script start');

        setTimeout(() => {
console.log("setTimeout 1");
        }, 0);

        setTimeout(() => {
console.log("setTimeout 2");
        }, 0);

new Promise ((resolve, reject) => {
            resolve("Promise 1 resolved");
        })
        .then(res => console.log(res))
        .catch(err => console.log(err));

new Promise ((resolve, reject) => {
            resolve("Promise 2 resolved");
        })
        .then(res => console.log(res))
        .catch(err => console.log(err));

console.log('Script end');

运行结果是:

Script start
Script end
Promise 1
Promise 2
setTimeout 1
setTimeout 2

通过上述例子可以看到无论宏队列的位置在何方,只要微队列尚未清空,一定会先清空微队列后,在去执行宏队列。下面介绍微队列任务中比较典型的几个API,通过相关举例,让你更深入理解JS的异步机制。


微任务队列

1.Promise(ES6)

Promise,就是一个对象,用来传递异步操作的消息。

基础用法:

var promise = new Promise(function(resolve, reject) {//异步处理逻辑//处理结束后,调用resolve返回正常内容或调用reject返回异常内容 }) promise.then(function(result){//正常返回执行部分,result是resolve返回内容 }, function(err){//异常返回执行部分,err是reject返回内容 }) .catch(function(reason){//catch效果和写在then的第二个参数里面一样。另外一个作用:在执行resolve的回调时,如果抛出异常了(代码出错了),那么并不过报错卡死JS,而是会进入到这个catch方法中,所以一般用catch替代then的第二个参数 });

  • 缺点:无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。再次,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
  • 优点:Promise能够简化层层回调的写法,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。

用法注意点 - 顺序:

new Promise((resolve, reject) => {
            resolve(1);
console.log(2);
        }).then(r => {
console.log(r);
        });

运行结果是:

2
1

说明:立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。也就是resolve(1)和console.log(2)是属于同步任务,需要全部执行完同步任务后,再去循环到resolve的then中。

用法注意点 - 状态:

const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000); });const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000); });const p3 = new Promise(function (resolve, reject) { setTimeout(() => resolve(new Error('fail')), 1000); }); p2 .then(result => console.log("1:", result)) .catch(error => console.log("2:",error)); p3 .then(result => console.log("3:", result)) .catch(error => console.log("4:",error));

运行结果是:

3: Error: fail
at setTimeout (async.htm:182)
2: Error: fail
at setTimeout (async.htm:174)

说明:p1是一个 Promise,3 秒之后变为rejected。p2和p3的状态是在 1 秒之后改变,p2 resolve方法返回的是 p1, p3 resolve方法返回的是 抛出异常。但由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。而p3返回的是自身的resolve,所以触发then中指定的回调函数。

用法注意点 - then链的处理

var p1 = p2 = new Promise(function (resolve){
            resolve(100);
        });

        p1.then((value) => {
return value*2;
        }).then((value) => {
return value*2;
        }).then((value) => {
console.log("p1的执行结果:",value)
        })

        p2.then((value) => {
return value*2;
        })
        p2.then((value) => {
return value*2;
        })
        p2.then((value) => {
console.log("p2的执行结果:",value)

运行结果是:

p2的执行结果:100
p1的执行结果:400

说明:p2写法中的 then 调用几乎是在同时开始执行的,而且传给每个 then 方法的 value 值都是 100。而p1中写法则采用了方法链的方式将多个 then 方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then 的顺序执行,并且传给每个 then 方法的 value 的值都是前一个promise对象通过 return 返回的值。

用法注意点 - catch的处理:

var p1 = new Promise(function (resolve, reject){
            reject("test");
//throw new Error("test");  效果同reject("test");
//reject(new Error("test")); 效果同reject("test");
            resolve("ok");
        });
        p1
        .then(value => console.log("p1 then:", value))
        .catch(error => console.log("p1 error:", error));

        p2 = new Promise(function (resolve, reject){
            resolve("ok");
            reject("test");
        });
        p2
        .then(value => console.log("p2 then:", value))
        .catch(error => console.log("p2 error:", error));

运行结果是:

p2 then: ok
p1 error: test

说明:Promise 的状态一旦改变,就永久保持该状态,不会再变了。不会即抛异常又会正常resolve。

2.async/await(ES7)

async基础用法:

async 用于申明一个 function 是异步的,返回的是一个 Promise 对象。

async function testAsync() {
return "hello async";
        }

var result = testAsync();
console.log("1:", result);

        testAsync().then(result => console.log("2:", result));

async function mytest() {
//"hello async";
        }
var result1 = mytest();
console.log("3:", result1);

运行结果是:

1: Promise {<resolved>: “hello async”}
3: Promise {<resolved>: undefined}
2: hello async

说明:async返回的是一个Promise对象,可以用 then 来接收,如果没有返回值的情况下,它会返回 Promise.resolve(undefined),所以在没有 await 的情况下执行 async 函数,它会立即执行,并不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

await基础用法:

await 只能出现在 async 函数中,用于等待一个异步方法执行完成(实际等的是一个返回值,强调 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果)。

function getMyInfo() {
return Promise.resolve("hello 2019!");
        }

async function testAsync() {
return "hello async";
        }

async function mytest() {
return Promise.reject("hello async");
        }

async function test() {
try {
const v1 = await getMyInfo();
console.log("getV1");

const v2 = await testAsync();
console.log("getV2");

const v3 = await mytest();
console.log(v1, v2, v3);

            } catch (error) {
console.log("error:", error);
            }
        }
        test();

运行结果是:

getV1
getV2
error: hello async

说明:await等到的如果是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。 放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

async/await的优势:

很多情况下,执行下一步操作是需要依赖上一步的返回结果的,如果当嵌套层次较多的时候,(举例3层的时候):

const getRequest = () => {
return promise1().then(result1 => {
//do something
return promise2(result1).then(result2 => {
//do something
return promise3(result1, result2)
                })
            })
        }

从上例可以看到嵌套内容太多。此时如果用async写法,可写成如下:

const getRequest = async () => {
const result1 = await promise1();
const result2 = await promise2(result1);
return promise3(result1, result2);
        }

说明:async / await 使你的代码看起来像同步代码,它有效的消除then链,让你的代码更加简明,清晰。


蓝鲸智云

本文由腾讯蓝鲸智云编辑发布,腾讯蓝鲸智云(简称蓝鲸)软件体系是一套基于PaaS的技术解决方案,致力于打造行业领先的一站式自动化运维平台。目前已经推出社区版、企业版,欢迎体验。