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优点:
-
解决回调地狱问题 ,有时我们要进行一些相互间有依赖关系的异步操作,比如有多个请求,后一个的请求需要上一次请求的返回结果。
-
更好地进行错误捕获,使用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更好?
-
它做到了真正的串行的同步写法,代码阅读相对容易
-
使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值。
-
对于条件语句和其他流程语句比较友好,可以直接写到判断条件里面 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