什么是promise?promsie的核心是什么?promise如何解决回调地狱的?等问题git
一、什么是promise?promise是表示异步操做的最终结果;能够用来解决回调地狱和并发IO操做的问题github
A promise represents the eventual result of an asynchronous operation.web
二、promise 的核心是什么?promise的核心就是链式调用数组
三、采用什么方法能够实现链式调用?经过使用then的方法,then方法是用来注册在这个Promise状态肯定后的回调,很明显,then方法须要写在原型链上。promise
四、promise是如何解决回调地狱的问题?(1)若是一个promise返回的是一个promise,会把这个promise传递结果传递到下一次的then中;(2)若是一个promise返回的是一个普通的值,会把这个普通值做为下一次then的成功回调结果;(3)若是当前promise失败了,会走下一个then的回调函数;(4)若是then不返回值,就会有一个默认值为undefined,做为普通值,会做为下一个then的成功回调;(5)catch是错误没有处理的状况才会执行;(6)then中能够不写东西浏览器
一、只有一个then方法,没有catch,race,all等方法,甚至没有构造函数;并发
Promise标准中仅指定了Promise对象的then方法的行为,其它一切咱们常见的方法/函数都并无指定,包括catch,race,all等经常使用方法,甚至也没有指定该如何构造出一个Promise对象,另外then也没有通常实现中(Q, $q等)所支持的第三个参数,通常称onProgress框架
二、then方法返回一个新的Promise;异步
Promise的then方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释async
promise2 = promise1.then(alert) promise2 != promise1 // true
三、不一样Promise的实现须要能够相互调用(interoperable)
四、Promise的初始状态为pending,它能够由此状态转换为fulfilled(本文为了一致把此状态叫作resolved)或者rejected,一旦状态肯定,就不能够再次转换为其它状态,状态肯定的过程称为settle
由于标准并无指定如何构造一个Promise对象,因此咱们一样以目前通常Promise实现中通用的方法来构造一个Promise对象,也是ES6原生Promise里所使用的方式,即:
// Promise构造函数接收一个executor函数,executor函数执行完同步或异步操做后,调用它的两个参数resolve和reject var promise = new Promise(function(resolve, reject) { /* 若是操做成功,调用resolve并传入value 若是操做失败,调用reject并传入reason */ })
咱们先实现构造函数的框架以下:
function Promise(executor) { var self = this self.status = 'pending' // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面 executor(resolve, reject) // 执行executor并传入相应的参数 }
上面的代码基本实现了Promise构造函数的主体,但目前还有两个问题:
一、咱们给executor函数传了两个参数:resolve和reject,这两个参数目前尚未定义
二、executor有可能会出错(throw),相似下面这样,而若是executor出错,Promise应该被其throw出的值reject:
new Promise(function(resolve, reject) { throw 2 })
因此咱们须要在构造函数里定义resolve和reject这两个函数:
function Promise(executor) { var self = this self.status = 'pending' // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,由于在Promise结束以前有可能有多个回调添加到它上面 function resolve(value) { // TODO } function reject(reason) { // TODO } try { // 考虑到执行executor的过程当中有可能出错,因此咱们用try/catch块给包起来,而且在出错后以catch到的值reject掉这个Promise executor(resolve, reject) // 执行executor } catch(e) { reject(e) } }
有人可能会问,resolve和reject这两个函数能不能不定义在构造函数里呢?考虑到咱们在executor函数里是以resolve(value),reject(reason)的形式调用的这两个函数,而不是以resolve.call(promise, value),reject.call(promise, reason)这种形式调用的,因此这两个函数在调用时的内部也必然有一个隐含的this,也就是说,要么这两个函数是通过bind后传给了executor,要么它们定义在构造函数的内部,使用self来访问所属的Promise对象。因此若是咱们想把这两个函数定义在构造函数的外部,确实是能够这么写的:
function resolve() { // TODO } function reject() { // TODO } function Promise(executor) { try { executor(resolve.bind(this), reject.bind(this)) } catch(e) { reject.bind(this)(e) } }
可是众所周知,bind也会返回一个新的函数,这么一来仍是至关于每一个Promise对象都有一对属于本身的resolve和reject函数,就跟写在构造函数内部没什么区别了,因此咱们就直接把这两个函数定义在构造函数里面了。不过话说回来,若是浏览器对bind的所优化,使用后一种形式应该能够提高一下内存使用效率。
另外咱们这里的实现并无考虑隐藏this上的变量,这使得这个Promise的状态能够在executor函数外部被改变,在一个靠谱的实现里,构造出的Promise对象的状态和最终结果应当是没法从外部更改的。
接下来,咱们实现resolve和reject这两个函数
function Promise(executor) { // ... function resolve(value) { if (self.status === 'pending') { self.status = 'resolved' self.data = value for(var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallback[i](value) } } } function reject(reason) { if (self.status === 'pending') { self.status = 'rejected' self.data = reason for(var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason) } } } // ... }
基本上就是在判断状态为pending以后把状态改成相应的值,并把对应的value和reason存在self的data属性上面,以后执行相应的回调函数,逻辑很简单,这里就很少解释了。
then方法是用来注册这个promise肯定状态后的回调,then方法是须要写在原型链上。
天然约束:then方法会返回一个Promise,关于这一点,Promise/A+标准并无要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(https://promisesaplus.com/dif...,因此在咱们的实现中,也让then返回一个新的Promise对象。
下面咱们来实现then方法:
// then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调 Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 // 根据标准,若是then的参数不是function,则咱们须要忽略它,此处以以下方式处理 onResolved = typeof onResolved === 'function' ? onResolved : function(v) {} onRejected = typeof onRejected === 'function' ? onRejected : function(r) {} if (self.status === 'resolved') { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === 'rejected') { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === 'pending') { return promise2 = new Promise(function(resolve, reject) { }) } }
Promise总共有三种可能的状态,咱们分三个if块来处理,在里面分别都返回一个new Promise。
根据标准,咱们知道,对于以下代码,promise2的值取决于then里面函数的返回值:
promise2 = promise1.then(function(value) { return 4 }, function(reason) { throw new Error('sth went wrong') })
若是promise1被resolve了,promise2的将被4
resolve,若是promise1被reject了,promise2将被new Error('sth went wrong')
reject,更多复杂的状况再也不详述。
try { module.exports = Promise } catch (e) {} function Promise(executor) { var self = this self.status = 'pending' self.onResolvedCallback = [] self.onRejectedCallback = [] function resolve(value) { if (value instanceof Promise) { return value.then(resolve, reject) } setTimeout(function() { // 异步执行全部的回调函数 if (self.status === 'pending') { self.status = 'resolved' self.data = value for (var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallback[i](value) } } }) } function reject(reason) { setTimeout(function() { // 异步执行全部的回调函数 if (self.status === 'pending') { self.status = 'rejected' self.data = reason for (var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason) } } }) } try { executor(resolve, reject) } catch (reason) { reject(reason) } } function resolvePromise(promise2, x, resolve, reject) { var then var thenCalledOrThrow = false if (promise2 === x) { return reject(new TypeError('Chaining cycle detected for promise!')) } if (x instanceof Promise) { if (x.status === 'pending') { //because x could resolved by a Promise Object x.then(function(v) { resolvePromise(promise2, v, resolve, reject) }, reject) } else { //but if it is resolved, it will never resolved by a Promise Object but a static value; x.then(resolve, reject) } return } if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { try { then = x.then //because x.then could be a getter if (typeof then === 'function') { then.call(x, function rs(y) { if (thenCalledOrThrow) return thenCalledOrThrow = true return resolvePromise(promise2, y, resolve, reject) }, function rj(r) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(r) }) } else { resolve(x) } } catch (e) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(e) } } else { resolve(x) } } Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return v } onRejected = typeof onRejected === 'function' ? onRejected : function(r) { throw r } if (self.status === 'resolved') { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onResolved try { var x = onResolved(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === 'rejected') { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onRejected try { var x = onRejected(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === 'pending') { // 这里之因此没有异步执行,是由于这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已经是异步执行,构造函数里的定义 return promise2 = new Promise(function(resolve, reject) { self.onResolvedCallback.push(function(value) { try { var x = onResolved(value) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) self.onRejectedCallback.push(function(reason) { try { var x = onRejected(reason) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) }) } } Promise.prototype.catch = function(onRejected) { return this.then(null, onRejected) } Promise.deferred = Promise.defer = function() { var dfd = {} dfd.promise = new Promise(function(resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) return dfd }
// 原生的Promise.resolve使用 Promise.resolve('hello swr').then((data)=>{ // 直接把成功的值传递给下一个then console.log(data) // hello swr }) // 那么Promise.resolve内部是怎么实现的呢? Promise.resolve = function(value){ return new Promise((resolve,reject)=>{ // 在内部new一个Promise对象 resolve(value) }) } // 同理,Promise.reject内部也是相似实现的 Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason) }) }
// 原生Promise的catch使用 Promise.reject('hello swr').catch((e)=>{ console.log(e) // hello swr }) // 上面这段代码至关于下面这段代码 Promise.reject('hello swr').then(null,(e)=>{ // then里直接走了失败的回调 console.log(e) // hello swr }) // 内部实现 Promise.prototype.catch = function(onRejected){ return this.then(null,onRejected) // 至关于then里的成功回调只传个null }
// 原生Promise.all的使用 // 假设1.txt内容为hello 2.txt内容为swr let fs = require('fs') function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) }) } Promise.all([read('./1.txt','utf8'),read('./2.txt','utf8')]).then((data)=>{ console.log(data) // 所有读取成功后返回 ['hello','swr'] // 须要注意的是,当其中某个失败的话,则会走失败的回调函数 })
promise.all内部实现
Promise.all = function(promises){ // promises 是一个数组 return new Promise((resolve,reject)=>{ let arr = [] let i = 0 function processData(index,data){ arr[index] = data // 5.咱们能用arr.length === promises.length来判断请求是否所有完成吗? // 答案是不行的,假设arr[2] = 'hello swr' // 那么打印这个arr,将是[empty × 2, "hello swr"], // 此时数组长度也是为3,而数组arr[0] arr[1]则为空 // 那么换成如下的办法 if(++i === promises.length){ // 6.利用i自增来判断是否都成功执行 resolve(arr) // 此时arr 为['hello','swr'] } } for(let i = 0;i < promises.length;i++){ // 1.在此处遍历执行 promises[i].then((data)=>{ // 2.data是成功后返回的结果 processData(i,data) // 4.由于Promise.all最终返回的是一个数组成员按照顺序排序的数组 // 并且异步执行,返回并不必定按照顺序 // 因此须要传当前的i },reject) // 3.若是其中有一个失败的话,则调用reject } }) }
// 原生Promise.race的使用 // 一个成功就走成功的回调,一个失败就走失败的回调 Promise.race([read('./1.txt','utf8'),read('./2.txt','utf8')]).then((data)=>{ console.log(data) // 可能返回 'hello' 也可能返回 'swr' 看哪一个返回快就用哪一个做为结果 }) // 内部实现 Promise.race = function(promises){ // promises 是一个数组 return new Promise((resolve,reject)=>{ for(let i = 0;i < promises.length;i++){ promises[i].then(resolve,reject) // 和上面Promise.all有点相似 } }) }
这个语法糖能够简化一些操做,好比:
let fs = require('fs') // 写法一: function read(filePath,encoding){ // 这里的new Promise依然是传递了一个executor回调函数 // 咱们该怎样减小回调函数嵌套呢? return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve(data) }) }) } // 写法二: // 这样的写法减小了一层回调函数的嵌套 function read(filePath,encoding){ let dfd = Promise.defer() fs.readFile(filePath,encoding,(err,data)=>{ if(err) dfd.reject(err) dfd.resolve(data) }) return dfd.promise } read('./1.txt','utf8').then((data)=>{ console.log(data) })
promise的核心在于:链式调用。
promise主要解决两个问题:
(1)回调地狱
(2)并发的异步IO操做,同一时间内把这个结果拿到,好比有两个异步io操做,当这2个获取完毕后,才执行相应的代码
一、回调地狱怎么解决
那么咱们来看下面的代码,而且改成promise。
// 回调函数 let fs = require('fs') fs.readFile('./a.txt','utf8',(err,data)=>{ // 往fs.readFile方法传递了第三个为函数的参数 if(err){ console.log(err) return } console.log(data) }) // 改写为Promise let fs = require('fs') function read(filePath,encoding){ return new Promise((resolve,reject)=>{ fs.readFile(filePath,encoding,(err,data)=>{ if(err) reject(err) resolve() }) }) } read('./a.txt','utf8').then((data)=>{ // 在这里则再也不须要传回调函数进去,而是采用then来达到链式调用 console.log(data) },(err)=>{ console.log(err) }) 这样看好像Promise也没什么优点,那么接下来咱们对比一下 // 假设有3个文件 // - 1.txt 文本内容为'2.txt' // - 2.txt 文本内容为'3.txt' // - 3.txt 文本内容为'hello swr' // 用回调函数 fs.readFile('./1.txt','utf8',(err,data)=>{ fs.readFile(data,'utf8',(err,data)=>{ fs.readFile(data,'utf8',(err,data)=>{ console.log(data) // hello swr }) }) }) // 用Promise read('./1.txt','utf8') .then((data)=>{ // 1.若是一个promise执行完后,返回的仍是一个promise, // 会把这个promise的执行结果会传递给下一次then中 return read(data,'utf8') }) .then((data)=>{ return read(data,'utf8') }) .then((data)=>{ // 2.若是在then中返回的不是一个promise, // 而是一个普通值,会将这个普通值做为下次then的成功的结果 return data.split('').reverse().join('') }) .then((data)=>{ console.log(data) // rws olleh // 3.若是当前then中失败了,会走下一个then的失败回调 throw new Error('出错') }) .then(null,(err)=>{ console.log(err) // Error:出错 报错了 // 4.若是在then中不返回值,虽然没有显式返回, // 可是默认是返回undefined,是属于普通值,依然会把这个普通值传到 // 下一个then的成功回调中 }) .then((data)=>{ console.log(data) // undefined })
从上面能够看得出,改写为Promise的代码,更好阅读和维护,从用Promise方式能够得出结论:
(1)若是一个promise执行完后,返回的是一个promise,会将这个promise的执行结果传递给下一个then回调成功中;
(2)若是在then中返回的不是一个promise,而是一个普通的值,会将这个普通的值传到下一个then成功回调中;
(3)若是当时then中失败了,会走下一个then的回调失败;
(4)若是then不返回值,可是默认是返回undefined的,属于普通值,会将这个普通值传到下一个then成功回调中。
若是在then中抛出错误,会怎么样呢?
状况1:会被下一个then中的失败回调捕获
// 情景一,会被下一个then中的失败回调捕获 read('./1.txt','utf8') .then((data)=>{ throw new Error('出错了') }) .then(null,(err)=>{ console.log(err) // Error:出错了 报错 })
状况2:没有被失败回调捕获,抛出错误最终会变成异常
read('./1.txt','utf8') .then((data)=>{ throw new Error('出错了') })
状况3:若是没有被失败的回调捕获,那么最终会被catch捕获到
read('./1.txt','utf8') .then((data)=>{ throw new Error('出错了') }) .then((data)=>{ }) .catch((err)=>{ console.log(err) // Error:出错了 报错 })
状况4:若是被失败的回调捕获,那么就不会被catch捕获到
read('./1.txt','utf8') .then((data)=>{ throw new Error('出错了') }) .then(null,(err)=>{ console.log(err) // Error:出错了 报错 }) .catch((err)=>{ console.log(err) // 不会执行到这里 })
(5)catch是错误没有处理的状况下才会执行
(6)then回调中能够不写
可能各位看官会以为奇怪,Promise能有什么性能问题呢?并无大量的计算啊,几乎都是处理逻辑的代码。
理论上说,不能叫作“性能问题”,而只是有可能出现的延迟问题。什么意思呢,记得刚刚咱们说须要把4块代码包在setTimeout里吧,先考虑以下代码:
var start = +new Date() function foo() { setTimeout(function() { console.log('setTimeout') if((+new Date) - start < 1000) { foo() } }) } foo()
运行上面的代码,会打印出多少次'setTimeout'呢,各位能够本身试一下,不出意外的话,应该是250次左右,我刚刚运行了一次,是241次。这说明,上述代码中两次setTimeout运行的时间间隔约是4ms(另外,setInterval也是同样的),实事上,这正是浏览器两次Event Loop之间的时间间隔,相关标准各位能够自行查阅。另外,在Node中,这个时间间隔跟浏览器不同,通过个人测试,是1ms。
单单一个4ms的延迟可能在通常的web应用中并不会有什么问题,可是考虑极端状况,咱们有20个Promise链式调用,加上代码运行的时间,那么这个链式调用的第一行代码跟最后一行代码的运行极可能会超过100ms,若是这之间没有对UI有任何更新的话,虽然本质上没有什么性能问题,但可能会形成必定的卡顿或者闪烁,虽然在web应用中这种情形并不常见,可是在Node应用中,确实是有可能出现这样的case的,因此一个可以应用于生产环境的实现有必要把这个延迟消除掉。在Node中,咱们能够调用process.nextTick或者setImmediate(Q就是这么作的),在浏览器中具体如何作,已经超出了本文的讨论范围,总的来讲,就是咱们须要实现一个函数,行为跟setTimeout同样,但它须要异步且尽早的调用全部已经加入队列的函数,这里有一个实现。
在一些场景里,咱们会遇到一个较长的promise的链式调用,在某一步出现的错误让咱们没有必要去运行链式调用后面全部的代码,相似于下面这样的(此处省略then/catch里的函数):
new Promise(function(resolve, reject) { resolve(42) }) .then(function(value) { // "Big ERROR!!!" }) .catch() .then() .then() .catch() .then()
假设这个Big ERROR!!!
的出现让咱们彻底没有必要运行后面全部的代码了,但链式调用的后面即有catch,也有then,不管咱们是return
仍是throw
,都不可避免的会进入某一个catch
或then
里面,那有没有办法让这个链式调用在Big ERROR!!!
的后面就停掉,彻底不去执行链式调用后面全部回调函数呢?
从一个实现者的角度看问题时,确实找到了答案,就是在发生Big ERROR
后return一个Promise,但这个Promise的executor函数什么也不作,这就意味着这个Promise将永远处于pending
状态,因为then返回的Promise会直接取这个永远处于pending
状态的Promise的状态,因而返回的这个Promise也将一直处于pending
状态,后面的代码也就一直不会执行了,具体代码以下:
new Promise(function(resolve, reject) { resolve(42) }) .then(function(value) { // "Big ERROR!!!" return new Promise(function(){}) }) .catch() .then() .then() .catch() .then()
这种方式看起来有些山寨,它也确实解决了问题。但它引入的一个新问题就是链式调用后面的全部回调函数都没法被垃圾回收器回收(在一个靠谱的实现里,Promise应该在执行完全部回调后删除对全部回调函数的引用以让它们能被回收,在前文的实现里,为了减小复杂度,并无作这种处理),但若是咱们不使用匿名函数,而是使用函数定义或者函数变量的话,在须要屡次执行的Promise链中,这些函数也都只有一份在内存中,不被回收也是能够接受的。
将返回一个什么也不作的Promise封装成一个有语义的函数,以增长代码的可读性
Promise.cancel = Promise.stop = function() { return new Promise(function(){}) } 这么使用了: new Promise(function(resolve, reject) { resolve(42) }) .then(function(value) { // "Big ERROR!!!" return Promise.stop() }) .catch() .then() .then() .catch() .then()
new Promise(function(resolve) { resolve(42) }) .then(function(value) { alter(value) })
但运行这段代码的话你会发现什么现象也不会发生,既不会alert出42,也不会在控制台报错,怎么回事呢。细看最后一行,alert
被打成了alter
,那为何控制台也没有报错呢,由于alter
所在的函数是被包在try/catch
块里的,alter
这个变量找不到就直接抛错了,这个错就正好成了then返回的Promise的rejection reason。
解决办法:
(1)全部的promise链的最后都加上一个catch,这样出错后就会被捕获到,这样违背了DRY原则,并且繁琐;
(2)借鉴Q的一个方法done,把这个方法加到promise链的最后,就可以处理捕获最后一个promise出现的错误,其实个catch的思路同样,这个是框架来实现的。
(3)在一个Promise被reject的时候检查这个Promise的onRejectedCallback数组,若是它为空,则说明它的错误将没有函数处理,这个时候,咱们须要把错误输出到控制台,让开发者能够发现。
在Promise被reject但又没有callback时,把错误输出到控制台。
四、出错时,使用throw new Error()仍是使用return Promise.reject(new Error())呢?
从性能和编码的温馨角度考虑:
(1)性能方面:throw new Error()会使代码进入catch块里的逻辑(咱们把多有的回调都包在try/catch里),传说throw多了会影响性能,由于一旦throw,代码就有可能跳转到不可预知的位置。
而使用promise.reject(new Error()),则须要构造一个新的promise对象(包含2个数组,4个函数:resolve/reject,onResolved/onRejected),也会花费必定的时间和内存。由于onResolved/onRejected函数是直接被包在promise实现里的try里,出错后直接进入到这个try对应的catch块,代码的跳跃幅度相对较小,性能应该能够忽略不记。
(2)编码的温馨度方面:出错用throw,正经常使用return,正常能够明显的区分出错和正常
综上以为仍是promise里发现显式错误后,用throw抛出错误比较好,而不是显式的构造一个呗reject的promise对象。
注意:
一、不要把promise写成嵌套的结构
// 错误的写法 promise1.then(function(value) { promise1.then(function(value) { promise1.then(function(value) { }) }) })
二、链式promise要返回一个promise,而不是构造一个promise
// 错误的写法 Promise.resolve(1).then(function(){ Promise.resolve(2) }).then(function(){ Promise.resolve(3) })