Bootstrap

【H2O2|全栈】JS进阶知识(十)ES6(6)

目录

前言

开篇语

准备工作

同步和异步

概念

优缺点

案例

同步情况

异步情况 

回调地域

Promise

概念

状态

成功状态

实例函数

案例

封装AJAX

优化回调地域

拓展——await

总结

异常

try-catch-finally

error

throw

结束语


前言

开篇语

本系列博客主要分享JavaScript的进阶语法知识,本期为第十期,依然围绕ES6的语法进行展开。

本期内容为:同步和异步、回调地域、Promise和异常。

与基础部分的语法相比,ES6的语法进行了一些更加严谨的约束和优化,因此,在之后使用原生JS时,我们应该尽量使用ES6的语法进行代码编写。

准备工作

软件:【参考版本】Visual Studio Code

插件(扩展包):Open in browser, Live Preview, Live Server, Tencent Cloud AI Code Assistant, htmltagwrap

提示:在不熟练的阶段建议关闭AI助手

浏览器版本:Chrome

系统版本: Win10/11/其他非Windows版本

同步和异步

概念

同步操作是指,程序由上至下顺序执行,后面的数据需要上一次执行的结果。

异步操作是指,多个功能并发(同时)执行,我们此前学过的计时器、AJAX都是支持异步操作的。

优缺点

同步操作的优点是数据安全,按顺序接收数据和执行。缺点是顺序执行的效率比较低

异步操作的优点是执行速度快。缺点是存在安全性问题,明明需要先执行的步骤,却因为异步条件不符合而被推迟到之后了。

案例

现在有这么一个案例,将大象放入冰箱,总共需要几步?

其实,也就下面三个步骤——

1. 打开冰箱门

2. 大象放进去

3. 关闭冰箱门

我们为这三个步骤封装三个方法——

function open() {
    console.log('打开冰箱门');
}

function putIn() {
    console.log('大象放进去');
}

function close() {
    console.log('关闭冰箱门');
}

同步情况

首先,我们来看看在同步情况下三个方法的执行情况。

我们封装一个执行函数exec(),它的参数为回调函数,由回调函数来决定执行哪一个功能,就像下面这样——

function exec(callback) {
    callback();
}

那么,我们想要顺序执行上面的三个事件,就可以这么做——

exec(open);
exec(putIn);
exec(close);

异步情况 

现在,我们为上面的三个步骤添加延时时间,即添加一个计时器。

我们假设第一个步骤需要延时3000ms完成,第二个步骤需要延时2000ms,最后一步需要延时1000ms,那么,我们的执行函数就需要更改成下面的形式——

function exec(callback, time) {
    setTimeOut = (callback, time);
}

注意,这里的callback实际上就是计时器中的() => callback(),由于箭头函数本质上就是用于执行回调函数,所以这里直接写回调函数名即可(不要写括号,箭头函数没有在参数中调用)。

这是,如果我们再次像之前那样直接写三个exec()函数,由于三者异步执行,于是出现了下面的结果——

exec(open, 3000);
exec(putIn, 2000);
exec(close, 1000);

可以看到,明明需要先执行的打开冰箱门的步骤,却因为在并发操作中延时最久,在最后一步才执行,导致出现了上面的荒唐的结果。

所以,对于这类情况,我们需要将原本的并发执行的异步情况转化为顺序执行的同步情况,确保所有的步骤都能安全地执行,这也是本节的重点。 

回调地域

为了解决异步操作之间的顺序问题,我们可以在exec()上做出一些小小的处理。

我们先执行完第一步,再执行第二步,之后的操作相同。

在callback的位置,我们知道callback和() => callback()是等价的,所以我们将该形参展开为箭头函数,而第二形参time保持不变——

exec(() => {
    open();
},3000);

在open()执行完毕后,我们再次执行exec(),这一次执行putIn(),就像下面这样——

exec(() => {
    open();
    exec(() => {
        putIn();
    },2000);
},3000);

由此一来,我们就成功实现了先执行完第一步,再执行第二步的操作,第三步也是一样的。

exec(() => {
    open();
    exec(() => {
        putIn();
        exec(() => {
            close();
        }, 1000);
    }, 2000);
}, 3000);

这样一种将异步操作转为同步操作的方式,就是回调地域

但是,不难发现,对于这种转化的方式,整个程序在排列上是按照纵向排列的。

当我们执行的步骤很多时,就需要不断地进行这种回调的过程,最后导致整个程序的代码量将会非常庞大,不方便我们进行代码的调试工作

Promise

概念

Promise就是一个构造函数,构造函数中有一个参数,该参数是一个函数。这个函数有两个参数resolved,rejected,分别代表两个回调函数。

于是,Promise就可以写成下面这样——

Promise((resolved, rejected) => {
    // 调用的回调函数
})

状态

一般来说,Promise有三种状态——准备(pending)、成功(fulfilled/resolved)和失败(rejected)。

首先,我们为Promise创建一个实例化对象,以便查看这些状态——

