同步与异步

什么是同步?

同步意为代码必须一步一步走下去。只有执行完当前的代码,才会执行下一条代码

let x = 1
let y = x + 1
console.log(y)
2

什么是异步?

异步则是代码的执行顺序不一样了。

如上一条指令写了一条三秒的延时,或上一条指令需要等待接口的返回,而下一步指令却已经执行了

上一条代码还未执行完成,下一条却已经开始执行了,就是异步

setTimeout(() => {
    let z = 1
});
console.log(z)
Uncaught ReferenceError: z is not defined // 没有执行到为z赋值的语句就执行了console.log

早期javascript处理异步的方式

早期javascript对异步函数的处理是稍许僵硬的,如:

异步代码的返回值,只能在异步代码执行结束之后调用回调函数传递

function YiBu(callback){
    setTimeout(() => {
        let x = 1 + 1
        callback(x)
    })
}

function someFunc(x){
    console.log(x)
}

YiBu(someFunc)
2

这种方法只能解决简单的异步问题,遇到较复杂的代码则容易遇上回调地狱

为了解决回调地狱的问题,ES6就出现了专门解决回调地狱的工具-Promise

什么是Promise?

Promise:期约,承诺,约定

Promise是现在JavaScript的一种优秀的异步解决方案,解决了回调地狱的问题。

平时异步编程时一般常用的有asyncawait

使用Promise需要先使用new关键字创建一个Promise的实例出来,并且在构造函数的入参携带一个匿名函数,叫做执行器

let a = new Promise(() => {
    // 我是执行器
})
console.log(a)
Promise {<pending>}

返回了一个默认为pending状态的Promise实例

在创建Promise实例时,入参的执行器是必填的

Promise的状态

每个Promise实例都有对应的状态,分别为pending,resolved,rejected

Promise执行器

执行器是Promise构造函数的入参,主要有以下两个作用:

  • 初始化Promise的行为
  • 控制Promise的状态转换

你可以在这个执行器中任意地方改变这个Promise的状态,如在定时器中

let a = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('定义了一个解决的值')
    }, 2000);
});
console.log(a)

亦或是在接口拿到值之后:

const xhr = new XMLHttpRequest()
let a = new Promise((resolve, reject) => {
    xhr.onreadystatechange = () => {
        if(xhr.readyState != 4) return;
        if(xhr.readyState == 4 && xhr.status == 200){
            // 请求成功的处理
            resolve(xhr.responsetext)
        }else {
            // 请求失败的处理
            reject('服务器错误')
        }
    }
    xhr.open('get', url);
    xhr.send(null)
});
console.log(a)

resolve:解决、决定

如果你认为运行成功,就可以调用resolve

reject:拒绝、驳回

如果你认为运行失败,就可以调用reject

这两个方法可以让Promise改变为对应的状态

执行器中,必须保证有这两个内部函数,通过两个相同名称的参数传到执行器里面来

触发promise对应的处理程序,需要改变Promise的状态。这就是改变Promise状态的作用

resolve()和reject()中的值即为解决时返回的值或拒绝时返回的值,若未填写则返回undefined

执行器中的代码块是和new Promise同步执行的

Promise.resolve和Promise.reject

原先通过执行器构造函数那种方式创建的Promise对象默认是Pending状态

使用Promise.resolvePromise.reject方法可以直接创建一个已经解决/已经拒绝的Promise

这两个函数接受的参数可以是任意类型,也可以接收一个Promise。他们入参是任意类型,出参是一个Promise

let a = new Promise((resolve, reject) => resolve(3))
console.log(a)
Promise {<fulfilled>: 3}

包装规则

目的:返回Promise类型的数据,方便.then链式调用

入参基本可以分为以下几种情况:

// 基础数据类型
let p1 = Promise.resolve(1)
// 入参为一个Promise
let p2 = Promise.resolve(new Promise(() => {}))
// 入参是一个异常
let p3 = Promise.resolve(new Error(''))
// 多个参数
let p4 = Promise.resolve(1, new Promise(() => {}), new Error())

console.log(p1)    // Promise {<fulfilled>: 1}
console.log(p2)    // Promise {<pending>}
console.log(p3)    // Promise {<fulfilled>: Error at <anonymous>:1:26}
console.log(p4) // Promise {<fulfilled>: 1}
  1. 一个值,无论是undefined、null还是false,表示的都是一个值,目标是一个Promise,即:包装成一个解决值为这个基础值的已解决Promise
  2. 已经是个Promise,传入这个Promise与入参Promise完全对应,直接返回pending
  3. 创建一个解决值为异常的Promise,所以返回值是一个解决值为这个异常的已解决Promise,
  4. 会自动忽略第一个参数外的其他参数,并将第一个参数按上述规则包装。

