promise和async

288 阅读12分钟

Promise和async

promise

promise的基本使用及注意事项

  • Promise构造函数接收一个函数作为参数,该函数的两个参数分别是resolve和reject。当异步操作成功会调用resolve,随后调用实例指定的then回调;异步操作失败会调用reject,随后调用实例指定的catch回调。

  • resolve的参数会被then回调的参数接收到,reject抛出的错误会被catch回调的参数接收到。

  • promise内部如果抛出错误,是不会传递到外层,会被自己内部的机制消化掉,因此不会影响到外部代码的执行。这与普通函数和async函数不同。(详见下面错误机制)跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。即控制台报错,proimise链式结构内的不执行,但是外面的代码还是会继续执行。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);

// Uncaught (in promise) ReferenceError: x is not defined
// 123

  • 注意,调用resolve或reject并不会终结 Promise 的 参数函数 的执行。一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

官网推荐下面这样写

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})
  • 如果 Promise 状态已经变成resolved,再抛出错误是无效的。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
 new Promise((resolve, reject) => {
    resolve(111);
    throw new Error('error')
}).then(value => {
    console.log(value) //111
}).catch(error => {
    console.log(error)
})

注意下面代码和上面的区别,上面的错误不会在控制台上显示,因为已经resolve了状态不会再改变;throw new Error('error')相当于reject('error')。下面的错误会在控制台上显示,因为resolve了并没有终结promise参数函数的执行;下面的throw new Error('test')是由定时器执行的;Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。fix (什么叫未捕获的错误)?,即没有被catch的错误。 后面代码还是能执行么,可以的。

  const promise = new Promise(function(resolve, reject) {
        resolve('ok');
        setTimeout(function() {
            throw new Error('test')
        }, 0)
    });
    promise.then(function(value) {
        console.log(value)
    });
    //ok
    //报错

Promise.all()的用法 参数为promise数组,返回值也组成一个数组

const promises = [1, 2, 3].map(function(id) {
    return getJSON(`./post${id}.json`);
});

Promise.all(promises).then(function(posts) {
    console.log('posts', posts)
}).catch(function(error) {
    console.log(error);
});

Promise.all()可以确定所有请求都成功了,但是只要有一个请求失败,它就会报错,而不管另外的请求是否结束。如果有请求失败,也想获取到结果呢?可以在每一个promise中都用catch捕获。

Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败)

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function(results) {
    console.log(results);
});

再看一个例子体会一下它的错误机制吧

  • 下面代码中,catch()方法抛出一个错误,因为后面没有别的catch()方法了,导致这个错误不会被捕获,也不会传递到外层。
  • 这个传递到外层,是指promise外面。因为不会传递到外层,所以promise 外面代码还是会执行。但是then属于链式结构内,所以catch里面抛出错误后面的then方法不会执行。
//返回promise实例的函数
 const someAsyncThing = function() {
    return new Promise(function(resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2);
    });
};



someAsyncThing().then(function() {
    return someOtherAsyncThing();
}).catch(function(error) {
    console.log('oh no', error);
    // 下面一行会报错,因为 y 没有声明
    y + 2;
}).then(function() {
    //这里不会执行了 ,因为上面报错了
    console.log('carry on');
});
//这里会执行,因为catch抛出的错误不会传递到外层
console.log('carry on 我在promise外面')


//普通函数
function fn() {
    return x + 1;

}
fn(); //报错了,下面的代码不会执行。
console.log('222')

promise的应用场景

加载图片

// 一张
const preloadImage = function(path) {
    return new Promise(function(resolve, reject) {
        const image = new Image();
        image.onload = function() {
            resolve(image)
        };
        image.onerror = function() {
            reject(new Error('Could not load image at ' + path));
        };
        image.src = path;
    });
};

const baiduLogoUrl = 'https://www.baidu.com/img/flexible/logo/pc/result.png'
preloadImage(baiduLogoUrl).then((img) => {
    const body = document.getElementsByTagName('body')[0]
    body.appendChild(img)

}).catch(error => {
    console.log(error)
})


 // 多张

function preloadMultipleImage(pathArray) {
    return pathArray.map((v, i) => {
        return new Promise((resolve, reject) => {
            let img = []
            img[i] = new Image()
            img[i].src = pathArray[i]
            img[i].onload = function() {
                const body = document.getElementsByTagName('body')[0]
                console.log(`第${i+1}张加载完成`)
                body.appendChild(img[i])
                resolve(img[i])
            }
            img[i].onerror = function() {
                console.log('error', i + 1)
                reject(new Error('Could not load image at ' + pathArray[i]));
            };
        })
    })

}
let MultipleImg = [
    'https://www.baidu.com/img/flexible/logo/pc/result.png',
    'https://www.baidu.com/img/flexible/logo/pc/result.png'
];
const result = preloadMultipleImage(MultipleImg);
Promise.all(result).then((imgs) => {
    console.log('全部加载完成', imgs)
}).catch(error => {
    console.log(error)
})

