es6+最佳入门实践(8)
8.Promise
8.1.什么是异步?
要理解异步,首先,从同步代码开始说
alert(1)
alert(2)
像上面的代码,执行顺序是从上到下,先后弹出1和2,这种代码叫做同步代码
alert(0)
setTimeout(function () {
alert(1);
}, 2000);
setTimeout(function () {
alert(2)
}, 1000);
alert(3)
上面代码的弹出顺序是 0 3 2 1 ,像这种不按从上到下依次执行的代码叫做异步代码,其实还有很多类似的异步代码,例如:ajax请求
ajax({
type:'get',
url: 'http://xxx.com/xxx',
success: function(result){}
})
console.log(111)
异步回调嵌套问题
setTimeout(function () {
alert(1)
setTimeout(function () {
alert(2)
setTimeout(function () {
alert(3)
}, 10)
}, 100)
}, 1000)
8.2.什么是Promise?
Promise是ES6中的异步编程解决方案,在代码中表现为一个对象,可以通过构造函数Promise来实例化,有了Promise对象,可以将异步操作以同步的流程表达出来,避免了回调地狱(回调函数层层嵌套)
直观的去看看Promise到底是什么
console.dir(Promise)
这样一看就很明白了,Promise是一个构造函数,它身上有几个方法,例如:reject、resolve、catch、all、race等方法就是我们常用的一些方法,还有then方法在它的原型上,也是非常常用的,后面我们会详细讲解这些方法
既然是构造函数,那么我们就可以使用new来调用一下,简单的使用
let p = new Promise((resolve, reject) => {
setTimeout(()=>{
//代码执行完成
console.log('代码执行完成');
resolve()
}, 1000)
})
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败),上面代码中传入的函数有两个参数,resolve和reject,这两个参数都是函数块,用于回调执行,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected,只有这两个结果可以去操作Promise的状态,其他任何操作都不能更改这个状态,这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。在初学阶段你可以简单的理解为resole就是异步执行成功后被调用的函数,reject是异步执行失败后调用的函数
注意: 上面代码中我们只是去new Promise() 得到一个实例,但是发现异步代码中的语句在1秒后被执行了,也就是说只要new Promise(), 那么promise里面的函数就会被立即执行,这是非常重要的一个细节,我们应该做到需要的时候去执行,而不是不管什么情况都去执行,因此,我们通常把上面的代码包到一个函数中去,需要的时候,调用一下函数就可以了
function AsyncFn() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
//代码执行完成
console.log('代码执行完成');
resolve()
}, 1000)
});
return p;
}
函数封装好后到底有什么用?在什么情况下用?resolve拿来做什么? 带着这些疑问,我们继续往下讲
在Promise的原型上有一个叫做then的方法,它的作用是为 Promise 实例添加状态改变时的回调函数,我们首先来看看then方法的位置
console.dir(Promise)
下面我们来具体使用这个then方法
function AsyncFn() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
//代码执行完成
console.log('代码执行完成');
resolve()
}, 1000)
});
return p;
}
AsyncFn().then(function () {
alert('异步代码执行完成后,该我执行了')
})
代码写到这里,我们已经能看出Promise的作用了,它其实已经可以把原来回调函数函数写到异步代码里的这种写法改变了,它已经把回调函数函数分离出来了,在异步代码执行完成后,通过链式调用的方式来执行回调函数函数,如果仅仅是向上面的代码只执行一次回调函数可能看不出Promise带来的好处,下面我们来看更复杂的代码
function AsyncFn1() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('异步代码1代码执行完成');
resolve()
}, 1000)
});
return p;
}
function AsyncFn2() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
//代码执行完成
console.log('异步代码2执行完成');
resolve()
}, 3000)
});
return p;
}
function AsyncFn3() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
//代码执行完成
console.log('异步代码3执行完成');
resolve()
}, 2000)
});
return p;
}
需求:AsyncFn3 是依赖于AsyncFn2的 AsyncFn2是依赖于AsyncFn1的,这就要求AsyncFn1执行完成后再执行AsyncFn2,AsyncFn2执行完成后执行AsyncFn3,这个时候怎么写?
function AsyncFn1() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('异步代码1代码执行完成');
resolve()
}, 1000)
});
return p;
}
function AsyncFn2() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
//代码执行完成
console.log('异步代码2执行完成');
resolve()
}, 3000)
});
return p;
}
function AsyncFn3() {
let p = new Promise((resolve, reject) => {
setTimeout(() => {
//代码执行完成
console.log('异步代码3执行完成');
resolve()
}, 2000)
});
return p;
}
//需求:AsyncFn3 是依赖于AsyncFn2的 AsyncFn2是依赖于AsyncFn1的,这就要求AsyncFn1执行完成后
// 再执行AsyncFn2,AsyncFn2执行完成后执行AsyncFn3,这个时候怎么写?
AsyncFn1().then(()=>{
alert('异步代码1执行完成后,该我执行了');
//上面代码执行完成后,返回一个Promise对象
return AsyncFn2()
}).then(()=>{
alert('异步代码2执行完成后,该我执行了');
return AsyncFn3()
}).then(()=>{
alert('异步代码3执行完成后,该我执行了');
})
到底为止,Promise的作用已经差不多可以理解了,它是ES6中的异步解决方案,可以将异步的代码以同步的形式表现出来,避免回调函数函数嵌套
如果理解了resolve的话,那么理解reject就比较容易了,它是异步代码执行失败后执行的回调函数。reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调
let oBtn = document.getElementById('btn');
let oResult = document.getElementById('result');
oBtn.onclick = () => {
AsyncGetData().then(() => {
oResult.innerHTML = '执行成功,获取到了数据。。。'
}, () => {
oResult.innerHTML = '<span style="color: red">执行失败,没有获取到数据。。。</span>'
})
};
function AsyncGetData() {
let num = Math.random() * 20;
return new Promise((resolve, reject) => {
setTimeout(() => {
if( num > 10){
resolve();
}else{
reject();
}
})
})
}
8.3.实例练习
1.异步加载图片
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="btn">获取图片</button>
<script>
const arr = [
'http://edu.nodeing.com/files/system/block_picture_1516366943.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516369814.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516977533.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516973104.jpg?version=8.2.15'
];
function AsyncLoadImg(url) {
let p = new Promise((resolve, reject) => {
let oImg = new Image();
oImg.src = url;
oImg.onload = () => {
resolve(oImg)
};
oImg.onerror = () => {
let error = new Error('图片加载失败');
reject(error)
}
});
return p;
}
let oBtn = document.getElementById('btn');
oBtn.onclick = () => {
//无序加载
// for (let i = 0; i < arr.length; i++) {
// AsyncLoadImg(arr[i]).then(function (oResult) {
// document.body.appendChild(oResult);
// })
// }
//按顺序加载
AsyncLoadImg(arr[0]).then((oResult) => {
oResult.title = '图片1';
document.body.appendChild(oResult);
return AsyncLoadImg(arr[1])
}).then((oResult) => {
oResult.title = '图片2';
document.body.appendChild(oResult);
return AsyncLoadImg(arr[2])
}).then((oResult) => {
oResult.title = '图片3';
document.body.appendChild(oResult);
return AsyncLoadImg(arr[3])
}).then((oResult) => {
oResult.title = '图片4';
document.body.appendChild(oResult);
})
}
</script>
</body>
</html>
8.4.Promise相关方法
1.catch的用法
catch方法和then的第二个参数作用差不多,都是用来指定异步执行失败后的回调函数函数的,不过,它还有一个功能就是如果在resolve中抛出错误,不会阻塞执行,而是可以在catch中捕获到错误
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="btn">模拟获取数据</button>
<p id="result"></p>
<script>
let oBtn = document.getElementById('btn');
let oResult = document.getElementById('result');
oBtn.onclick = () => {
AsyncGetData().then(() => {
oResult.innerHTML = '执行成功,获取到了数据。。。'
throw new Error('这里报错了')
}).catch((e) => {
console.log(e)
})
};
function AsyncGetData() {
let num = Math.random() * 20;
return new Promise((resolve, reject) => {
setTimeout(() => {
if( num > 10){
resolve();
}else{
reject();
}
})
})
}
</script>
</body>
</html>
2.all方法
all方法中传入一个数组,里面是多个Promise实例,只有当所有的Promise实例的状态变为fulfilled的时候,整体的状态才会变成fulfilled,这个时候每个Promise的实例返回的值会组成一个数组传给回调函数,如果整个数组中的Promise实例中有一个的状态是rejected,那么整体的状态都会是rejected,这个时候,第一个rejected实例的返回值会传给回调函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="btn">获取图片</button>
<script>
const arr = [
'http://edu.nodeing.com/files/system/block_picture_1516366943.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516369814.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516977533.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516973104.jpg?version=8.2.15'
];
function AsyncLoadImg(url) {
let p = new Promise((resolve, reject) => {
let oImg = new Image();
oImg.src = url;
oImg.onload = () => {
resolve(oImg)
};
oImg.onerror = () => {
let error = new Error('图片加载失败');
reject(error)
}
});
return p;
}
let oBtn = document.getElementById('btn');
oBtn.onclick = () => {
Promise.all([AsyncLoadImg(arr[0]),AsyncLoadImg(arr[1]),AsyncLoadImg(arr[2]),AsyncLoadImg(arr[3])])
.then((result) => {
// console.log(result)
for(let i in result){
document.body.appendChild(result[i]);
}
})
}
</script>
</body>
</html>
all方法通常适用于先加载资源,再执行操作的场景,例如:前面我们写的贪吃蛇项目,首先去加载地图、图片、以及声音等这些资源,等加载成功后再执行初始化
3.race方法
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="btn">获取图片</button>
<script>
const arr = [
'http://edus.nodeing.com/files/system/block_picture_1516366943.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516369814.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516977533.jpg?version=8.2.15',
'http://edu.nodeing.com/files/system/block_picture_1516973104.jpg?version=8.2.15'
];
function AsyncLoadImg(url) {
let p = new Promise((resolve, reject) => {
let oImg = new Image();
oImg.src = url;
oImg.onload = () => {
resolve(oImg)
};
});
return p;
}
//图片超时测试
function timeOut() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('图片超时'));
}, 1000)
})
}
let oBtn = document.getElementById('btn');
oBtn.onclick = () => {
let p = Promise.race([AsyncLoadImg(arr[0]), timeOut()])
.then((result) => {
document.body.appendChild(result)
}).catch((err) => {
console.log(err);
})
}
</script>
</body>
</html>