期约 + 异步编程

165 阅读11分钟

Promise

简介

Promise是异步编程的一种解决方案(相当于异步操作的同步写法)。

一项操作需要依赖另外一项操作的返回值,如果使用嵌套回调的方式会造成回调地狱的问题,Promise解决了这个问题。

Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise有三个状态:待定,解决,拒绝;Promise状态一旦改变为拒绝或是拒绝就无法再更改。

Promise弊端:

  • 一旦执行,不可取消;
  • 无法得知执行进度,即:处于待定状态时,不知道是刚刚开始还是快要结束;

使用

创建

创建一个Promise对象,即期约。

通过new Promise()创建

Promise回调函数接收到两个参数:

  • resolve :异步操作执行成功后的回调函数
  • reject:异步操作执行失败后的回调函数 使用resolve()指定状态为-解决
let p1 = new Promise((resolve,reject)=>{resolve('解决!!')})

console.log(p1)//Promise { '解决!!' }

使用reject()指定状态为-拒绝

let p1 = new Promise((resolve,reject)=>{reject('拒绝!!')})

console.log(p1)//Promise { <rejected> '拒绝!!' }   【并抛出错误!】

执行器函数是同步执行的。

使用resolve()和reject()将状态改变后不可撤销,继续改变状态默认失败(没有效果!)

let p1 = new Promise((resolve,reject)=>{
  resolve('解决!!')
  reject('失败!!')//不会造成任何效果
})

console.log(p1)//Promise { '解决!!' }

使用定时器保证期约不会一直卡在待定状态,定时退出

let p1 = new Promise((resolve,reject)=>{
  //因为状态不可更改,所以如果在定时器内部执行前得到期约解决,定时器内部的reject()就无效了
  setTimeout(() => {//2秒后拒绝   
    reject('拒绝!!')
  }, 2000);
})

console.log(p1)//Promise { <pending> }   同步查看-状态为待定

setTimeout(() => {//3秒后其状态为拒绝
  console.log(p1)//Promise { <rejected> '拒绝!!' } 【并抛出错误!】
}, 3000);

Promise.resolve()-实例化一个解决期约

let p1 = Promise.resolve('解决!')

console.log(p1)//Promise { '解决!' }

Promise.reject()-实例化一个拒绝期约

let p1 = Promise.reject('拒绝!')

console.log(p1)//Promise { <rejected> '拒绝!' } 【并抛出错误!】

拒绝期约的错误不能被try/catch捕获,只能通过拒绝处理程序捕获。

原因:拒绝期约的错误没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理!

期约的实例方法

期约的实例方法是连接外部同步代码和内部异步代码直接的桥梁。

Promise.prototype.then()

then()接收两个参数

  • 第一个参数:onResolved()---期约解决时执行的回调函数
  • 第二个参数:onRejected()---期约拒绝时执行的回调函数
//解决的回调能接收到resolve接收的数据
new Promise((resolve,reject)=>{
  resolve('解决!!')
}).then(
  (data)=>{console.log(data)},//解决
  (err)=>{console.log(err)}
)

//拒绝的回调能接收到reject接收的数据
new Promise((resolve,reject)=>{
  reject('拒绝!!')
}).then(
  (data)=>{console.log(data)},
  (err)=>{console.log(err)}//拒绝
)

只需要提供一个回调函数的情况

//只需要解决的回调,那就省略2参
new Promise((resolve,reject)=>{
  resolve('解决!!')
}).then(
  (data)=>{console.log(data)},//解决
)

//只需要拒绝的回调,那就在1参的位置放上undefined
new Promise((resolve,reject)=>{
  reject('拒绝!!')
}).then(
  undefined,
  (err)=>{console.log(err)}//拒绝
)

注意!

  • 如果then()在使用时不传入处理程序,则原样向后传。

  • 如果没有显式的返回值,则Promise.resolve()会包装这个值。

  • 抛出异常会返回拒绝的期约。

  • 返回的错误值会被Promise.resolve()包装。

let p = new Promise((resolve,reject)=>{
  resolve('解决!!')
})

//不传入处理程序
let p1 = p.then()
setTimeout(() => {
  console.log(p1)//Promise { '解决!!' }
}, 0);

//没有显式的返回值
let p2 = p.then(()=>{})
setTimeout(() => {
  console.log(p2)//Promise { undefined }
}, 0);

//抛出异常
let p3 = p.then(()=>{throw '异常!!'})
setTimeout(() => {
  console.log(p3)//Promise { <rejected> '错误!!' }
}, 0);