顺带总结下图片上传的案例: 用户上传一张或多张图片并且展示在页面上

// 读取图片地址为base64 返回一个promise
export const readAsURL=(data:Blob)=>{
    return new Promise<FileReader['result']>((resolve) => {
        // 1创建一个FileReader的实例
        const reader=new FileReader();
        // 2调用readAsDataURL方法,将图片转成DataUrl格式
        reader.readAsDataURL(data);
        // 3调用onload方法返回图片地址
        reader.onload=function(){
            // 4记住图片地址存在result属性上
            resolve(reader.result)   
        }   
    })  
}
<!-- <input type="file" ref='file' > -->
<input type="file" ref='file' multiple>
<div id="img-container"></div>
<button @click="chooseFile">点我上传文件</button>


 methods: {
    chooseFile(){
        this.$refs.file.click();
    }
},

mounted () {
    const fileInput=this.$refs.file
    fileInput.addEventListener('change',function (){
         const imgContainer=document.getElementById('img-container')
        // 展示一张图片
        //   const file=this.files[0];
        //   readAsURL(file).then(res=>{
        //       const img=document.createElement('img')
        //       img.src=res
        //       imgContainer.appendChild(img)
        //   })


        // 展示多张图片
        const files=this.files
        Array.from(files).map((file)=>{
               readAsURL(file).then(res=>{
                const img=document.createElement('img');
                img.src=res
                imgContainer.appendChild(img)
          })
        })

    })
},

async的基本使用及注意事项

async函数让异步操作变得像同步代码的写法,可读性强。

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。async函数内部return语句返回的值,会成为then方法回调函数的参数;若没有返回值,则为undefined。

通过以下几个例子来感受一下吧:

  async function asyncFn() {
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(111)
        }, 2000)
    });
    // fix 为啥这里不打印,因为await后面的promise一直是pending状态呀,没有调用resolve或reject
    console.log(222)

}
const res = asyncFn()
console.log(res) 

//Promise {<pending>}
//111
//async函数在执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
  async function asyncFn() {
        await new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log(111)
                resolve(333)
            }, 2000)
        });
        console.log(222) //过2秒后先打印111,再打印222

    }
    //只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。若async函数没有返回值,则打印undefind
    asyncFn().then(value => {
        console.log(value) //这里打印是undefined
    })

 async function asyncFn() {
    return await new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(111)
            resolve(333)
        }, 2000)
    });
    console.log(222)

}
//若有返回值,则打印promise返回的结果
asyncFn().then(value => {
    console.log(value) //333
})

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

 async function fn() {
   throw new Error('出错啦')
}
fn().then(val => {
   console.log(val)
}).catch(error => {
   console.log(error)
})

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,(fix) 除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

下面这个例子实际只打印了111,第二个await并没有执行啊。

async function asyncFn() {
//因为await等待的是一个promise对象的执行结果,第一个await一直是pending状态,所以下面的代码是不会执行的。
    await new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(111)
        }, 2000)
    });
    await new Promise((resolve, reject) => {
        console.log(333)
    })
    console.log(222)

}
asyncFn().then(value => {
//这里也不会打印,因为只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数,上面async函数并没有resolve
    console.log(value) 
})

await命令后面是一个 Promise 对象,返回该对象的结果,可以定义一个参数接收。在promise中可以通过then方法获得该结果,即直接调用该async函数,返回一个promise,可以用then方法接收它的返回值。如果不是 Promise 对象,就直接返回对应的值。

来看一个例子吧:

 //实际例子:比如文件下载,
// 第一步:先点击下载按钮,调用后端下载接口,后端会返回下载的地址
// 第二步:获取到下载的地址后,调用端上的sdk获取下载的进度等。
        const download = (url) => {
            console.log(`这里拿到了后端返回的文件地址是${url},可以做一些别的处理逻辑了`)
        }
        async function handleDownloadFile() {
            response = await getJSON('./test.json')
            console.log('fileUrl', response);
            const fileUrl = response.url
            download(fileUrl)
        }
        handleDownloadFile();

通过await可以实现js的休眠效果,来看一个官网的例子吧:

 // 实现休眠效果
    function sleep(interval) {
        return new Promise(resolve => {
            setTimeout(resolve, interval);
        })
    }

    // 用法
    async function one2FiveInAsync() {
        for (let i = 1; i <= 5; i++) {
            console.log(i);
            await sleep(1000);
        }
    }

    one2FiveInAsync();

