面试之JavaScript系列——异步编程(三座大山之一)

158 阅读4分钟

概要: JavaScript是一门单线程语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就会阻塞,而实现单线程非阻塞的方法就是事件循环。js这种单线程事件循环模型中,同步操作与异步操作是事件循环所要依赖的核心机制。

简介

异步编程 是为了优化因计算量大而时间长的操作。比如,张三上js基础网课,这件事中,因为上网课是实时同步的,而张三做练习是随时异步的,张三为了不打乱同步上网课的节奏,把异步做练习的操作,放在同步上完网课之后。这样一来,张三上js基础网课的事件顺序就是先同步上网课,后异步做练习。

图解

JS事件循环.jpg

  • 蓝框:所有的同步代码先放在代码调用栈中(call stack),按从上到下的循序前后执行;

  • 红框:异步代码则全放在挂起区(macrotask宏任务:主要为DOM/BOM操作和API是脱离语法层面的、microtask微任务:主要为Promise/aysnc/await是es语法的范畴)

  • 黄框:先把微任务放进事件循环(event loop),循环进入代码调用栈中调用,再DOM渲染,最后宏任务

微宏任务

FE2theMax_2_5267965404038319067.jpeg

注意:

  1. 如果回调时间相同,按先微任务、中DOM渲染、后宏任务的顺序调用执行

  2. 如果微任务中有两个Promise同时回调,那按代码先后循序,从上往下执行

  3. 若是调用Promise()或者async function(){}则先走里面的同步函数,遇到then、await就会挂起(因为堵塞后面的代码),直接往下执行,执行完后再按前后循序回调

习题

一、 FE2theMax_2_1831701644062975216.jpeg 二、

function eventLoop() {
            async  function async1() {
                console.log('async1 start')//2
                await  0//异步阻塞,挂起去代码顺序3
                async2()//执行函数6
                console.log('async1 end')//7
            }
 
            async  function async2() {
                console.log('async2')//6
            }
            
            // 代码顺序1
            console.log('script start')//1
 
            setTimeout(function  () {
                console.log('setTimeout')//9
            }, 0)
            
            // 代码顺序2
            async1()//执行函数2
            
            // 代码顺序3
            new  Promise(
                function  (resolve) {//执行函数3
                    console.log('promise1')//3
                    resolve()//履约往下
                    console.log('promise1.1')//4
                }
            )
                .then(function  () {//异步阻塞,挂起去代码顺序4
                    console.log('promise2')//8
                })
             
            // 代码顺序4,执行完后在函数体内从上往下回看"挂起"
            console.log('script end')//5
        }

异步编程常用于ajax网络请求、setTimeout定时函数、Promise(then/catch/finally)对象、async/await异步函数(es7的语法,是Promise的语法糖,不用再写then/catch/finally)


Promise

简介

以往的异步编程通常需要深度嵌套的回调函数,很容易产生“回调地狱”,这种情况理解起来很复杂,不利于代码维护。

// 1+2+3+4 + ... + 100
    ajax({
        url"./mathserver.php",
        data: { a1, b2 },
        success(result) {
            console.log(result);
            ajax({
                url"./mathserver.php",
                data: { a: result, b3 },
                success(result) {
                    console.log(result);
                    ajax({
                        url"./mathserver.php",
                        data: { a: result, b4 },
                        success(result) {
                            console.log(result);
                            ajax({
                                url"./mathserver.php",
                                data: { a: result, b5 },
                                success(res){
                                    console.log(res);
                                }
                            })
                        },
                    });
                },
            });
        },
    });
    console.log("你妹");

因此,Promise承诺/契约/期约应运而生,一种异步编程的解决方案,是es6的新语法。

优点

  1. 链式操作降低了编码难度(Promise().then().catch().finally())

  2. 代码可读性提高

  3. 代码可维护性增强

状态

  1. pending待定挂起态:初始状态

  2. fulfilled兑现履约态:有时候也称为“resolved解决”,操作成功完成,HTTP响应状态码在200-299

  3. rejected拒绝毁约态:操作失败

改变Promise的状态

控制期约状态的转换是通过调用resolve()履约和reject()毁约,这两个函数来实现。

写法

// es6 promise
    Promise()
        // 履约走then,value为履约的返回值
        .then(value=>console.log(value))
        // 捕捉回调链中任意环的报错/毁约,直接在这打印错误
        .catch(error=>console.log(error))
        // 不管执行过程是否报错,最后必执行
        .finally(()=>console.log("最后必执行"))

// es7(async/await异步等待)  
// async是Promise的语法糖,本质上是Promise,await是阻塞作用
    async (function fn() {
       try {
         // await≈Promise.then()
         let data = await ajaxPromise({url, data: {a: 1, b: 2}})
         data = await ajaxPromise({url, data: {a: data.result, b: 3}})
         data = await ajaxPromise({url, data: {a: data.result, b: 4}})
         console.log("结果为", data);
        } 
        
        // catch≈Promise.catch()
        catch (error) {
            console.log("error:", error);
        }
        
        // ≈Promise.finally() 
        console.log("over");
   })()

console.log("continue");

// 拓展方法
// all():全荣共荣,一损俱损
    Promise.all([promise1,promise2,promise3])
       .then(value=>console.log(value))//全resolve才有
       .catch(error=>console.log(error))//有一个reject就error
       
// allSettled():完全期约,无论resolve还是reject都要输出,明确每个promise的最终状态
// 不要用await测试,会报错,因为async/await挂起的特性
    Promise.allSettled([promise1, promise2, promise3])
       .then(values => values.forEach(item => console.log(item.status, item)))
 
// 完全期约结果如下
// fulfilled {status: 'fulfilled', value: 6}
// rejected  {status: 'rejected', reason: '6.5'}
// fulfilled {status: 'fulfilled', value: 7}

// race()竞速契约,不管resolve还是reject,谁快,结果就是谁
    Promise.race([promise1, promise2])
       .then(value=>console.log(value))
       .catch(error=>console.log(error))

参考文献

面试官系列

面试官:说说你对事件循环的理解 https://vue3js.cn/interview/JavaScript/event_loop.html

面试官:你是怎么理解ES6中 Promise的?使用场景 https://vue3js.cn/interview/es6/promise.html

《JavaScript高级程序设计(第4版 中文高清)》P321-326