欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ES6--浅析Promise内部结构

程序员文章站 2022-04-15 13:33:52
...

首发于:sau交流学习社区

一、前言

什么是promise?promsie的核心是什么?promise如何解决回调地狱的?等问题

1、什么是promise?promise是表示异步操作的最终结果;可以用来解决回调地狱和并发IO操作的问题

A promise represents the eventual result of an asynchronous operation.

2、promise 的核心是什么?promise的核心就是链式调用

3、采用什么方法可以实现链式调用?通过使用then的方法,then方法是用来注册在这个Promise状态确定后的回调,很明显,then方法需要写在原型链上。

4、promise是如何解决回调地狱的问题?(1)如果一个promise返回的是一个promise,会把这个promise传递结果传递到下一次的then中;(2)如果一个promise返回的是一个普通的值,会把这个普通值作为下一次then的成功回调结果;(3)如果当前promise失败了,会走下一个then的回调函数;(4)如果then不返回值,就会有一个默认值为undefined,作为普通值,会作为下一个then的成功回调;(5)catch是错误没有处理的情况才会执行;(6)then中可以不写东西

 

 

二、promise的标准解读

1、只有一个then方法,没有catchraceall等方法,甚至没有构造函数;

Promise标准中仅指定了Promise对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定,包括catchraceall等常用方法,甚至也没有指定该如何构造出一个Promise对象,另外then也没有一般实现中(Q, $q等)所支持的第三个参数,一般称onProgress

2、then方法返回一个新的Promise;

Promise的then方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释

promise2 = promise1.then(alert)
promise2 != promise1 // true

3、不同Promise的实现需要可以相互调用(interoperable)

4、Promise的初始状态为pending,它可以由此状态转换为fulfilled(本文为了一致把此状态叫做resolved)或者rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为settle

 

三、实现一个promise

1、构造函数

因为标准并没有指定如何构造一个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构造函数的主体,但目前还有两个问题:

1、我们给executor函数传了两个参数:resolve和reject,这两个参数目前还没有定义

2、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属性上面,之后执行相应的回调函数,逻辑很简单,这里就不多解释了。

 

2、then方法

then方法是用来注册这个promise确定状态后的回调,then方法是需要写在原型链上。

自然约束:then方法会返回一个Promise,关于这一点,Promise/A+标准并没有要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(https://promisesaplus.com/differences-from-promises-a#point-5)对象,所以在我们的实现中,也让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,更多复杂的情况不再详述。

 

3、完整的promise

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的常用方法是如何实现

1、Promise.resolve / Promise.reject 实现

// 原生的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)
    })
}

 

2、catch的实现

// 原生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
}

 

3、promise.all的实现同时执行多个异步,并且返回一个新的promise,成功的值是一个数组,该数组的成员的顺序是传参给promise.all的顺序

// 原生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
        }
    })
}

 

4、promise.race方法实现,同时执行多个异步,然后那个快,就用那个的结果,race是赛跑

// 原生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有点类似
        }
    })
}

 

5、promise.defer = promise.deferred这个语法糖怎么理解呢?

这个语法糖可以简化一些操作,比如:

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的核心在于:链式调用。

promise主要解决两个问题:

(1)回调地狱

(2)并发的异步IO操作,同一时间内把这个结果拿到,比如有两个异步io操作,当这2个获取完毕后,才执行相应的代码

1、回调地狱怎么解决

那么我们来看下面的代码,并且改为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的其他问题

1、性能问题

可能各位看官会觉得奇怪,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一样,但它需要异步且尽早的调用所有已经加入队列的函数,这里有一个实现。

 

2、如何停止一个promise链?

在一些场景里,我们会遇到一个较长的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()

 

3、promise的链上返回的最后一个promise出错了怎么办?

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时,把错误输出到控制台。

 

4、出错时,使用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对象。

 

七、实践

注意:

1、不要把promise写成嵌套的结构

// 错误的写法
promise1.then(function(value) {
  promise1.then(function(value) {
    promise1.then(function(value) {

    })
  })
})

2、链式promise要返回一个promise,而不是构造一个promise

// 错误的写法
Promise.resolve(1).then(function(){
  Promise.resolve(2)
}).then(function(){
  Promise.resolve(3)
})

 

八、参考

https://github.com/xieranmaya/blog/issues/3