当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。 有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面, 这样不管这个异步操作是否成功,第二个await都会执行。

bad:

async function f() {
    await Promise.reject('出错了');
    return await Promise.resolve('hello world'); // 不会执行
}
f().then(val => {
    console.log(val)
})

good:

 async function f() {
    try {
        await Promise.reject('出错了');
    } catch (e) {
        console.log(e)
    }
    return await Promise.resolve('hello world'); // 会执行
}
f().then(val => {
    console.log(val)
})

与promise的对比一下吧:promise自己内部会消化错误,即哪怕出错也不会影响外面代码的执行。

 function fn() {
   return new Promise((resolve, reject) => {
       reject('出错了');
       //下面的111怎么还打印了,再来回顾一下吧:promise内部若没有return ,则后面的代码还是会执行。若上面的reject前面加return ,结果又是不一样的。
       console.log(111)
   })
}
console.log(222)
fn()
//222
//111
//报错

再比较下reject和throw Error()的区别吧:

function fn() {
   return new Promise((resolve, reject) => {
       throw new Error('出错啦')
       console.log(111)
   })

}
console.log(222)
fn()
//222
//报错
注意:这里不会再打印111啦,因为 throw new Error('出错啦')相当于return reject('出错啦')

错误处理 下面的例子使用try...catch结构,实现多次重复尝试。

const superagent = require('superagent');
const NUM_RETRIES = 3;

async function test() {
    let i;
    for (i = 0; i < NUM_RETRIES; ++i) {
        try {
            await superagent.get('http://google.com/this-throws-an-error');
            break;
        } catch (err) {}
    }
    console.log(i); // 3
}

test();

// 上面代码中,如果await操作成功,就会使用break语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。

使用注意点 第一点,前面已经说过,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。 第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。 来看下面代码的执行时间感受下吧:

   const getFoo = () => {
      return new Promise((resolve, reject) => {
          setTimeout(() => {
              resolve('getFoo')
          }, 2000)
      })
  }
  const getBar = () => {
      return new Promise((resolve, reject) => {
          setTimeout(() => {
                  resolve('getBar')
              },
              3000)
      })
  }

  // 继发关系,执行总耗时5s多
  async function fn() {
      const startTime = Date.now();
      let foo = await getFoo();
      let bar = await getBar();
      const endTime = Date.now();
          console.log(foo,bar)
      console.log('该程序执行总耗时', endTime - startTime)
  }
  fn()

如果写成并发执行,则只有3s多哦,有2中写法:

一:

  async function fn() {
      const startTime = Date.now();
      let [foo, bar] = await Promise.all([getFoo(), getBar()]);
      const endTime = Date.now();
      console.log(foo, bar)
      console.log('该程序执行总耗时', endTime - startTime)
  }
  fn()

二:

```
   async function fn() {
        const startTime = Date.now();
        // fooPromise barPromise是发送异步请求,没有先后顺序,同时触发,是同步
        let fooPromise = getFoo();
        let barPromise = getBar();
        // foo bar是等待结果,是异步
        let foo = await fooPromise;
        let bar = await barPromise;
        const endTime = Date.now();
        console.log(foo, bar)
        console.log('该程序执行总耗时', endTime - startTime)
    }
    fn()

```

自己在写代码验证的时候出现了一个问题,并没有触发异步等待,看看我的问题代码吧:

问题代码1:下面写法并没有触发异步等待

 // 下面的写法,resolve直接执行,不会等待3秒后执行。因为下面的写法不是定时器的回调函数,是自执行的
// 回调函数的定义就是  不会自执行而是等着被调用
// 像上面那样写  你只是定义一个箭头函数,那函数最终为啥会执行,是因为被定时器调用了
const getFoo = () => {
    return new Promise((resolve, reject) => {
        setTimeout(resolve('getFoo'), 3000)
    })
}
const getBar = () => {
    return new Promise((resolve, reject) => {
        setTimeout(resolve('getBar'), 2000)
    })
}
async function fn() {
    const startTime = Date.now();
    let foo = await getFoo();
    let bar = await getBar();
    const endTime = Date.now();
    console.log(foo, bar) //getFoo getBar
    console.log('该程序执行总耗时', endTime - startTime)
}
fn()

问题代码2:原本是想返回一个promise,结果返回了一个setTimeout的timerId,因为return的是一个定时器

 const getFoo = () => {
    return setTimeout(() => {
            Promise.resolve('getFoo')
        }, 3000)
}
 const getBar = () => {
    return setTimeout(() => {
            Promise.resolve('getBar')
        }, 3000)
}

async function fn() {
    const startTime = Date.now();
    let foo = await getFoo();
    let bar = await getBar();
    const endTime = Date.now();
    console.log(foo, bar) //1 ,2
    console.log('该程序执行总耗时', endTime - startTime) //基本是同步执行
}
fn()

