Promise 是什么?它解决了什么问题?
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6 将其写进了语言标准,统一了用法,并原生提供了 Promise 对象。
所谓 Promise,简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上来说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的API,各种异步操作都可以用同样的方法进行处理。
为了解释 Promise 是什么,我在这里引用了 阮一峰 著作的 《ES6 标准入门》 书中对 Promise 的定义,大家可以把 Promise 理解为一个拥有状态的对象。那我们为什么需要 Promise 呢? 没有 Promise 就不能解决异步编程了吗,Promise 到底有什么优势? 为了解答这些问题,请看如下示例代码:
const fs = require('fs')
function succeed (callback) {
console.log('成功了', data)
callback()
}
fs.readFile('path', (err, data) => {
if (err) throw err;
fs.writeFile('path', data, (err, data) => {
if (err) throw err;
succeed((data) => {
console.log('')
})
})
})
上述代码运行于 node.js 环境,使用 fs 模块对文件进行异步的读取和写入的操作。所以没有 Promise 我们能使用异步编程吗,答案是肯定的。但是我相信大家一眼望去就能发现代码的缺点,第一点:回调函数嵌套太多,就是我们俗称的 回调地狱 ,第二点:必须要先指定回调函数,像上述代码中 fs 对象的 readFile 和 writeFile 方法, 在调用的时候就必须要先指定回调函数,关于这点,下文会细说。
Promise 的基本使用
Promise 对象一共有三种状态,分别是 Penging (进行中)、Fulfilled (已成功)、Rejected (已失败), Promise 对象的状态只能改变一次,要么从 Penging 状态变成 Fulfilled 状态,要么变成 Rejected,不可能从失败或成功状态改变成另一种状态。当状态改变时,Promise 会执行 then/catch 方法中和状态相对应的回调函数。
示例:
let p = new Promise((resolve, reject) => {
setTimeout(() => {
let time = new Date().getTime()
if ( time % 2 === 0 ) {
resolve(time)
return;
}
reject(time);
}, 1000)
})
p.then((data) => {
console.log('成功了', data)
}, (err) => {
console.log('失败了', err)
})
创建一个 Promise 对象 p ,并且传入一个执行器函数,在执行器函数中执行异步代码(使用 setTimeout 模拟),一秒后,如果时间戳是偶数就会调用 resolve 方法改变当前 p 对象的状态为成功状态,否则调用 reject 方法改变状态为失败状态。调用 p 对象的 then 方法指定成功和失败的回调函数,回调函数中的参数的值则是调用 resolve 或者 reject 方法传入的值,在这里是时间戳。
除了在 then 方法指定两个回调函数,还可以只指定一个成功的回调函数,失败的函数使用 catch 方法指定,Promise 对象是支持链式调用的
new Promise((resolve, reject) => {
setTimeout(() => {
let time = new Date().getTime()
if ( time % 2 === 0 ) {
resolve(time)
return;
}
reject(time);
}, 1000)
})
.then(data => {
console.log('成功了', data)
})
.catch(err => {
console.log('失败了', err)
})
使用 Promise 重写上述示例
new Promise((resolve, reject) => {
fs.readFile('path', (err, data) => {
if (err) throw err;
resolve(data);
})
})
.then(data => {
return new Promise((resolve, reject) => {
fs.writeFile('path', data, (err, data) => {
if (err) return reject(err);
resolve(data);
})
})
})
.then(data => {
succeed((data) => {
console.log('');
})
})
.catch(err => {
console.log(err)
})
通过 Promise 的链式调用进行重构之后,代码变得更加简洁易读。这里需要注意的是, Promise 对象可以有多个 then 方法,then 方法执行的规则是通过上一个 then 方法的返回值决定的,如果上一个返回值是基本数据类型、没有返回值、成功状态的 Promise 对象,后面的 then 则执行成功的回调。如果抛出了错误,或者返回失败状态的 Promise 对象,后面的 then 方法则执行失败的回调。
同步与异步
众所周知,js 是单线程的,那为何还可以执行异步代码呢?为了解答这个问题,我们不得不说说 js 的事件循环机制,js 确实是单线程的,只有一个主线程,所以代码可以被阻塞,大家可以试试 alert 方法。
那为什么不把 js 弄成多线程的呢?是实现起来有难度吗,并不是,多线程都有线程安全的问题,如果 js 是多线程的,那么操作 dom 将变得不安全,如果一个线程正在修改 dom 元素,另一个线程却在删除 dom 元素,那么 dom 元素将变得不安全。
虽然 js 执行器不是多线程的,但是浏览器是多线程的,浏览器有很多的模块,例如计时器模块、Ajax 模块等等,当执行器执行 js 代码的时候,如果遇到了定时器,就会把定时器交给浏览器的定时模块,让浏览器去计时,而js 继续执行代码,当计时器到了指定的时间之后,浏览器模块就会把定时器任务放进任务队列,当 js 调用栈空闲时,就会把任务队列中的任务取出来执行,这就是事件循环机制。如下图:
当执行栈遇到异步代码时,就会交给浏览器相关的模块,并且弹出调用栈,浏览器模块在适当的时机就会交给任务队列,在这个阶段,调用栈会继续往下执行,直到同步代码都被执行完,当调用栈空闲的时候,任务队列就会推送任务给调用栈。
graph LR
A[执行栈] --> |异步代码|B(浏览器模块)
B --> |在某些时刻|C(任务队列)
C --> |执行栈空闲|A
任务队列
现在我们都知道了任务队列,大概的知道了代码执行的顺序,那如果遇到两个异步代码呢,是按照放进任务队列的顺序执行还是依照什么顺序执行呢?
setTimeout(function () {
console.log(1)
}, 0)
new Promise(function (resolve, reject) {
resolve(2)
})
.then(console.log)
上述代码输出的结果是 2 1,由此可知当遇到两个异步任务的时候,并不是谁在前面就先执行谁。
任务队列把任务细分了两种,一种是微任务队列,一种是宏任务队列
代码执行的优先级:同步代码优先率最高,微任务其二,宏任务其次。在浏览器的环境中,Promise、MutationObserver 都是微任务队列,宏任务队列有 setTimeout, setInterval, Ajax, requestAnimationFrame 等等。
Promise 真的是异步的吗
其实 Promise 并不全是异步的,我们在new Promise的时候是同步的,then 才是异步的,其实这样的比喻并不是很准确。我们可以思考一下 setTimeout 是同步执行还是异步执行的,那大家会很确定的说是异步执行的。
setTimeout(() => {
console.log('异步执行')
}, 0)
setTimeout 其实是同步执行的,但是传给它的回调函数才是异步执行的,现在大家肯定很迷糊,new Promise到底是同步还是异步,答案是同步执行的,then 也是同步执行,但是传给then的回调函数并不是同步执行,而是异步执行,也就是放在了任务队列里面。
new Promise((resolve, reject) => {
console.log(1)
})
console.log(2)
打印顺序是 1、2
setTimeout(() => {
console.log(1)
}, 0)
console.log(2)
打印顺序是 2、1
看到这里我相信大家已经明白了。
自定义 Promise (typescript)
这里给大家贴上自定义Promise的代码,因为用文字逐行讲解我可能并不是很在行,大家只能自己去研究了。
enum Status {
PENDING = 'pending',
RESOLVED = 'resolved',
REJECTED = 'rejected'
}
interface Stack {
resolve (data: any): void,
reject (error: any): void
}
type ChangeStatus = (data: any) => void
class myPromise {
private data: any = null
private status: Status = Status.PENDING
private stackQueue: Array<Stack>= []
constructor(execute: (resolve: ChangeStatus, reject: ChangeStatus) => any) {
try {
execute(this.resolve.bind(this), this.reject.bind(this))
} catch (e) {
this.reject(e)
}
}
private resolve (data: any): void {
if (this.status !== Status.PENDING) return
this.status = Status.RESOLVED
this.data = data
for (const stackQueueElement of this.stackQueue) {
setTimeout(() => {
stackQueueElement.resolve(data)
}, 0)
}
}
private reject (data: any): void {
if (this.status !== Status.PENDING) return
this.status = Status.REJECTED
this.data = data
for (const stackQueueElement of this.stackQueue) {
setTimeout(() => {
stackQueueElement.reject(data)
}, 0)
}
}
public then (onResolve = (value: any): any => {}, onReject = (err: any): any => {throw err}): myPromise {
return new myPromise((resolve, reject): void => {
let self = this
function handleFn (callback: (data: any) => {}):void {
try {
let result = callback(self.data)
if (result instanceof myPromise) {
result.then(resolve, reject)
return
}
resolve(result)
} catch (e) {
reject(e)
}
}
if (this.status === Status.PENDING) {
this.stackQueue.push({
resolve(data): void {
handleFn(onResolve)
},
reject(error): void {
handleFn(onReject)
}
})
}
if (this.status === Status.RESOLVED) {
setTimeout(() => {
handleFn(onResolve)
}, 0)
}
if (this.status === Status.REJECTED) {
setTimeout(() => {
handleFn(onReject)
}, 0)
}
})
}
public catch (callback: (err: any) => any) {
return this.then(undefined, callback)
}
static resolve (value?: any): myPromise {
return new myPromise((resolve, reject) => {
if (value instanceof myPromise) {
value.then(resolve, reject)
return
}
resolve(value)
})
}
static reject (value?: any): myPromise {
return new myPromise((resolve, reject) => {
if (value instanceof myPromise) {
value.then(resolve, reject)
return
}
reject(value)
})
}
static all (myPromiseList: Array<myPromise>): myPromise {
const values: Array<any> = new Array(myPromiseList.length)
let successCount = 0
return new myPromise((resolve, reject) => {
myPromiseList.forEach((item, index) => {
item.then(data => {
values[index] = data
successCount ++
if (successCount === myPromiseList.length) {
resolve(values)
}
}, err => {
reject(err)
})
})
})
}
static race (myPromiseList: Array<myPromise>): myPromise {
const values: Array<any> = []
let successCount = 0
return new myPromise((resolve, reject) => {
myPromiseList.forEach((item, index) => {
item.then(data => {
values.push(data)
successCount ++
if (successCount === myPromiseList.length) {
resolve(values)
}
}, err => {
reject(err)
})
})
})
}
}