likes
comments
collection
share

[ES6]新方法--Promise 手搓异步,拳打回调 ,拿捏同步

作者站长头像
站长
· 阅读数 12

前言

ES6新增Promise方法有何用处?同步和异步是什么?

我们来学习一下JavaScript中的同步和异步!以及如何用Promise处理代码当中的异步!

这同步和异步的概念,可能和我们在字面的理解有点不同!接下来,我们直接开始学习!

正文

JavaScript在执行代码时是单线程的,这意味着它一次只能处理一个任务。但随着Web应用程序变得越来越复杂,我们需要一种能够处理异步操作的方式。这就是异步编程变得至关重要的地方。在这篇文章中,我将带大家JavaScript中异步和同步编程的概念

一、同步

1、什么是同步

看到“同步”大家就会顾名思义,就是同一时间发生执行嘛!我第一次看到也是这么想的!

但是在JavaScript其实同步和我们的想法其实有点出入!

同步编程是指代码按照顺序一行一行地执行,每一行代码都需要等待上一行代码执行完成后才能执行。这种方式简单直观,但在执行耗时的任务时可能导致程序阻塞,影响用户体验。

也就是说!

同步,就是我们的代码一行一行从上往下执行!当前执行的语句执行完了,才会执行下一句!也就是这样

console.log('这是1');
console.log('这是2');
console.log('这是3');
console.log('这是4');
console.log('这是5');
输出:
这是1
这是2
这是3
这是4
这是5

这样就一个同步执行的代码!我们不妨来思考一下,假如代码一昧得按照同步执行下去会有什么后果!

假如,我们遇到一行需要花费一定时间的代码行或者代码块!按照同步执行的定义,一定要这一步执行完了,才会去执行接下来的语句,是不是效率就会大大降低?

2、同步编程的缺点

虽然同步编程在某些情况下可能简单直观,但也存在一些不足之处,特别是在处理大量I/O操作、网络请求或其他可能导致阻塞的任务时。

  1. 阻塞: 同步编程中,如果某个任务耗时较长,整个程序会被阻塞,导致用户界面冻结或响应变慢。这种阻塞可能是因为文件读取、网络请求等I/O操作,会显著影响程序的性能和用户体验。

  2. 不适合大规模任务: 在处理大量并发任务时,同步编程可能会导致程序效率低下。例如,在服务器端处理多个客户端请求时,同步编程可能无法充分利用系统资源,导致响应时间延长。

  3. 可读性差: 在同步编程中,如果有多个嵌套的回调函数(回调地狱),代码可读性会降低,难以理解和维护。这种情况通常称为"回调地狱",会使代码结构复杂,难以调试。

  4. 错误处理困难: 在同步代码中,错误处理通常通过异常处理来实现。然而,异步代码中的错误处理更加灵活,可以通过回调函数传递错误信息,而不会导致整个程序崩溃。

什么是回调?我们直接给大家上一个案例,大家就知道什么是回调了!

// a的词法作用域在全局
function a(){
    setTimeout(()=>{
    console.log('我是a');
    b()//作用域  这里叫回调
},1000)
}
function b(){
    setTimeout(() => {
        console.log('这里是回调b');
    }, 2000);
    
}
a()
输出:
我是a
这里是回调b

这样的就是一个回调!

3、回调地狱

回调地狱(Callback Hell)是指在JavaScript中,多层嵌套的回调函数造成代码难以维护和理解的情况。这通常发生在处理异步操作时,其中一个异步操作的结果需要传递给另一个异步操作,而这可能导致多层嵌套的回调函数。

回调地狱的典型情景是,一个异步操作完成后,执行一个回调函数,在这个回调函数中可能包含另一个异步操作,依此类推。这样的代码结构会导致代码嵌套层次加深,形成梯形或金字塔状的代码结构,降低了代码的可读性、可维护性和可扩展性。

我们给大家看一个简单的回调地狱案例:

a1(function(result1) {
  console.log(result1);
  
  a2(function(result2) {
    console.log(result2);
    
    a3(function(result3) {
      console.log(result3);
      
      // 更多的嵌套...
    });
  });
});

在上述例子中,a1 的回调中包含了 a2as2 的回调中包含了 a3,依此类推。这种结构会使代码变得难以理解和维护。

好了到了这里,接下来我们就开始进入异步的学习!

二、异步

1、什么是异步

其实,平时我们Coding产生更多问题的是异步带来的问题!

我们先来介绍一下异步吧:异步编程是一种处理可能耗时的任务而不阻塞程序执行的编程范式。在异步编程中,代码不会按照顺序一行一行地执行,而是通过回调函数、Promise、async/await等机制,允许程序在执行某些任务的同时继续执行其他任务。这种方式对于处理I/O操作、网络请求、定时器等异步任务非常重要,可以提高程序的性能和响应性。

也就是说,异步的时候,不会按照一行一行代码执行,通过某种机制将我们需要花费时间的代码块或者代码行不影响到后续代码的执行,我们给大家看一个定时器带来的异步效果!

setTimeout(()=>{
    console.log('我是开始');
},1000)
console.log('我是结束');
输出:
我是结束
我是开始

我们可以看到这个案例就是一个异步的效果,也就是当我们的编译器执行到这个定时器时,会被识别成一个需要花费时间的代码块,所以编译器会先跳过它,先把后面同步执行的代码先给它执行完,再回过头来执行这个代码块!这个代码还不够直观,我们再来看

setTimeout(()=>{
    console.log('我是开始');
},1000)
console.log('我是结束');
setTimeout(()=>{
    console.log('我是第二个定时器');
},1000)
console.log('我是真的结束');
输出:
我是结束
我是真的结束
我是开始
我是第二个定时器

这样一看,大家其实就能更加直观地看到这个里面的特点,我们的编译器并不是一行一行执行完全按照同步的方法执行!而是有一个异步的操作,万事有好有坏!我们来说说异步的问题吧!

2、异步的缺点

尽管异步编程在处理耗时任务和提高程序性能方面有很多优势,但它也存在一些缺点,这些缺点在某些情况下也可能会影响开发和维护代码的难度。

  1. 回调地狱(Callback Hell): 在复杂的异步操作中,多次嵌套回调函数可能导致代码结构混乱,降低代码的可读性和可维护性。

  2. 错误处理困难: 异步代码中的错误处理相对复杂,可能需要多次使用回调函数的第一个参数Promise.catch(),或者在try-catch块中使用async/await,这增加了错误处理的复杂性。

  3. 难以追踪和调试: 异步代码的执行顺序不同于同步代码,这使得在追踪和调试程序时更加困难,特别是当涉及多个异步操作时。

  4. 并发控制: 在异步操作中,可能会涉及到多个任务的并发执行。这可能导致竞态条件(Race Conditions)和其他并发问题,需要额外的工作来确保数据的一致性和正确性。

  5. 代码可读性降低: 对于一些初学者或不熟悉异步编程模型的开发者来说,异步代码的写法可能相对晦涩,降低了代码的可读性。

尽管异步编程存在这些缺点,但通过使用适当的工具和设计模式,可以缓解这些问题。例如,使用Promise可以解决回调地狱问题,而async/await可以使代码更加清晰易读。在编写异步代码时,合理的代码结构和良好的注释也能够提高代码的可维护性。

我们来看看这个一个“龙哥相亲”的案例,再接着往下学习

function xq(){
        setTimeout(()=>{
            console.log('龙哥相亲了!');
        },2000)
    
}
function marry() {
        setTimeout(()=>{
            console.log('龙哥结婚了');
        },1000)
}
xq()
marry()
输出:
龙哥结婚了
龙哥相亲了!