//返回的错误值
let p4 = p.then(()=>{return Error('错误!!')})
setTimeout(() => {
  console.log(p4)//Promise {Error: 错误!!at D:......\text.js:22:29}
}, 0);

Promise.prototype.catch()

catch(回调函数) ---在接收到拒绝期约时执行回调函数---可用作最后的捕错手段。

new Promise((resolve,reject)=>{
  resolve('解决!!')
}).catch((err)=>{console.log(err)})//解决---无反馈

new Promise((resolve,reject)=>{
  reject('拒绝!!')
}).catch((err)=>{console.log(err)})//拒绝---执行回调函数

Promise.prototype.finally()

finally(回调函数) ---无论如何都会执行,其内部回调函数没有办法知道期约状态---可用作添加处理程序。

new Promise((resolve,reject)=>{
  resolve('解决!!')
}).finally(()=>{console.log('执行!!')})//解决-回调执行

new Promise((resolve,reject)=>{
  reject('拒绝!!')
}).finally(()=>{console.log('执行!!')})//拒绝-回调执行
  • 当期约进入落定状态时,与之相关的处理程序会被排期(进入任务队列),而不是立即执行。

  • 多个处理程序的执行顺序:按照添加它们的顺序执行。

期约连锁

因为每个期约实例的方法都会返回一个新的期约对象,所有可以连缀调用。

let timer = new Date()
let p = new Promise((resolve,reject)=>{
  setTimeout(() => {
    resolve(1)
    console.log(1,(new Date() - timer)/1000,'秒后执行')
  }, 1000);
})

p.then(()=>{
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      resolve(2)
      console.log(2,(new Date() - timer)/1000,'秒后执行')
    }, 1000);
  })
}).then(()=>{
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      resolve(3)
      console.log(3,(new Date() - timer)/1000,'秒后执行')
    }, 1000);
  })
}).then(()=>{
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      resolve(4)
      console.log(4,(new Date() - timer)/1000,'秒后执行')
    }, 1000);
  })
})
//1 1.002 秒后执行
//2 2.019 秒后执行
//3 3.025 秒后执行
//4 4.035 秒后执行

期约合成

将多个期约组合成为一个期约。

Promise.all()

Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决,其返回值为一个期约。

当期约全部解决---则合成期约的解决值为所有包含期约解决值的数组

let p1 = Promise.all([
  Promise.resolve('解决-1'),
  Promise.resolve('解决-2'),
  Promise.resolve('解决-3'),
])
setTimeout(() => {
  console.log(p1)//Promise { [ '解决-1', '解决-2', '解决-3' ] }
}, 0);

一个待定---则合成期约也待定

一个拒绝---则合成期约返回第一个拒绝的期约。

let p2 = Promise.all([
  Promise.resolve('解决-1'),
  Promise.resolve('解决-2'),
  Promise.reject('拒绝-1'),//第一个拒绝被返回
  Promise.reject('拒绝-2')//后面的拒绝会静默处理
])
setTimeout(() => {
  console.log(p2)//Promise { <rejected> '拒绝-1' }
}, 0);

Promise.race()

Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像

let p1 = Promise.race([
  Promise.resolve('解决-1'),//第一个落定---被返回
  Promise.resolve('解决-2'),//后面的期约---运行不会受影响
  Promise.resolve('解决-3'),
])
setTimeout(() => {
  console.log(p1)//Promise { '解决-1' }
}, 0);

let p2 = Promise.race([
  Promise.reject('拒绝-1'),//第一个落定---被返回
  Promise.resolve('解决-1'),
  Promise.resolve('解决-2'),
  Promise.reject('拒绝-2')//后面的拒绝---静默处理
])
setTimeout(() => {
  console.log(p2)//Promise { <rejected> '拒绝-1' }
}, 0);

手写实现

简易实现

//有什么作用? -- 解决回调地狱问题

// 1.结构初始化---使用类
// 2.this指向---注意!
// 3.then实现---在类上添加一个then方法
// 4.执行异常---使用tryCatch捕获并返回一个拒绝的Promise对象
// 5.异步---使用setTimeout模拟
// 6.回调保存---使用数组将每次then中的回调保存,放在最后执行
// 7.链式调用---在then中返回一个新的Promise对象

class MyPromise{

    static P = '待定'
    static F = '解决'
    static R = '拒绝'

