Share-meeting--Promise

什么是Promise

Promise在英语里的意思是:“承诺”,它表示如A调用一个长时间的任务B的时候,B将返回一个“承诺”给A,A不用关心整个实施过程,继续做自己的任务;当B实施完成的时候,会通过A,并将执行A之间的预先约定的回调。

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件监听——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise是一种封装和组合未来值的易于复用的机制

既然是对象,那么就有属性方法

属性

Promise.length:长度属性,其值总是为 1 (构造器参数的数目).

Promise.prototype:表示 Promise 构造器的原型.

方法

基本用法

1
2
3
4
new Promise((resolve, reject) => {
// 待处理的异步逻辑
// 处理结束后,调用resolve或reject方法
})

新建一个promise很简单,只需要new一个Promise对象即可。所以Promise本质上就是一个函数,它接受一个函数作为参数,并且会返回promise对象,这就给链式调用提供了基础。

三种状态

其实Promise函数的使命,就是构建出它的实例,并且负责帮我们管理这些实例。而这些实例有以下三种状态:

  1. pending: 初始状态,位履行或拒绝
  2. fulfilled: 意味着操作成功完成
  3. rejected: 意味着操作失败

Promise States

A promise must be in one of three states: pending, fulfilled, or rejected.

  • When pending, a promise:
    • may transition to either the fulfilled or rejected state.
  • When fulfilled, a promise:
    • must not transition to any other state.
    • must have a value, which must not change.
  • When rejected, a promise:
    • must not transition to any other state.
    • must have a reason, which must not change.

pending 状态的 Promise对象可能以 fulfilled状态返回了一个值,也可能被某种理由(异常信息)拒绝(reject)了。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers)就会被调用,then方法分别指定了resolve方法和reject方法的回调函数

alt promise图解

简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const promise = new Promise((resolve, reject) => {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
promise.then(value => {
// 如果调用了resolve方法,执行此函数
}, error => {
// 如果调用了reject方法,执行此函数
})

用Promise封装ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const request = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.send()
xhr.onload = () => resolve(JSON.parse(xhr.responseText))
xhr.onerror = () => reject(new Error(xhr.responseText))
})
}
request('http://update.babyeye.com/releases/upgrade.json')
.then(response => {
console.log(response)
})
.catch(err => {
console.log(err)
})

then

每一个 promise 都会提供给你一个 then() 函数 (或是 catch(),实际上只是 then(null, ...) 的语法糖)。它接收两个函数作为参数,then(onFilfulled, onRejected)onFilfulled是成功的操作,onRejected是失败的操作。我们在 then() 函数内部可以做三种事情:

  • return 另一个promise
  • return 一个同步的值(或者undefined)
  • throw一个同步异常

return另一个promise

1
2
3
4
5
6
7
8
9
10
11
12
request('1.txt')
.then(() => {
return request('2.txt')
})
.then(response => {
console.log(response) // 2222222222
})
.catch(err => {
console.log(err)
})
// 这里这里是return第二个promise,这个return非常重要。如果没有写return,那么request('2.txt')将会出问题,并且下一个函数将会接收到undefined,而不是2222222222,请看下面的例子
1
2
3
4
5
6
7
8
9
10
request('1.txt')
.then(() => {
request('2.txt') // 这里没有return
})
.then(response => {
console.log(response) // 这里的值将是undefined
})
.catch(err => {
console.log(err)
})

返回一个同步值(或者undefined)

1
2
3
4
5
6
7
8
9
10
11
12
13
request('1.txt')
.then(() => {
return 100 // 返回一个同步值
})
.then(response => {
console.log(response) // 100
})
.catch(err => {
console.log(err)
})
// 返回同步值相当于用Promise包裹了一层
// 上面的return 100 相当于 return Promise.resolve(100)