const p = new Promise(() => { });

如果不调用任何回调函数,表示准备状态,控制台打印结果如下——

如果调用resolved()函数,表示成功状态,控制台打印的结果如下——

const p = new Promise((resolved, rejected) => {
    resolved();
});

如果调用rejected()函数,表示失败状态,控制台打印的结果如下——

const p = new Promise((resolved, rejected) => {
    rejected();
});

成功状态

这也是一道经典的面试题,一般来说,成功的状态有下面这些——

1. 初始化 resolved函数

2. 没有报错 但是没有返回值

3. 没有报错 但是有返回值 返回值作为下一个then的接收参数

4. Promise有一个静态函数 resolve

其中resolved()是直接调用的,静态函数是由Promise.resolve()调用的。 

实例函数

Promise有三种常用的实例函数——then(),catch(),finally()。

then()有两个参数,分别是两个回调函数,第一个函数在接受成功的情况下执行,第二个函数在接受失败的情况下执行。只写一个参数,代表成功的情况。

catch()仅有一个参数,仅用于接收失败的情况,一般来说失败的情况都由该方法接受。

finally()没有参数,无论成功还是失败都需要接受参数。

案例

封装AJAX

假设我们现在有三个文本文件,请用ajax来按顺序接受他们的参数。

由于ajax为异步操作,所以我们可以使用Promise将这三次ajax转为同步操作。

在ajax返回success()时,调用resolved();返回error()时,调用rejected()——

function getAjax(url) {
    return new Promise((resolved, rejected) => {
        $.ajax({
            url,
            success(res) {
                // ajax成功时调用resolved
                resolved(res);
            },
            error(res) {
                // ajax失败时调用rejected
                rejected(res);
            }
        })
    })
}

为简单起见,这里暂时只考虑成功的情况,通过resolved()的参数,可以获取success()接收的数据。

而then()可以拿到resolved()的参数或者上一次return的结果,于是,我们可以在then()中接收上一步ajax()获取的数据,并返回下一次getAjax()的结果。

getAjax(url1)
    .then(data => {
        // 将数据从ajax中通过resolved的参数拿出来
        console.log(data);
        // 进行下一次的ajax操作,返回的参数交给下一次的then
        return getAjax(url2);
    }).then(data => {
        console.log(data);
        return getAjax(url3);
    }).then(data => console.log(data));

优化回调地域

对于之前的大象放进冰箱的案例,我们也可以利用Promise来优化。

思路是,让exec()方法返回一个Promise()对象,然后再将这个对象交给后续的then()接收,由此往复。

exec(open, 3000).then(() => exec(in1, 2000)).then(() => exec(close, 1000))

拓展——await

其实,上述使用then的过程,还可以使用另一种等价的方式来实现——

await exec(open, 3000);
await exec(putIn, 2000);
await exec(close, 1000);

await可以让当前的方法执行完毕之后,再执行后续的方法。

但await必须在使用async修饰的函数中使用,它用于将同步函数转为异步函数

async function fn() {
    await exec(open, 3000);
    await exec(putIn, 2000);
    await exec(close, 1000);
}

fn();

当同步函数返回了一个Promise对象后,由于then()的函数式异步,从而将异步转为同步

实质上,await()返回的数据就是Promise的then()返回的数据。

总结

常见的异步操作汇总如下——

计时器: setTimeout【经过多少毫秒执行一次】/ setInterval【每隔多少毫秒执行一次】;

ajax:jquery的ajax方法的async参数为true时(默认状态)为异步;

③Promise的then() / catch()是异步。

事件是异步的。

异常

try-catch-finally

当我们认为一段代码可能会出现错误时,就可以尝试(try)执行这段代码,并捕获(catch)其中可能出现的错误,为了不让这个错误阻断我们的程序,我们还需要让程序抛出错误之后还能最终执行(finally)

error

如果我们捕获到了这个错误,并想要让这个错误在控制台输出出来,就可以catch(err),并使用console.error(err)将这个错误输出。形参err就是我们捕获的错误。

throw

对于异常而言,有一些常见的异常对象,由new + 构造函数获取,并由throw抛出。

由于常见的异常对象的父类都是Error()类型的,所以我们也常常使用下面的形式统一抛出错误——

throw new Error('自定义的错误提示语句');

比如,我们定义一个常量a,当常量a的值小于100时,我们抛出自定义的异常——

const a = 10;
if(a < 100) {
    throw new Error("错误:a不足100");
}

结束语

本期内容到此结束。关于本系列的其他博客,可以查看我的JS进阶专栏。

在全栈领域,博主也只不过是一个普通的萌新而已。本系列的博客主要是记录一下自己学习的一些经历,然后把自己领悟到的一些东西总结一下,分享给大家。

文章全篇的操作过程都是笔者亲自操作完成的,一些定义性的文字加入了笔者自己的很多理解在里面,所以仅供参考。如果有说的不对的地方,还请谅解。

==期待与你在下一期博客中再次相遇==

——临期的【H2O2】

;