    constructor(func){
        this.status = MyPromise.P //初始化状态为待定
        this.value = null//保存解决和拒绝的值
        this.resArr = []//保存每次then内部解决的回调函数-回调保存
        this.rejArr = []//保存每次then内部拒绝的回调函数-回调保存
        try {
            func(this.res.bind(this),this.rej.bind(this))//使用bind改变this指向是有必要的
        } catch (error) {
            this.rej(error)//如果MyPromise内部有异常则返回拒绝的MyPromise对象
        }
    }

    res(value){
        setTimeout(()=>{//将res内部代码放在最后执行-回调保存
            if(this.status === MyPromise.P){
                this.status = MyPromise.F
                this.value = value
                this.resArr.forEach(fn=>{//-回调保存
                    fn(value)
                })
            }
        })
    }
    rej(value){
        setTimeout(()=>{//将rej内部代码放在最后执行-回调保存
            if(this.status === MyPromise.P){
                this.status = MyPromise.R
                this.value = value
                this.rejArr.forEach(fn=>{//-回调保存
                    fn(value)
                })
            }
        })
    }

    then(onRes,onRej){
        return new MyPromise(()=>{//返回一个新的MyPromise对象---实现链式调用
            onRes = typeof onRes === 'function' ?onRes:()=>{}//如果不是函数则将其置为一个空函数
            onRej = typeof onRej === 'function' ?onRej:()=>{}//如果不是函数则将其置为一个空函数
    
            if(this.status === MyPromise.P){//如果状态为待定,则将回调保存,因为状态会异步更改-回调保存
                this.resArr.push(onRes)
                this.rejArr.push(onRej)
            }
            if(this.status === MyPromise.F){
                setTimeout(()=>{//then为异步,使用定时器将其与同步区分开
                    onRes(this.value)
                })
            }
            if(this.status === MyPromise.R){
                setTimeout(()=>{
                    onRej(this.value)
                })
            }
        })
    }
}


//测试
console.log('同步-1')
let p = new MyPromise((res,rej)=>{
    console.log('初始化')
    setTimeout(() => {
        res('解决!')
        rej('拒绝!')//无效
    }, 1000);
})
console.log('同步-2')
p.then(
    (res)=>{console.log(res)},
    (err)=>{console.log(err)}
).then(
    (res)=>{console.log(res)},
    (err)=>{console.log(err)}
)
console.log('同步-3')

// 同步-1
// 初始化
// 同步-2
// 同步-3
// 解决!

扩展版

添加了catch和resolve、reject、race、all。

class MyPromise{

    static P = '待定'
    static F = '解决'
    static R = '拒绝'

    constructor(func){
        this.status = MyPromise.P //初始化状态为待定
        this.value = null//保存解决和拒绝的值
        this.resArr = []//保存每次then内部解决的回调函数-回调保存
        this.rejArr = []//保存每次then内部拒绝的回调函数-回调保存
        try {
            func(this.res.bind(this),this.rej.bind(this))//使用bind改变this指向是有必要的
        } catch (error) {
            this.rej(error)//如果MyPromise内部有异常则返回拒绝的MyPromise对象
        }
    }

    res(value){
        setTimeout(()=>{//将res内部代码放在最后执行-回调保存
            if(this.status === MyPromise.P){
                this.status = MyPromise.F
                this.value = value
                this.resArr.forEach(fn=>{//-回调保存
                    fn(value)
                })
            }
        })
    }
    rej(value){
        setTimeout(()=>{//将rej内部代码放在最后执行-回调保存
            if(this.status === MyPromise.P){
                this.status = MyPromise.R
                this.value = value
                this.rejArr.forEach(fn=>{//-回调保存
                    fn(value)
                })
            }
        })
    }

    then(onRes,onRej){
        return new MyPromise((res,rej)=>{//返回一个新的MyPromise对象---实现链式调用
            onRes = typeof onRes === 'function' ?onRes:value=>value//如果不是函数则将其置为一个空函数
            onRej = typeof onRej === 'function' ?onRej:err=>{throw err}//如果不是函数则将其置为一个空函数
    
            if(this.status === MyPromise.P){//如果状态为待定,则将回调保存,因为状态会异步更改-回调保存
                this.resArr.push(onRes)
                this.rejArr.push(onRej)
            }
            if(this.status === MyPromise.F){
                setTimeout(()=>{//then为异步,使用定时器将其与同步区分开
                    try {
                        onRes(this.value)
                    } catch (error) {
                        rej(error)//如果有异常则返回拒绝的MyPromise对象
                    }
                })
            }
            if(this.status === MyPromise.R){
                setTimeout(()=>{
                    try {
                        onRej(this.value)
                    } catch (error) {
                        rej(error)//如果有异常则返回拒绝的MyPromise对象
                    }
                })
            }
        })
    }