抛出同步异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
request('1.txt')
.then(() => {
a() // a未定义
return 100
})
.then(
response => {
console.log('then0000', response)
},
err => {
return err
})
.then(
response => {
console.log('then1111', response) // then1111 ReferenceError: a is not defined
},
err => {
console.log('then2222', err)
})
.catch(err => {
console.log('catch3333', err)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
request('1.txt')
.then(() => {
a()
return 100
})
.then(
response => {
console.log('then0000', response)
},
err => {
throw err
})
.then(
response => {
console.log('then1111', response)
},
err => {
console.log('then2222', err) // then2222 ReferenceError: a is not defined
})
.catch(err => {
console.log('catch3333', err)
})

为什么使用Promise

异步不能使用try…catch

try…catch当然很好,但是无法跨异步操作工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
request('1.txt') // 异步操作
.then(response => {
a() // 因为a没有定义
console.log(response)
})
}
try {
foo() // 所以执行到foo()的时候从a()抛出全局错误
} catch (err) {
console.log(err) // 永远不会到达这里
}

error-first

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function b() {
c() // c未定义,报错
return 'b'
}
function foo(callback) {
request('1.txt')
.then(response => {
try {
callback(null, b()) // b里面是同步代码,所以错误会被捕捉到
} catch (err) {
callback(err)
}
})
}
foo((err, data) => {
if (err) {
console.log('错误', err) // 错误 ReferenceError: c is not defined
} else {
console.log('成功', data)
}
})

捕获成功了! 但是我们接着往下看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function b() {
return request('2.txt')
}
function foo(callback) {
request('1.txt')
.then(response => {
try {
b().then(response => {
c() // c没有定义,会报错,但是不会被捕捉到
callback(null, response)
})
} catch (err) {
callback(err)
}
})
}
foo((err, data) => {
if (err) {
console.log('错误', err)
} else {
console.log('成功', data)
}
})
// Uncaught (in promise) ReferenceError: c is not defined

上面的代码有什么问题呢? 很明显,错误没有被捕获到

只有在try里面调用同步的立即成功或失败的情况下,这里的try…catch才会工作。如果try里面还有异步完成函数,其中的任何异步错误都无法捕捉到。

多级error-first回调交织在一起,再加上这些无所不在的if检查语句,都不可避免地导致了回调地狱的风险。

callback hell

callback hell

回调维护成本大

链式调用

1
2
3
4
5
6
7
8
9
10
11
12
// request为封装好的Promise
request('http://update.babyeye.com/releases/upgrade.json')
.then(response => {
console.log('获取第一个数据', response)
request('http://update.babyeye.com/releases/upgrade.json')
.then(response => {
console.log('获取第二个数据', response)
})
})
.catch(err => {
console.log(err)
})

问题:Promise回调地狱

正确的姿势

1
2
3
4
5
6
7
8
9
10
11
request('http://update.babyeye.com/releases/upgrade.json')
.then(response => {
console.log('获取第一个数据', response)
return request('http://update.babyeye.com/releases/upgrade.json')
})
.then(response => {
console.log('获取第二个数据', response)
})
.catch(err => {
console.log(err)
})

Promise.prototype.then方法返回的是一个新的Promise对象,因此可以采用链式写法。

1
2
3
4
5
6
7
8
9
10
11
12
const p = new Promise((resolve, reject) => {
resolve(100)
})
p
.then(response => {
console.log(response) // 100
return response * 2
})
.then(response => {
console.log(response) // 200
})

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 写法一
const promise = new Promise((resolve, reject) => {
try {
throw new Error('test')
} catch(e) {
reject(e)
}
})
promise.catch(error => {
console.log(error)
})
// 写法二
const promise = new Promise((resolve, reject) => {
reject(new Error('test'))
})
promise.catch(error => {
console.log(error)
})

比较上面两种写法,可以发现reject方法的作用,等同于抛出错误。

如果 Promise 状态已经变成resolved,再抛出错误是无效的。

1
2
3
4
5
6
7
const promise = new Promise((resolve, reject) =>{
resolve('ok')
throw new Error('test') // 错误不会被抛出,因为Promise的状态一旦改变就永久保持该状态不会再变了
})
promise
.then(value => console.log(value)) // ok
.catch(error => console.log(error))

catch

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// bad
promise.then(data => {
// success
}, err => {
// error
})
// good
promise
.then(data => {
// success
})
.catch(err => {
// error
})

吃掉错误

先看一段同步代码

1
2
3
console.log(a) // 会报错,因为a没有定义
setTimeout(() => console.log('123'), 100)
// 第一行代码报错,程序退出进程,终止脚本,所以不会输出 ”123“

再看Promise

1
2
3
4
5
const p = new Promise((resolve, reject) => {
resolve(a) // 会报错,因为a没有定义
})
setTimeout(() => console.log('123'), 100) // 依然打印出”123“
// 内部有语法错误。浏览器运行到错误的这一行,会打印出错误提示ReferenceError: a is not defined,但是不会退出进程、终止脚本执行,200 毫秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。

Promise.resolve

如果我们经常写下面这样的代码

1
2
3
new Promise((resolve, reject) => {
resolve(同步值);
}).then(/* ... */);

那么我用Promise.resolve会更加简洁

1
Promise.resolve(同步值).then(/* ... */)

Promise.resolve()常用于创建一个已完成的Promise,但是Promise.resolve(...)也会展开thenable值。在这种情况下,返回的Promise采用传入的这个thenable的最终决议值,可能是完成,也可能是拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fulfilled = {
then(cb) {
cb(42)
}
}
const rejected = {
then(cb, errCb) {
errCb('err')
}
}
const p1 = Promise.resolve(fulfilled) // p1是完成的promise
const p2 = Promise.resolve(rejected) // p2是拒绝的promise
console.log(p1) // [[PromiseStatus]]: "resolved" [[PromiseValue]]: 42
console.log(p2) // [[PromiseStatus]]: "rejected" [[PromiseValue]]: "err"
  • 如果向Promise.resolve(…)传递一个非Promise、非thenable的立即值,就会得到一个用这个值填充的promise。
1
2
3
4
5
6
const p1 = new Promise(resolve => {
resolve(100)
})
const p2 = Promise.resolve(100)
// 以上两种情况, promise p1和promise p2的行为是完全一样的
  • 如果向Promise.resolve(...)传递一个真正的Promise,就只会返回同一个promise
1
2
3
4
const p1 = Promise.resolve(100)
const p2 = Promise.resolve(p1)
p1 === p2 // true

Promise.reject

创建一个已被拒绝的Promise的快捷方式是使用Promise.reject(),所以以下两个promise是等价的:

1
2
3
4
const p1 = new Promise((resolve, reject) => {
reject('err')
})
const p2 = Promsie.reject('err')

Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.resolve([1, 2, 3])
.then(list => {
list.forEach(item => {
Promise.resolve(item)
.then(val => {
console.log('forEach' + val) // 同步
})
})
})
.then(result => {
console.log('遍历完了', result)
})
.catch(err => {
console.log(err)
})
// forEach1
// forEach2
// forEach3
// 遍历完了undefined

异步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Promise.resolve([1, 2, 3])
.then(list => {
list.forEach(item => {
Promise.resolve(item)
.then(n => {
request(`${n}.txt`)
.then(response => console.log(response)) // 异步
})
})
})
.then(result => {
console.log('遍历完了' + result)
})
.catch(err => {
console.log(err)
})
// 遍历完了undefined
// forEach1
// forEach2
// forEach3

那么我们怎么在Promise中使用forEach呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise.resolve([1, 2, 3])
.then(list => {
return Promise.all(list.map(item => request(item + '.txt')))
})
.then(([data1, data2, data3]) => {
console.log('全部请求完毕', data1)
console.log('全部请求完毕', data2)
console.log('全部请求完毕', data3)
})
.catch(err => {
console.log(err)
})
// 全部请求完毕 111111111111
// 全部请求完毕 222222222222
// 全部请求完毕 333333333333

Promise.all([...])来说,只有传入的所有promise都完成,返回promise才能完成。如果有任何promise被拒绝,返回的主promise就立即会被拒绝。如果完成的话,我们会得到一个数组,其中包含传入的所有promise完成值。对于拒绝的情况,我们只会得到一个拒绝promise的理由。

1
2
3
4
5
6
7
8
9
10
11
// 需求:在请求1.txt和2.txt之后再请求3.txt
const p1 = request('1.txt')
const p2 = request('2.txt')
Promise.all([p1, p2])
.then(([result1, result2]) => {
// 这里可以把p1,和p2完成后的值传入
return request('3.txt')
})
.then(result3 => {
console.log(result3)
})

Promise.all([...])需要一个参数,是一个数组通常由Promise实例组成。从Promise.all[(...)]调用返回的promise会收到一个完成消息,这是一个由所有传入promise的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)