对于相亲,我们按照正常的思维,应该是要先相亲再结婚吧!我们看看这个案例,函数的调用顺序也没有问题啊,怎么就先结婚再相亲了呢?怎么个事?这其实就是异步带来的一个问题之一啊!其实我们可以这样写!

function xq(){
        setTimeout(()=>{
            console.log('龙哥相亲了!');
            marry()
        },2000)
    
}
function marry() {
        setTimeout(()=>{
            console.log('龙哥结婚了');
        },1000)
}
xq()
输出:
龙哥相亲了!
龙哥结婚了

但是这样,假如嵌套一多起来,哇塞,想一想就头皮发麻,这就是我们所说的回调地狱!!!

所以,我们要学习一个一个方法,能够手搓异步,拳打回调地狱!

三、Promise处理异步

1、promise的基本用法

当涉及到 JavaScript 异步编程时,Promise 是一种非常强大和灵活的工具。Promise 提供了一种更结构化、更可读的方式来处理异步操作,避免了传统的回调地狱(Callback Hell)问题。

Promise 是一种用于处理异步操作的对象,可以看作是对异步操作的封装。

例如:在我们“龙哥相亲”的案例当中,我们通过Promise就能将整个过程合理化!

我们要如何操作呢?

function xq(){
    //返回了一个promise的实例对象
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log('龙哥相亲了!');
            resolve('相亲成功啦!')//里面的参数可以传给then(随便加一个形参),抛出一个值
            //resolve()不用你传,源码已经打造好了,直接用
        },2000)
    })//promise是ES6新增加的构造函数,要new一下
    
}
function marry() {
        setTimeout(()=>{
            console.log('龙哥结婚了');
            console.log('结婚完成了')
        },1000)
}

xq()
.then((res)=>{
    console.log(res);
    marry()
})
输出:
龙哥相亲了!
相亲成功啦!
龙哥结婚了
结婚完成了

上述案例,我们就可以知道,使用Promise可以处理异步的问题

这样我们龙哥的相亲过程就没有问题了嘛。

  1. promise用于处理异步,因为在xq函数当值,我们用return new Promise((resolve,reject)=>{}包裹住了我们的定时器,这就是一个promise的基本语法,同时在定时器当中,我们要调用resolve返回一个参数,这个参数可以被.then获取到!
  2. resolvereject是两个已经被设定好的语法,调用resolve那么.then可以执行,调用reject那么表示 Promise 的状态变为被拒绝(rejected)状态。这意味着与 Promise 相关联的异步操作失败了。调用 reject 通常发生在异步操作出现错误或者无法完成的情况下。
  3. xq 函数返回一个 Promise 实例对象,该实例对象在 2 秒后会调用 resolve 方法,打印 龙哥相亲了! 并传递字符串 相亲成功啦!
  4. marry 函数体内,只是使用了一个定时器1秒后打印龙哥结婚了结婚完成了
  5. 我们首先调用xq(),然后接.then,里面包裹一个函数res接收的是resolve传过来的值,先打印res再调用marry
  6. .then必须要接在promise的实例对象之后!

假如我们的龙哥要生一个baby怎么办!我们的语句该怎么写?

你会说这样编写!

xq()
.then((res)=>{
    console.log(res);
    marry()
}
)
.then(...)

其实这样是行不通的!那这样编写有什么后果?我们来看看!

function xq(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log('龙哥相亲了!');
            resolve('相亲成功啦!')
        },2000)
    })
}
function marry() {
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log('龙哥结婚了');
            resolve('结婚完成了')
        },1000)
    })
}
function baby(){
    setTimeout(()=>{
        console.log('生个Baby');
    },500)
}
xq()
.then((res)=>{
    console.log(res);
    marry()
})
.then(()=>{
    baby()
})
输出:
龙哥相亲了!
相亲成功啦!
生个Baby
龙哥结婚了

我们写成这样也不对的,为什么呢?