    catch(fn){
        return this.then(null,fn)
    }
}

//resolve方法
MyPromise.resolve = function(val){
    return new Promise((resolve,reject)=>{
        resolve(val)
    });
}
//reject方法
MyPromise.reject = function(val){
    return new Promise((resolve,reject)=>{
        reject(val)
    });
}
//race方法 
Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        for(let i=0;i<promises.length;i++){
        promises[i].then(resolve,reject)
        };
    })
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
    let arr = [];
    let i = 0;
    function processData(index,data){
        arr[index] = data;
        i++;
        if(i == promises.length){
        resolve(arr);
        };
    };
    return new Promise((resolve,reject)=>{
        for(let i=0;i<promises.length;i++){
        promises[i].then(data=>{
            processData(i,data);
        },reject);
        };
    });
}


//测试
console.log('同步-1')
let p = new MyPromise((res,rej)=>{
    console.log('初始化')
    setTimeout(() => {
        res('解决!')
        rej('拒绝!')//无效
    }, 1000);
})
console.log('同步-2')
p.then(
    (res)=>{console.log(res)},
    (err)=>{console.log(err)}
).then(
    (res)=>{console.log(res);},
    (err)=>{console.log(err)}
).catch(
    (err)=>{console.log(err)}
)
console.log('同步-3')

// 同步-1
// 初始化
// 同步-2
// 同步-3
// 解决!

MyPromise.resolve('Promise.resolve---解决!').then((res)=>{console.log(res)},(err)=>{console.log(err)})
MyPromise.reject('Promise.reject---拒绝!').then((res)=>{console.log(res)},(err)=>{console.log(err)})

// Promise.resolve---解决!
// Promise.reject---拒绝!

# 手把手一行一行代码教你“手写Promise

异步函数

让以同步代码方式写的代码能够异步执行。即用同步方式,执行异步操作

async

作用:声明异步函数

async function fn1(){}//函数声明

let fn2 = async function(){}//函数表达式

let fn3 = async ()=>{}//箭头函数

class Test{
  async fn4(){}//方法
}

使用async关键字可以使函数具备异步特性,但其代码仍然是同步求值的。参数和闭包方面与同步函数一致。

异步函数return了个啥?

期约:即通过Promise.resolve()包装的Promise对象。

async function fn1(){}//无返回
console.log(fn1())//Promise { undefined }   

async function fn2(){return '字符串!'}//有返回
console.log(fn2())//Promise { '字符串!' }

async function fn3(){throw '异常!'}//异步函数内部抛出错误,函数返回拒绝的期约
fn3().catch(console.log)//异常!

async function fn4(){Promise.reject('拒绝!')}//拒绝期约的错误不会被异步函数捕获
fn4().catch(console.log)//无返回--因为无法捕获

//返回期约-返回什么就是什么
async function fn5(){return Promise.resolve('解决!')}
async function fn6(){return Promise.reject('拒绝!')}
fn5().then(
  (data)=>{console.log(data)},//解决!
  (err)=>{console.log(err)}
)
fn6().then(
  (data)=>{console.log(data)},
  (err)=>{console.log(err)}//拒绝!
)

await

暂停异步函数代码执行,等待期约解决。

await必须在异步函数内部使用。

如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果,也就是说它会阻塞后面的代码,等待 Promise 对象结果。如果等待的不是 Promise 对象,则返回该值本身。

错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

async function test() {
  await Promise.reject('错误了')
};

test().then(res=>{
  console.log('success',res);
},err=>{
  console.log('err ',err);
})
// err 错误了

防止出错的方法,也是将其放在try...catch代码块之中。

async function test() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('错误了');
    });
  } catch(e) {
      console.log('err', e)
  }
  return await('成功了');
}

多个await命令后面的异步操作,如果不存在继发关系(即互不依赖),最好让它们同时触发。

let foo = await getFoo();
let bar = await getBar();
// 上面这样写法 getFoo完成以后,才会执行getBar

// 同时触发写法 ↓

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二   利用平行执行
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

其他

  • 异步函数性能强于Promise。

  • 可以使用生成器手写实现async/await

参考

async/await 优雅永不过时

7张图,20分钟就能搞定的async/await原理!为什么要拖那么久?