严格来说,传给Promise.all([...])的数组中的值可以是Promise、thenable、甚至是立即值。就本质而言,列表中的每个值都会通过Promise.resolve(...)过滤,以确保要等待的是一个真正的Promise,所以立即值会被规范化为这个值构建的Promise。如果数组是空的,主Promise就会立即完成。

Promise.race

对于Promise.race(...)来说,一旦有任何一个Promise决议完成,Promise,race([...])就会完成,一旦有任何一个Promise决议为拒绝,它就会拒绝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const p1 = Promise.resolve(42)
const p2 = Promise.resolve('完成')
const p3 = Promise.reject('err')
Promise.race([p1, p2, p3])
.then(result => {
console.log(result) // 42
})
Promise.all([p1, p2])
.then(result => {
console.log(result) // [42, "完成"]
})
Promise.all([p1, p2, p3])
.then(result => {
console.log(result)
})
.catch(err => {
console.log('错误', err) // 错误 err
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const timeoutResolve = (time, value) => {
return new Promise((resolve, reject) => {
setTimeout(resolve, time, value)
})
}
const timeoutReject = (time, reason) => {
return new Promise((resolve, reject) => {
setTimeout(reject, time, reason)
})
}
console.log(timeoutResolve())
Promise.race([
timeoutResolve(1000, '1000毫秒resolve'),
timeoutResolve(40, '40毫秒resolve'),
timeoutReject(50, '50毫秒reject')
])
.then(
value => console.log('resolve', value),
reason => console.log('reject', reason)
)
// resolve 40毫秒resolve

注意:如果向Promise.all([...])传入空数组,它会立即完成,但Promise.race([...])会挂住,且永远不会决议。

穿透

1
2
3
4
5
6
7
Promise.resolve('foo')
.then(Promise.resolve('bar'))
.then(result => {
console.log(result)
})
// foo

我们以为会输出'bar',实际上却输出'foo',这是为什么呢?上面我们说到then函数接收两个函数作为参数,但是如果我们不传函数作为参数会怎么样呢?

它实际上会将其解释为 then(null),这就会导致前一个 promise 的结果会穿透下面。

1
2
3
4
5
6
7
Promise.resolve('foo')
.then(null)
.then(result => {
console.log(result)
})
// 这段代码和上一段效果是一样的
// foo

添加任意数量的 then(null),它依然会打印 foo

正确姿势

1
2
3
4
5
6
Promise.resolve('foo')
.then(() => Promise.resolve('bar'))
.then(result => {
console.log(result)
})
// bar

记住: 永远在then()中传递函数

期待async/await

更加语义化

参考文档

MDN

Promise/A+规范

ECMAScript 6 入门 – 阮一峰

你不知道的JavaScript:异步与性能 第三章-Promise


本文结束,感谢阅读。

本文作者:melody0z
本文链接:https://melodyvoid/JavaScript/Share-meeting--Promise.html
欢迎转载,转载请注明文本链接

坚持原创技术分享,您的支持将鼓励我继续创作!