reject的包装规则大致相同

.then

.then是Promise规范规定所有Promise必须支持的方法,实现这个then方法也叫做实现了Thenable接口

作用:为Promise添加处理程序的主要方式

入参:两个,分别是onResolvedonRejected,分别代表这个Promise状态被解决的时候执行和被拒绝的时候执行

返回值:一个Promise,如上文解释,可链式调用

入参规则:两个参数都是可选的。如果都提供,则分别在Promise进入解决状态或拒绝状态时执行,入参多于两个则自动忽略多余参数,不是函数也会自动忽略

let a = new Promise((resolve, reject) => {
    resolve('111')
})

a.then((res) => {
    // onResolved处理程序放这里
    console.log(res)
    console.log('解决')
},(res) => {
    console.log('拒绝')
},(res) => {
    // 如果有多余的参数则会被忽略
})

// 如若只想传递一个onRejected处理程序,第一个入参函数不可不写,需要写一个占位的空函数或无意义的非函数入参,建议写null
let b = new Promise((resolve, reject) => {
    reject('111')
})
b.then(null, (res) => {console.log('推荐这样写');console.log(res)})

.catch/.finally

.catch相当于一个小马甲,是.then中第二个参数onRejected的另一种写法

let a = new Promise((resolve, reject) => {
    reject('111')
})

// 以下两种写法是完全相同的
a.then(null, () => {
    console.log('拒绝')
})

a.catch(() => {
    console.log('拒绝')
})

.finally在Promise转换为已拒绝或者已解决时都会执行,一般用于兜底或清理代码

.finally和.catch也都返回一个Promise

非重入Promise方法

Promise状态改变的时候,对应的程序如onResolve和onRejected不是立马被执行的,而是会被排到一个异步事件队列,这个异步事件队列

一般情况是会在同步代码之后执行,同时这个异步事件队列中的事件讲究先进先出:即假如有两个Promise都改变到对应状态了,先改变状态的程序就先执行。

这里涉及到一个知识点叫EventLoop

Promise .then里面的所有状态处理程序都不会立即执行,而是先放到这个异步事件队列里面,这就叫做非重入Promise方法

Promise连锁

这个概念其实就是Promise的链式调用,由于Promise的事件处理程序只会在Promise状态改变之后触发,而事件处理程序最后又会返回一个包装的Promise。

那么在处理程序中,再创建另一个异步事件的Promise,就可以继续逻辑清晰地调用.then,把多个任务像链子一样串起来

Promise.all

Promise.all将它入参的所有Promise全部resolved解决之后,它才会变成resolved,只要入参中有一个rejected,那么它就是rejected

入参:包含Promise的可迭代对象(一般传Promise数组就行)

let a = new Promise((resolve, reject) => {
    console.log('promise1')
    resolve(1)
})

let b = new Promise((resolve, reject) => {
    console.log('promise2')
    resolve(2)
})

let finalP = Promise.all([a.b])
console.log(finalP)

返回值:Promise

Promise.race

race:竞赛、竞争

Promise.race创建的新Promise就是让他入参的几个Promise进行竞赛,如果谁先改变状态,那么Promise.race的状态就改变成这个第一个改变状态的入参Promise状态

用例:如现在调用一个接口,创建一个Promise,然后和另一个定时Promise进行race。这里定时任务写个5秒,意为:接口要是5秒还没结果,那么定时任务就要拒绝了,这样就可以做个接口超时的处理

let a = new Promise((resolved, reject) => {
    console.log('比如我是一个接口')
    setTimeout(() => {
        // 花一点时间接收数据
        resolve('接收到了接口数据')
    },10000)
})

let b = new Promise((resolved, reject) => {
    setTimeout(() => {
        reject('接收数据超时')
    },2000);
})

let request = Promise.race([a,b])
request.then(res => {
    console.log('解决:接口成功接收到了数据')
},res => {
    console.log('拒绝:接口超时接收')
})

.then返回规则

  1. 由于.then的两个入参都是可选的,如果都不填,那么就会直接返回上一个Promise
  2. 传入了解决处理程序且上一个Promise resolve了,走进了onResolved回调,那么就得看返回值了:无返回值则返回undefined,有则返回包装成解决值为这个的resolve

    let a = new Promise((resolve, reject) => {
        console.log('比如我调一个接口')
        resolve(111)
    })
    
    let b = a.then((res) => {
        // 啥也不写,就等于返回一个undefined
        // 等同如下
        return undefined
    })
    
    let c = a.then((res) => {
        // 都返回reject的undefined
        return undefined
        return 1
        return '1'
        return null
    })
最后修改:2022 年 08 月 24 日
如果觉得我的文章对你有用,能不能v我50参加疯狂星期四