异步处理代码存在的困境
首先看下面一个示例代码,用户点击千层面按钮,程序等待两秒,然后告知用户取餐提醒
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button class="burger">千层面</button>
<script>
var burgerEL = document.querySelector(".burger");
// 异步任务
burgerEL.onclick = function (){
function excuteCode(callback) {
console.log("请稍等一下")
setTimeout(() => {
var order = burgerEL.textContent;
callback(order);
}, 2000)
}
excuteCode((data) => {
console.log(`你的${data}已经做好了`)
})
}
</script>
</body>
</html>
下面是优化代码,设置了千层面的余量,如果余量不足,则告知用户卖光了。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button class="burger">千层面</button>
<script>
var balance = 2
var burgerEL = document.querySelector(".burger");
var order = burgerEL.textContent;
// 异步任务
burgerEL.onclick = function () {
function excuteCode(successCallback, failCallback) {
if (balance >= 1) {
console.log("请稍等一下")
setTimeout(() => {
successCallback(order);
balance--;
}, 2000)
} else {
failCallback(order);
}
}
excuteCode((data) => {
console.log(`你的${data}已经做好了`)
}, (err) => {
console.log(`不好意思,${err}卖光了`)
})
}
</script>
</body>
</html>
当然上面这个我自己编写的小程序只有successCallback, failCallback两个传入参数,但是实际开发中肯定不会这么少,而且往往编写异步程序和编写执行代码的人不是一个人。
那么这个时候参数摆放的位置就很重要了,因为这直接决定了编写执行代码的人传入callback的顺序,例如一般人可能习惯于excuteCode(100, ( ) => { }, ( ) => { })
这种顺序,但是编写逻辑的人完全可以不按照这种顺序定义,可能让你先传入函数再传入变量,你就不舒服了。
这并不是说另一个人故意刁难,而是每个人的设计思路确实不一样,
困境在这个小例子中就可以窥见一斑,虽然人们存在一种共识或者规范,但是依然不能保证每个人编写的方式按照标准来,因此ES5之前的解决方案(上面的代码)不适用了,进而在ES6之后引出了promise。
Promise解决异步处理
既然矛盾出在传参的地方,那么干脆直接不需要传了,但是不传参怎么回调呢?
想要理解promise的处理方式,我先放一个伪代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button class="burger">千层面</button>
<script>
var balance = 2
var burgerEL = document.querySelector(".burger");
var order = burgerEL.textContent;
// 异步任务
burgerEL.onclick = function () {
function excuteCode() {
const caller = new Caller () // 取餐呼叫器
if (balance >= 1) {
console.log("请稍等一下")
setTimeout(() => {
caller.success(order)
balance--;
}, 2000)
} else {
caller.failure(order)
}
return caller;
}
caller.监听震动((data) => {
console.log(`$(data)卖光了 `)
})
caller.监听闪烁((err) => {
console.log(`$(err)卖光了 `)
})
}
</script>
</body>
</html>
这个伪代码于原来的差异在于不需要进行传参回调,而是创建了一个取餐呼叫器对象,然后在异步程序执行完毕后返回,因此,我们只需要调用这个对象的方法(震动代表成功,可以取餐;闪烁代表失败,无法取餐),就可以调用。
这实际上就是promise的逻辑,下面将伪代码替换成promise的实现:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button class="burger">千层面</button>
<script>
var balance = 2
var burgerEL = document.querySelector(".burger");
var order = burgerEL.textContent;
// 异步任务
burgerEL.onclick = function () {
function excuteCode() {
const promise = new Promise((resolve, reject) => {
// 一般将异步程序放在这个函数中,因为虽然放在外部的话也会被执行,但这样就无法调用resolve和reject
if (balance >= 1) {
console.log("请稍等一下")
setTimeout(() => {
resolve(order)
balance--;
}, 2000)
} else {
reject(order)
}
})
return promise;
}
const promise = excuteCode()
promise.then((data) => {
console.log(`${data}做好了`)
})
promise.catch((err) => {
console.log(`${err}卖光了`)
})
}
</script>
</body>
</html>
将其中promise的结构抽象出来就是:
JavaScript
const promise = new Promise((resolve, reject) => {
resolve()
reject()
})
promise.then(() => { }).catch(() => { })
Promise是一个类,可以翻译成 承诺、许诺 、期约。
当我们需要的时候,给予调用者一个承诺: 待会儿我会给你回调数据时,就可以创建一个Promise的对象口在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor。
这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、 reject。
当我们调用resolve回调函数时会执行Promise对象的then方法传入的回调函数当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数。
至此就是promise最核心的内容,可能刚上手很多人觉得还不如之前的方法,但是熟悉之后你就会习惯这种规范,体会到他的优势。
promise的各个状态
待定(pending) : 初始状态,既没有被兑现,也没有被拒绝;
✓ 当执行executor中的代码时,处于该状态;
已兑现(fulfilled) : 意味着操作成功完成;
✓ 执行了resolve时,处于该状态, Promise已经被兑现;
已拒绝(rejected) : 意味着操作失败;
✓ 执行了reject时,处于该状态, Promise已经被拒绝;
注意:promise的状态一旦被确定下来就不能再更改,也不能再执行下一个回调函数来改变状态。
关于then的返回值
先来看一个小例子:
JavaScript
<script>
const promise = new Promise((resolve, reject) => {
resolve("aaaa")
})
// 第一个promise对象
promise.then(res => {
console.log("第一个then", res)
}).then(res => {
console.log("第二个then", res)
})
// 第二个promise对象
promise.then(res => {
console.log("独立的then", res)
})
</script>
前面我们已经说过,then方法是可以进行链式调用的,那么上面这个小例子中总共有三个then方法,究竟哪些then方法会被成功执行呢?

看一下输出,三个then方法均被成功执行,但是细心的你一定发现输出的顺序好像有点奇怪,并且出现了一个undefined。
这是因为then方法执行后会返回一个promise对象,而第一个promise对象的第二个then方法等待的是上面一个then方法放回的promise对象的决议,而不是原先的promise的决议,以此类推。 这句话可能听起来有点绕,但这就是关键所在,好好理解一下。如果没有返回值,默认返回undefined。
因此当我们给第一个then方法编写一个特定的返回值,promise源码就会创建一个新的promise对象,并在对象内部用resolve包含这个返回值,进而后面一个then方法等待这个resolve方法的决议。
我们根据这个逻辑修改代码,就不会出现undefined错误了。
JavaScript
<script>
const promise = new Promise((resolve, reject) => {
resolve("aaaa")
})
// 第一个promise对象
promise.then(res => {
console.log("第一个then", res)
return "bbbbb"
}).then(res => {
console.log("第二个then", res)
})
// 第二个promise对象
promise.then(res => {
console.log("独立的then", res)
})
</script>
