什么是Promise
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更加合理和强大
简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果
从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API.各种异步操作都可以用同样的方法进行处理
Promise对象特点
- 对象状态不受外界影响 :Promise代表一个异步操作,有pending(进行中),fulfilled(已成功),rejected(已失败)三种状态,只有异步操作的结果可以决定当前是哪种状态,其它任何操作都无法改变这个状态
- 状态一旦改变就不会再变,任何时候都可以得到这个结果。状态改变只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果
Promise缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
微任务相关
Promise上的then方法属于微任务
js是单线程语言,按照顺序自上而下执行,简单的说就是只有一条通道,在任务多的情况下,就会出现拥挤的情况,这种情况下就产生了“多线程”,但是这种“多线程”是通过单线程模仿的,也就是假的。那么就产生了同步任务和异步任务。
同步任务
立即执行的任务,在主线程上排队执行,前一个任务执行完毕才能执行后一个任务。即代码书写顺序和执行顺序一致
异步任务
不进入主线程,而是在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候读取执行。异步任务分为宏任务和微任务
宏任务
由宿主环境(浏览器、Node)规定的一般都是宏任务, 如: 定时器, ajax, 事件回调, script标签,I/O操作
注: 定时器不管定时多少毫秒都是异步,即使定义0,最小也是4毫秒
script作为宏任务的执行顺序:
如果有两个script代码块,执行第一个代码块里的同步任务–> 执行第一个代码块微任务 --> 执行第二个代码块里的同步任务 --> 执行第二个代码块的微任务 --> 执行第一个代码块的宏任务 --> 执行第二个代码块的宏任务
微任务
一般由ECMA规定的东西就是微任务, 如: promise.then/.catch/.finally;nextTick
有了宏任务为什么要微任务
任务队列特点是"先进先出",排在前面的事件会被主线程优先读取,如果出现优先级更高的任务也在队列中排队等着执行显然合理,所以引入微任务用来"插队",微任务比宏任务先执行,所有的微任务执行完毕后才会去执行宏任务
事件循环(Event Loop)
- 一开始整个脚本作为一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务(一个宏任务)执行完出队,检查微任务列表,有则依次执行,直到全部执行完
- 执行浏览器UI线程的渲染工作
- 检查是否有
Web Worker
任务,有则执行 - 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
宏任务与微任务的区别
- 执行时机:宏任务在每次事件循环中执行,微任务在每一个宏任务执行完毕,且调用栈为空之后,ui重新更新之前立即依次执行
- 宏任务执行过程中如果宏任务中又添加了一个新的宏任务到任务队列中,这个新的宏任务会等到下一次事件循环中执行;微任务执行过程中如果又添加了新的微任务,新的微任务会在本次微任务执行过程中被执行,直到微任务队列为空
执行顺序
- 同步任务 > 异步任务
- 同步任务 > 微任务 > 宏任务
手写实现 Promises/A+ 规范
规范
代码
const isFunction = obj => typeof obj === 'function'
const isObject = obj => !!(obj && typeof obj === 'object')
//从其它浏览器窗口接收到的promise可能与当前窗口不同,所以instanceof promise无法识别promise实例
//自己识别:先判断p是否是函数或对象,是否有then()方法,满足这些条件认定为thenable
//鸭子类型(Duck Typing):是一种设计风格,不是一种具体的类型。一个函数不会关心它传入参数的类型,只关心这个参数对应的对象有没有自己想要的方法和属性。如果有,就能运行。如果没有,就不能运行。这就像是我看到了一只鸟,只要它能像鸭子一样叫,像鸭子一样走路,有鸭子一样的白色羽毛,那么,无论它实际上是什么东西,我都认为它是鸭子
//thenable:任何含有then()方法的对象或函数
const isThenable = obj => (isFunction(obj) || isObject(obj)) && 'then' in obj
const isPromise = promise => promise instanceof Promise
//三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
//状态和val/reason,then被调用
function myPromise(f) {
this.state = PENDING
this.result = null
this.callbacks = []
//构造 onFulfilled 去切换到 fulfilled 状态,构造 onRejected 去切换到 rejected 状态
let onFulfilled = value => transition(this, FULFILLED, value)
let onRejected = reason => transition(this, REJECTED, reason)
//构造 resolve 和 reject 函数,在 resolve 函数里,通过 resolvePromise 对 value 进行验证
//配合 ignore 这个 flag,保证 resolve/reject 只有一次调用作用
let ignore = false
let resolve = value => {
if(ignore) return
ignore = true
resolvePromise(this, value, onFulfilled, onRejected)
}
let reject = reason => {
if(ignore) return
ignore = true
onRejected(reason)
}
//Promise本身是同步的立即执行函数
//将 resolve/reject 作为参数,传入 f 函数
try {
f(resolve, reject)
} catch(error) {//若 f 函数执行报错,该错误就作为 reject 的 reason 来用
reject(error)
}
}
const handleCallbacks = (callbacks, state, result) => {
while(callbacks.length) handleCallback(callbacks.shift(), state, result)
}
//对单个promise进行状态迁移
//只有pending状态可以切换状态,其它状态不可变
//状态变更时,异步清空所有callbacks
const transition = (promise, state, result) => {
if(promise.state !== PENDING) return
promise.state = state
promise.result = result
setTimeout(() => handleCallbacks(promise.callbacks, state, result), 0)
}
//then方法接受两个参数
//then方法的核心用途是构造下一个promise的result
myPromise.prototype.then = function(onFulfilled, onRejected) {
//then返回新的Promise对象
return new myPromise((resolve, reject) => {
let callback = { onFulfilled, onRejected, resolve, reject}
if(this.state === PENDING) {
this.callbacks.push(callback)
} else {
//在执行上下文堆栈仅包含平台代码之前,不得调用onFulfilled或onRejected
//就是说then是个异步任务(其实是微任务),这里用宏任务定时器代替
setTimeout(() => handleCallback(callback, this.state, this.result), 0)
}
})
}
//在当前promise和下一个promise之间进行状态传递
const handleCallback = (callback, state, result) => {
let {onFulfilled, onRejected, resolve, reject} = callback
//根据state状态判断走哪条路径
//判断onFulfilled/onRejected是否是函数,如果是,以他们的返回值作为下一个promise的result,如果不是,直接以当前promise的result作为下一个Promise的result
try {
if(state === FULFILLED) {
isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
} else if(state === REJECTED) {
isFunction(onRejected) ? resolve(onRejected(result)) : reject(result)
}
} catch (error) {//如果执行过程中抛错,这个错误作为下一个promise的rejected reason来用
reject(error)
}
}
//对特殊的result进行特殊处理
//一些特殊的val被resolve时要做特殊处理
const resolvePromise = (promise, result, resolve, reject) => {
//如果result是当前Promise本身就抛出TypeError错误(循环调用)
if(result === promise) {
let reason = new TypeError('Can not fufill prmise with itself')
return reject(reason)
}
//如果result是另一个promise,沿用state和result状态
if(isPromise(result)) {
return result.then(resolve, reject)
}
//如果result是thenable对象,先取then函数,再call then函数,重新进入 The Promise Resolution Procedure 过程
if(isThenable(result)) {
try {
let then = result.then
if(isFunction(then)) {
return new myPromise(then.bind(result)).then(resolve, reject)
}
} catch (error) {
return reject(error)
}
}
//非上述情况,这个result成为当前promise的result
resolve(result)
}
module.exports = myPromise
运行测试套件
- 安装promises-aplus-tests
npm install promises-aplus-tests -D
- 在代码
module.exports = myPromise
前面加上:
myPromise.deferred = function() {
let result = {};
result.promise = new myPromise(function(resolve, reject) {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
- 配置启动命令
{
"name": "promise",
"version": "1.0.0",
"description": "my promise",
"main": "myTest.js",
"scripts": {
"test": "promises-aplus-tests myTest"//myTest是js文件名
},
"author": "ITEM",
"license": "ISC",
"devDependencies": {
"promises-aplus-tests": "^2.1.2"
}
}
- 测试
npm run test
promise常用方法
reject()
定义
返回一个状态为已拒绝的 Promise
对象,并将给定的失败信息传递给对应的处理函数。
手写
Promise.myReject = function (value) {
return new Promise((_, reject) => {
reject(value)
})
}
resolve()
定义
返回一个状态由给定 value 决定的 Promise
对象。如果该值是 thenable(即,带有 then
方法的对象),返回的 Promise 对象的最终状态由 then 方法执行结果决定;否则,返回的 Promise 对象状态为已兑现,并且将该 value 传递给对应的 then 方法。
通常而言,如果你不知道一个值是否是 promise 对象,使用Promise.resolve(value)
来返回一个 Promise 对象,这样就能将该 value 以 promise 对象形式使用。
手写
Promise.myResolve = function (value) {
// 是Promise实例,直接返回即可
if (value && typeof value === 'object' && (value instanceof Promise)) {
return value
}
// 否则其他情况一律再通过Promise包装一下
return new Promise((resolve) => {
resolve(value)
})
}
all()
定义
这个方法返回一个新的 promise 对象,等到所有的 promise 对象都成功或有任意一个 promise 失败。
如果所有的 promise 都成功了,它会把一个包含 iterable 里所有 promise 返回值的数组作为成功回调的返回值。顺序跟 iterable 的顺序保持一致。
一旦有任意一个 iterable 里面的 promise 对象失败则立即以该 promise 对象失败的理由来拒绝这个新的 promise。
手写
Promise.MyAll = function (promises) {
let arr = [],
count = 0
return new Promise((resolve, reject) => {
promises.forEach((item, i) => {
Promise.resolve(item).then(res => {
arr[i] = res
count += 1
if (count === promises.length) resolve(arr)
}).catch(reject)
})
})
}
race()
定义
等到任意一个 promise 的状态变为已敲定。
当 iterable 参数里的任意一个子 promise 成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应处理函数,并返回该 promise 对象。
手写
Promise.MyRace = function (promises) {
return new Promise((resolve, reject) => {
// 这里不需要使用索引,只要能循环出每一项就行
for (const item of promises) {
Promise.resolve(item).then(resolve, reject)
}
})
}
allSettled()
定义
等到所有 promise 都已敲定(每个 promise 都已兑现或已拒绝)。
返回一个 promise,该 promise 在所有 promise 都敲定后完成,并兑现一个对象数组,其中的对象对应每个 promise 的结果。
手写
Promise.MyAllSettled = function (promises) {
let arr = [],
count = 0
return new Promise((resolve, reject) => {
const processResult = (res, index, status) => {
arr[index] = { status: status, val: res }
count += 1
if (count === promises.length) resolve(arr)
}
promises.forEach((item, i) => {
Promise.resolve(item).then(res => {
processResult(res, i, 'fulfilled')
}, err => {
processResult(err, i, 'rejected')
})
})
})
}
any()
定义
接收一个 promise 对象的集合,当其中的任意一个 promise 成功,就返回那个成功的 promise 的值。
手写
Promise.MyAny = function (promises) {
let arr = [],
count = 0
return new Promise((resolve, reject) => {
promises.forEach((item, i) => {
Promise.resolve(item).then(resolve, err => {
arr[i] = { status: 'rejected', val: err }
count += 1
if (count === promises.length) reject(new Error('没有promise成功'))
})
})
})
}
有关promise的代码输出结果题
🌰
const fn = () =>
new Promise((resolve, reject) => {
console.log(1);
resolve("success");
});
console.log("start");
fn().then(res => {
console.log(res);
});
⚠️如果new Promise() 被包裹在函数中,只有函数调用的时候才会执行
🌰
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
⚠️
- 从上到下,先执行new Promise中的函数,将setTimeout放进下一个宏任务队列
- 跳出new Promise,遇到promise1.then,放进微任务队列,因为promise状态还未发生改变,所以先不执行
- then()方法返回的是新Promise
- 执行同步代码
console.log('promise1', promise1)
,promise1的状态是pending - 执行同步代码
console.log('promise2', promise2)
,promise2的状态是pending - 遇到setTimeout,放进下一个宏任务队列
- 第一轮宏任务执行结束,微任务队列为空,因此开始执行第二轮宏任务
- 先执行第一个setTimeout,promise的状态变为resolved,保存结果,因为状态变更,所以promise1.then()被推进微任务队列
- 第一个setTimeout没有其它同步代码,执行微任务队列中的promise1.then,它抛出了一个错误并且将promise2的状态设置为rejected
- 第一个setTimeout执行完毕,开始执行第二个setTimeout
- 打印出’promise1’,promise1的状态是resolved
- 打印出’promise2’,promise2的状态是rejected
🌰
const promise = new Promise((resolve, reject) => {
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then1: ", res);
}).then(res => {
console.log("then2: ", res);
}).catch(err => {
console.log("catch: ", err);
}).then(res => {
console.log("then3: ", res);
})
⚠️
- catch不管被连接到哪里都能捕获上层未捕获过的错误
- catch()也会返回一个promise,因为这个promise没有返回值,所以打印出undefined
🌰
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
⚠️
resolve(1)
之后走的是第一个then方法,没有走到catch,第二个then得到第一个then的返回值- return 2 会被包装成
resolve(2)
🌰
Promise.resolve().then(() => {
return new Error('error!!!')
}).then(res => {
console.log("then: ", res)
}).catch(err => {
console.log("catch: ", err)
})
⚠️返回任意一个非promise的值都会被包裹成promise对象,return new Error('error!!!')
被包裹成return Promise.resolve(new Error('error!!!'))
,所以走入then()里而非catch()
将return new Error('error!!!')
换成return Promise.reject(new Error('error!!!'));
/throw new Error('error!!!')
会走进catch()中
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
⚠️.then或者.catch的参数期望是函数,传入非函数会发生值透传,前两个then传入的都不是函数,因此发生透传,将resolve(1)的值直接传到最后一个then
🌰
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
⚠️
- 由于无法知道
promise
的最终状态,所以finally
的回调函数中不接收任何参数 - 它最终返回的默认会是一个上一次的Promise对象值,不过如果在
finally
回调中throw
(或返回被拒绝的 promise)将以throw()
指定的原因拒绝新的 promise
🌰
function promise1 () {
let p = new Promise((resolve) => {
console.log('promise1');
resolve('1')
})
return p;
}
function promise2 () {
return new Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally1'))
promise2()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally2'))
⚠️为什么finally1在error之后输出:
- 先执行同步任务,再执行微任务
resolve('1')
确定了p的状态是resolved,之后会经过.then(res => console.log(res))
,由于是微任务,要等同步任务执行完,先放进微任务队列,因为还没执行then(),所以不会继续往then()的后面走reject('error')
确定了promise2中promise的状态是rejected,之后会经过.catch(err => console.log(err))
,同样放进微任务队列,因为还没执行,不会往链式调用的后面走- 本轮宏任务执行完(同步任务都执行了),执行微任务队列,先执行
.then(res => console.log(res))
,既然执行了,链式调用往后走遇到.finally(() => console.log('finally1'))
,放进微任务队列,在执行微任务队列里的.catch(err => console.log(err))
,链式调用遇到.finally(() => console.log('finally2'))
放进微任务队列 - 之后又依次执行finally1和finally2
🌰
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))
⚠️
all和race
传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then
的第二个参数或者后面的catch
捕获;但并不会影响数组中其它的异步任务的执行Promise.all().then()
结果中数组的顺序和Promise.all()
接收到的数组顺序一致
场景应用题
🌰循环打印红黄绿: 红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;三个灯不断交替重复亮灯,三个亮灯函数已经存在:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
🙌
function red() {
console.log("red");
}
function green() {
console.log("green");
}
function yellow() {
console.log("yellow");
}
const light = function (timer, cb) {
return new Promise(resolve => {
setTimeout(() => {
cb()
resolve()
}, timer)
})
}
const step = function () {
Promise.resolve().then(() => {
return light(3000, red)
}).then(() => {
return light(2000, green)
}).then(() => {
return light(1000, yellow)
}).then(() => {
return step()
})
}
step();
🌰用Promise实现每隔1秒输出1,2,3
🙌
const arr = [1, 2, 3]
arr.reduce((p, x) => {
return p.then(() => {
return new Promise(r => {
setTimeout(() => r(console.log(x)), 1000)
})
})
}, Promise.resolve())
🌰封装异步加载图片的方法
🙌
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log("一张图片加载完成");
resolve(img);
};
img.onerror = function() {
reject(new Error('Could not load image at' + url));
};
img.src = url;
});
有关文章
从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节