我们知道,.then必须接在promise的实例对象之后,而.then天生就会默认返回一个promise实例对象!它与marry的对象相同一致,也就意味着,第一个.then和第二个.then其实是同步执行的,又因为定时器的存在,又产生了异步,先执行时间花费少的,导致了他们输出结果的次序不同!

那么这里我们要改进一下代码了!

xq()
.then((res)=>{
    console.log(res);
    return marry()
})
.then(()=>{
    baby()
})
输出:
龙哥相亲了!
相亲成功啦!
龙哥结婚了
生个Baby

咦?为什么改成return marry()返回函数的调用就可以了呢?我们改为return,就会覆盖掉.then默认返回的promise,你写了就用你的,会等你返回,如果没写就用默认的,所以我们return marry()的执行结果,是不是返回了marry函数中返回的promise实例对象,用它覆盖了默认返回的promise,所以第二个.then就接在marry()promise实例对象之后了!这样我们的结果就是正确的顺序!

我们前面讲到了.then用于接收resolve传过来的参数!

但是参数中还有了一个reject,其实.catch就会接收reject传的参数!

function a(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            console.log('你好');
            reject('不好')
        },1000)
    }
    )
}
function b(){
    setTimeout(()=>{
        console.log('好好好');
    },500)
}
a()
.catch((ref)=>{
    console.log(ref);
    b()
})
输出:
你好
不好
好好好

假如我们在这样一段案例当中,就是resolvereject的结合,我们使用了race

2、Promise的race方法

function a(){
    return new Promise(
        (resolve,reject) =>{
    setTimeout(()=>{
        console.log('a');
        reject('no')
    },1000)
}
    )
}
function b(){
    return new Promise(
        (resolve,reject)=>{
    setTimeout(()=>{
        console.log('b');
        reject('no')

    },1500)
})
}
function c() {
    console.log('c');
}
//a和b都要成功运行,.then根据最快完成执行的运行!用于优化代码!比如在接口当中,用race就可以直接拿最快的接口
Promise.race([a(),b()]).then(c)
//catch负责兜底
.catch((err)=>{
    console.log(err);
})
//只要race里面的参数只要有一个resolve运行就执行
输出:
a
no
b

可以看到这个案例当中,并没有执行.then的逻辑,因为ab都调用的是reject所以.then不执行,但是我们要知道在.race接收的数组,而数组当中的函数只要有一个调用了resolve方法,那么race后面接的.then就会执行!

还有一个all方法!

3、Promise的all方法

function a(){
    return new Promise(
        (resolve,reject) =>{
    setTimeout(()=>{
        console.log('a');
        reject('no')
    },1000)
}
    )
}
function b(){
    return new Promise(
        (resolve,reject)=>{
    setTimeout(()=>{
        console.log('b');
        resolve('yes')

    },1500)
})
}
function c() {
    console.log('c');
}
//a和b都要成功运行,.then根据最快完成执行的运行!用于优化代码!比如在接口当中,用race就可以直接拿最快的接口
Promise.all([a(),b()]).then(c)
//catch负责兜底
.catch((err)=>{
    console.log(err);
})
//只要race里面的参数只要有一个resolve运行就执行
输出:
a
no
b

我们也可以看到,我们在b中调用了resolve也没有执行.then的逻辑,这些因为allrace又有不同,它必须满足参数数组的函数的状态都为resolve才会执行.then的逻辑!

总结

异步

为了让js执行效率更高! 才是同时干多件事情!同一时间干多件事情--并发!

同步

可以按顺序一个一个往下走,顺势而为

回调

回调地狱:代码维护困难 当项目够大,一层套一层,一个出问题,全坏,排查问题代码维护非常难

promise

race 只要接收的其中有一个promise状态为resolve,then就会调用

all 必须要接收到的所有的promise状态都为resolve,then才会调用

好了,今天我们有关Promise和异步,同步的学习到这里就结束啦!

欢迎大家评论留言!点个小小的赞鼓励支持一下吧!🌹🌹🌹