第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。

    function dbFuc(db) { //这里不需要 async
        let docs = [{}, {}, {}];

        // 可能得到错误结果,并发执行
        docs.forEach(async function(doc) {
            await db.post(doc);
        });
    }

    async function dbFuc(db) {
        let docs = [{}, {}, {}];

        // 继发执行
        for (let doc of docs) {
            await db.post(doc);
        }
    }

    async function dbFuc(db) {
        let docs = [{}, {}, {}];

        await docs.reduce(async(_, doc) => {
            await _;
            await db.post(doc);
        }, undefined);
    }

    // 如果确实希望多个请求并发执行,可以使用Promise.all方法。
    // 当三个请求都会resolved时,下面两种写法效果相同。

    async function dbFuc(db) {
        let docs = [{}, {}, {}];
        let promises = docs.map((doc) => db.post(doc));

        let results = await Promise.all(promises);
        console.log(results);
    }

    // 或者使用下面的写法

    async function dbFuc(db) {
        let docs = [{}, {}, {}];
        let promises = docs.map((doc) => db.post(doc));

        let results = [];
        for (let promise of promises) {
            results.push(await promise);
        }
        console.log(results);
    }

    // 第四点,async 函数可以保留运行堆栈。 fix:这里自己也不太懂哦
    const a = () => {
        b().then(() => c());
    };

关于promise和async面试题

promise和async的优缺点比较(面试中有被问到)

Promise的优缺点

promise优点:

  1. 解决回调地狱问题 ,有时我们要进行一些相互间有依赖关系的异步操作,比如有多个请求,后一个的请求需要上一次请求的返回结果。

  2. 更好地进行错误捕获,使用catch

promise的缺点:

首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。

 let promise = new Promise(function(resolve, reject) {
    console.log('Promise');
     resolve();
 });

 promise.then(function() {
    console.log('resolved.');
 });

 console.log('Hi!');
// Promise
// Hi!
// resolved.

其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。即不影响外面代码的执行 第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

async和await的优缺点

为什么Async/Await更好?

  1. 它做到了真正的串行的同步写法,代码阅读相对容易

  2. 使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值。

  3. 对于条件语句和其他流程语句比较友好,可以直接写到判断条件里面 async/await无所谓优缺点:无法处理promise返回的reject对象,要借助try…catch…

promise为啥可以一直.then?

then() 、catch ()返回的是一个新的Promise实例, 因此可以无限链式。那么then() 、catch ()中有return的话,就可以把return后面的数据交给下一个链式结构的形参

    // new Promise((resolve, reject) => {
    //   resolve(111)
    // }).then(value1 => {
    //   console.log(value1) //111
    //   return value1
    // })
    //   .then(value2 => console.log(value2)) //111
    //   .then(value3 => console.log(value3)) //undefined

await与事件循环

    // 外面定义一个初始值,异步请求回来后改变值。外面有一个同步函数,入参是该值。猜猜外面的函数能否接收到ajax的值
        let num  = 0;
        const fn = async ()=>{
            const res = await new Promise(resolve=>{
                console.log('await')
                resolve({num:999})
            })
            console.log('num---async--before',num)
            num = res.num
            console.log('num---async--after',num)

        }
        fn();

        const fn2 = ()=>{
            console.log('fn2---num',num)  //0

        }
        fn2()

        console.log('num---outer',num)
        
        // await
        // fn2---num 0
        // num---outer 0
        // num---async--before 0
        // num---async--after 999

从上面示例中我们需要学习js事件循环机制。熟悉它,可以很方便的帮我们定位bug。

await右值类型区别

  • 接非 thenable 类型,会立即向微任务队列添加一个微任务then但不需等待
        async function test() {
            console.log(1);
            await 123
            console.log(2);
        }

        test();
        console.log(3);

        Promise.resolve()
            .then(() => console.log(4))
            .then(() => console.log(5))
            .then(() => console.log(6))
            .then(() => console.log(7));

        // 1 3 2 4 5 6 7 (遇到awit,添加一个微任务,先执行同步任务打印3,接着执行await后面的语句打印2
  • 接 thenable 类型,需要等待一个 then 的时间之后执行
 async function test() {
    console.log(1);
    await {
        then(cb) {
            cb();
        },
    };
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));

// 1 3 4 2 5 6 7 这里先打印了4,再打印2
  • Promise类型(有确定的返回值),会立即向微任务队列添加一个微任务then但不需等待
 async function test() {
    console.log(1);
    await new Promise((resolve, reject) => {
        console.log(9)
        resolve()
    })
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7))
// 1 9 3 2 4 5  6 7  打印了3后立即打印2