介绍
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
为什么会有Promise
单线程和异步
JS单线程,同一时间只能做一件事,可以避免DOM渲染冲突。
- 浏览器需要渲染DOM,JS可以修改DOM结构,所以JS执行的时候,浏览器DOM渲染会暂停,且两端JS也不能同时执行。
解决方案是异步。但常规的回调函数的解决方案存在问题。
- 不按照书写方式执行,可读性差
- 回调函数不容易模块化。回调地狱。
异步的发展历史
CallBack写法
CallBack意为“回调函数”,即异步操作执行完后触发执行的函数,例如:
$.get("http://api.xxxx.com/xxx",callback);
CallBack写法多层嵌套
项目要求前端ajax请求后端接口列表类型名称,然后在用类型名称ajax请求列表id,在用id请求列表具体内容,最后代码大概是这样的
$.ajax({
url: "type",
data:1,
success: function (a) { // 第一次回调
$.ajax({
url: "list",
data:a,
success: function (b) { // 第二次回调
$.ajax({
url: "content",
data:b,
success: function (c) { // 第三次回调
console.log(c)
}
})
}
})
}
})
回调函数和执行函数都是写在同一个函数里。可读性差,也不好复用
jquery中的deferred
在jquery1.5之后,把回调函数分离出来,并且可以执行链式操作
var ajax = $.ajax('data.json');
ajax.done(function() {
console.log('success1');
})
.fail(function() {
console.log('error');
})
.done(function() {
console.log('success1');
})
.fail(function() {
console.log('error');
})
//或者下面的写法
//很像Promise写法
var ajax = $.ajax('data.json')
ajax.then(function() {
console.log('success1');
}, function() {
console.log('error1');
})
.then(function() {
cosole.log('sucess2');
}, function() {
console.log('error2');
})
console.log(ajax); //返回一个XHR对象
- 这只是写法上的优化,改装了callback的写法,无法改变异步和单线程的本质
- 是一种语法糖,解耦了代码,并且很好的体现了开放封闭原则:对扩展开放,对修改封闭。
使用jquery Deferred。
原来的函数
var wait = function() {
var task = function () {
console.log('执行操作');
};
setTimeout(task, 2000);
}
wait();
使用deferred:
function waitHandle() {
var dtd = $.Deferred(); //创建一个dtd对象
var wait = function() {
var task = function (dtd) {
console.log('执行操作');
dtd.resolve(); //表示异步任务完成
//dtd.reject() //表示异步任务失败
}
setTimeout(task, 2000);
return dtd; //返回的是dtd
}
return wait(dtd); //dtd进行了一部分操作,又返回出去
}
var w = waitHandle();
w.then(function() {
console.log('ok1');
},function() {
console.log('error1');
}).then(function() {
console.log('ok2');
},function(){
console.log('error2');
})
dtd的API有两类,用意不同:
第一类,dtd.resolve()、 dtd.reject() ——主动执行的函数
第二类,dtd.then :dtd.done 、 dtd.fail ——被动监听的函数
初步引入Promise概念
function waitHandle() {
var dtd = $.Deferred(); //创建一个dtd对象
var wait = function() {
var task = function (dtd) {
console.log('执行操作');
dtd.resolve(); //表示异步任务完成
//dtd.reject() //表示异步任务失败
}
setTimeout(task, 2000);
return dtd.promise(); //返回的是dtd.promise()
}
return wait(dtd); //dtd进行了一部分操作,又返回出去
}
promise过滤掉了resolve和reject函数。只能调用监听的方法。所以外部的只能监听成功失败,而不能自己决定成功或者失败。
怎么实现一个Promise
PromiseA+规范,Promise要符合以下要求
- 实现then 方法,包含成功的回调和失败的回调
- then方法必须返回一个Promsie
- then可以链式调用多次
- Promise三种状态
- 等待(PENDING)
- 成功(RESOLVED)
- 失败(REJECTED)
- 初始状态为
PENDING,且其他状态只能由PENDING状态转移而来 根据上面要求实现一个简单的Promise
const PENDING = 'PENDING' // 等待的状态
const RESOLVED = 'RESOLVED' // 成功的状态
const REJECTED = 'REJECTED' // 失败的状态
class Promise {
constructor(executor) { // 传入的执行器函数
this.status = PENDING // 宏变量, 默认是等待态
this.value = undefined // then方法要访问到所以放到this上
this.reason = undefined // then方法要访问到所以放到this上
let resolve = (value) => { // 将状态设置为成功的函数
if (this.status === PENDING) {// 保证只有状态是等待态的时候才能更改状态
this.value = value // 成功的赋值
this.status = RESOLVED // 将状态设置为成功状态
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason
this.status = REJECTED
}
};
// 执行executor传入我们定义的成功和失败函数:把内部的resolve和reject传入executor中用户写的resolve, reject
try {
executor(resolve, reject)
} catch(e) {
console.log('catch错误', e)
reject(e) //如果内部出错 直接将error手动调用reject向下传递
}
}
then(onfulfilled, onrejected) {
if (this.status === RESOLVED) {
onfulfilled(this.value)
}
if (this.status === REJECTED) {
onrejected(this.reason)
}
}
}
测试下我们的代码
const p = new Promise((resolve, reject) => {
resolve('hello world')
})
p.then(val => {
console.log(val)
}, err => {
console.log('error', err)
})
// [Running] node "d:\skyan\demo\promise\promsie-1.js"
// hello world
// [Done] exited with code=0 in 1.602 seconds
通过上面不到40行代码我们实现了一个简单的promise,看上去也难,哈哈。接下来我们增加几行代码实现promise异步,方案就是发布订阅者模式,一起来吧
const PENDING = 'PENDING' // 等待的状态
const RESOLVED = 'RESOLVED' // 成功的状态
const REJECTED = 'REJECTED' // 失败的状态
class Promise {
constructor(executor) { // 传入的执行器函数
this.status = PENDING // 宏变量, 默认是等待态
this.value = undefined // then方法要访问到所以放到this上
this.reason = undefined // then方法要访问到所以放到this上
this.onResolvedCallbacks = [] // 存放成功的回调
this.onRejectedCallbacks = [] // 存放失败的回调函数
let resolve = (value) => { // 将状态设置为成功的函数
if (this.status === PENDING) {// 保证只有状态是等待态的时候才能更改状态
this.value = value // 成功的赋值
this.status = RESOLVED // 将状态设置为成功状态
this.onResolvedCallbacks.forEach(fn => fn())
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason
this.status = REJECTED
this.onResolvedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch(e) {
console.log('catch错误', e)
reject(e) //如果内部出错 直接将error手动调用reject向下传递
}
}
then(onfulfilled, onrejected) {
if (this.status === RESOLVED) {
onfulfilled(this.value) // 执行成功的函数
}
if (this.status === REJECTED) {
onrejected(this.reason) // 执行失败的函数
}
if(this.status === PENDING) {
this.onResolvedCallbacks.push(() => { // 存放成功的函数
onfulfilled(this.value)
})
this.onRejectedCallbacks.push(() => { // 存放失败的函数
onfulfilled(this.reason)
})
}
}
}
写个测试用例,完美、成功执行
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello world')
}, 1000);
})
p.then(val => {
console.log(val)
}, err => {
console.log('error', err)
})
// [Running] node "d:\skyan\demo\promise\promsie-2.js"
// hello world
// [Done] exited with code=0 in 1.916 seconds
经过上面一番操作,我们的Promise已经基本可以使用了,写出这些面试也可以了,哈哈。但想要拿高分,还要努力一番。接下来我们就实现Promsie的链式调用
const PENDING = 'PENDING' // 等待的状态
const RESOLVED = 'RESOLVED' // 成功的状态
const REJECTED = 'REJECTED' // 失败的状态
function resolvePromise(promise2, x, resolve, reject) {
// 判断上一个promise返回值x,普通值或者是Prmsie,
if((typeof x === 'object' && x !== null) || typeof x === 'function') {
let then = x.then // 获取x属性上的then方法
if(typeof then === 'function') { // 进一步判断x是不是promise
then.call(x, y => { // 如果有then方法,那么x大概就是个Promise,此时将控制权交给x
resolvePromise(promise2, y, resolve, reject) //resolve reject 都是promise2的
}, r => {
reject(r)
})
} else { // x 是个对象 例如 {a:1, then: 1}
resolve(x)
}
} else { // x 是个普通值
resolve(x)
}
}
class Promise {
constructor(executor) { // 传入的执行器函数
this.status = PENDING // 宏变量, 默认是等待态
this.value = undefined // then方法要访问到所以放到this上
this.reason = undefined // then方法要访问到所以放到this上
this.onResolvedCallbacks = [] // 存放成功的回调
this.onRejectedCallbacks = [] // 存放失败的回调函数
let resolve = (value) => { // 将状态设置为成功的函数
if (this.status === PENDING) {// 保证只有状态是等待态的时候才能更改状态
this.value = value // 成功的赋值
this.status = RESOLVED // 将状态设置为成功状态
this.onResolvedCallbacks.forEach(fn => fn())
}
}
let reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason
this.status = REJECTED
this.onResolvedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch(e) {
console.log('catch错误', e)
reject(e) //如果内部出错 直接将error手动调用reject向下传递
}
}
then(onfulfilled, onrejected) {
// 为了实现链式调用,创建一个新promise
let promise2 = new Promise((resolve, reject) => {
// 执行then中的方法,可能是一个普通值,也可能是一个promise
// 使用宏任务将代码放大下次执行, 否则取不到promise2
if(this.status === RESOLVED) {
setTimeout(() => {
try {
let x = onfulfilled(this.value) || this.value
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if(this.status === REJECTED) {
setTimeout(() => {
try {
let x = onrejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
// 处理异步情况
if(this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onfulfilled(this.value) || this.value
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onrejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
}
写个测试用例,一切都好
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello world-1')
}, 1000);
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello world-2')
}, 1000);
})
p1.then(val => {
console.log(val)
return p2
}, err => {
console.log('error', err)
}).then(val => {
console.log(val)
return p2
}, err => {
console.log('error', err)
})
/*
[Running] node "d:\skyan\demo\promise\promsie-3.js"
hello world-1
hello world-2
[Done] exited with code=0 in 1.805 seconds
*/
到这里我们的Promise已经基本实现,但是还有些小瑕疵,我们来完善一下,让它更符合PromiseA+规范
const PENDING = 'PENDING'
const RESOLVED = 'RESOLVED'
const REJECTED = 'REJECTED'
function resolvePromise(promise2, x, resolve, reject) {
if(x === promise2) { // 不能引用同一个对象,可能出现死循环
return reject(new TypeError('[TypeError: 循环引用promise]----'))
}
let called // promsieA+要求,防止多次调用
if((typeof x === 'object' && x !== null) || typeof x === 'function') {
try { // 将执行代码都放到trycatch防止,这样可以捕获到代码问题
let then = x.then
if(typeof then === 'function') {
then.call(x, y => {
if(called) return
called = true
resolvePromise(promise2, y, resolve, reject)
}, r => {
if(called) return
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if(called) return
called = true
reject(e)
}
} else {
resolve(x)
}
}
class Promise {
constructor(executor) {
this.status = PENDING
this.value = undefined
this.reason = undefined
this.onResolvedCallbacks = []
this.onRejectedCallbacks = []
let resolve = (value) => {
if(value instanceof Promise) { // // 如果value是一个promise,执行它的then方法,结束当前promsie实例
value.then(resolve, reject)
return
}
if(this.status === PENDING) {
this.status = RESOLVED
this.value = value
this.onResolvedCallbacks.forEach(fn => fn())
}
}
let reject = reason => {
if(reason instanceof Promise) {
console.log('error Promise')
reason.then(resolve, reject)
return
}
if(this.status === PENDING) {
this.status = REJECTED
this.reason = reason
this.onRejectedCallbacks.forEach(fn => fn())
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
then(onfulfilled, onrejected) {
// 成功回调和失败回调的边界函数判断
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : v => v
onrejected = typeof onrejected === 'function' ? onrejected : e => {throw e}
let promise2 = new Promise((resolve, reject) => {
if(this.status === RESOLVED) {
setTimeout(() => {
try {
let x = onfulfilled(this.value) || this.value
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if(this.status === REJECTED) {
setTimeout(() => {
try {
let x = onrejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if(this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onfulfilled(this.value) || this.value // promiseA+要求 如果没有返回promsie,则返回自身的promsie实例
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onrejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
catch(errCallback) { // catch 方法
return this.then(null, errCallback)
}
finally (f) { // finally方法
return this.then(value =>{
return Promise.resolve(f()).then(() => {
return value
})
}, err => {
return Promise.resolve(f()).then(() => {
throw err
})
})
}
}
// 异步测试
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(1000)
}, 1000);
})
promise.then(value => {
console.log('resolved', value)
}).catch(e => {
console.log('error', e)
})
// // 链式调用测试
// const p1 = new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('hello world')
// }, 100);
// })
// const p2 = new Promise((resolve, reject) => {
// setTimeout(() => {
// reject('hello world---')
// }, 100);
// })
// p1.then(val => {
// console.log('1:', val)
// return p2
// }).then (val => {
// console.log('2:',val)
// }, e => {
// console.log('error1', e)
// }).catch(e => {
// console.log('error2', e)
// })
// 循环引用测试
// const p1 = new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve('hello')
// }, 500);
// })
// const p2 = p1.then(() => {
// return p2
// })
// p2.then(() => {}, e => {console.log(e)})
// resolve reject Promise 测试
// const p = new Promise((resolve, reject) => {
// setTimeout(() => {
// reject(new Promise((resolve, reject) => {
// resolve('hello world')
// }))
// }, 1000);
// })
// p.then(val => {
// console.log('1:', val)
// }).catch(e => {
// console.log('error', e)
// })
至此,我们的Promise已经基本完成,120行的代码量,我们就实现了如此强大的Promise。看起来,也不是很难,但要熟练地手写出来,并且能保证正确的运行,还是要费一番功夫的,主要还是得多联系,写几遍自然就记住了,等面试的时候,问到Promise,自然也不在话下,分分钟实现一个Promsie,你就是最靓的仔。记得素质三连啊。还有一些Promise周边,没有实现,下次一定哟。