<font id="zqva1"></font>
<rt id="zqva1"></rt>
  • <tt id="zqva1"></tt>
    <cite id="zqva1"></cite>

    <cite id="zqva1"><noscript id="zqva1"></noscript></cite>
      <rp id="zqva1"><meter id="zqva1"></meter></rp>

        <cite id="zqva1"></cite>
          <b id="zqva1"></b>
          <rp id="zqva1"></rp>
          <cite id="zqva1"></cite>

          <rt id="zqva1"></rt>

        1. <rp id="zqva1"></rp>

          剖析Promise內部結構,一步一步實現一個完整的、能通過所有Test case的Promise類

          時間:?2017-11-15閱讀:?1007標簽:?Promise


          本文寫給有一定Promise使用經驗的人,如果你還沒有使用過Promise,這篇文章可能不適合你,建議先了解Promise的使用

          Promise標準解讀

          只有一個then方法,沒有catch,race,all等方法,甚至沒有構造函數

          Promise標準中僅指定了Promise對象的then方法的行為,其它一切我們常見的方法/函數都并沒有指定,包括catch,race,all等常用方法,甚至也沒有指定該如何構造出一個Promise對象,另外then也沒有一般實現中(Q, $q等)所支持的第三個參數,一般稱onProgress

          then方法返回一個新的Promise

          Promise的then方法返回一個新的Promise,而不是返回this,此處在下文會有更多解釋

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

          不同Promise的實現需要可以相互調用(interoperable)

          Promise的初始狀態為pending,它可以由此狀態轉換為fulfilled(本文為了一致把此狀態叫做resolved)或者rejected,一旦狀態確定,就不可以再次轉換為其它狀態,狀態確定的過程稱為settle

          更具體的標準見這里

          一步一步實現一個Promise

          下面我們就來一步一步實現一個Promise

          構造函數

          因為標準并沒有指定如何構造一個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方法,用來注冊在這個Promise狀態確定后的回調,很明顯,then方法需要寫在原型鏈上。then方法會返回一個Promise,關于這一點,Promise/A+標準并沒有要求返回的這個Promise是一個新的對象,但在Promise/A標準中,明確規定了then要返回一個新的對象,目前的Promise實現中then幾乎都是返回一個新的Promise(詳情)對象,所以在我們的實現中,也讓then返回一個新的Promise對象。

          關于這一點,我認為標準中是有一點矛盾的:

          標準中說,如果promise2 = promise1.then(onResolved, onRejected)里的onResolved/onRejected返回一個Promise,則promise2直接取這個Promise的狀態和值為己用,但考慮如下代碼:

          promise2 = promise1.then(function foo(value) {
            return Promise.reject(3)
          })

          此處如果foo運行了,則promise1的狀態必然已經確定且為resolved,如果then返回了this(即promise2 === promise1),說明promise2和promise1是同一個對象,而此時promise1/2的狀態已經確定,沒有辦法再取Promise.reject(3)的狀態和結果為己用,因為Promise的狀態確定后就不可再轉換為其它狀態。

          另外每個Promise對象都可以在其上多次調用then方法,而每次調用then返回的Promise的狀態取決于那一次調用then時傳入參數的返回值,所以then不能返回this,因為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,更多復雜的情況不再詳述。

          所以,我們需要在then里面執行onResolved或者onRejected,并根據返回值(標準中記為x)來確定promise2的結果,并且,如果onResolved/onRejected返回的是一個Promise,promise2將直接取這個Promise的結果:

          Promise.prototype.then = function(onResolved, onRejected) {
            var self = this
            var promise2
          
            // 根據標準,如果then的參數不是function,則我們需要忽略它,此處以如下方式處理
            onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
            onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}
          
            if (self.status === 'resolved') {
              // 如果promise1(此處即為this/self)的狀態已經確定并且是resolved,我們調用onResolved
              // 因為考慮到有可能throw,所以我們將其包在try/catch塊里
              return promise2 = new Promise(function(resolve, reject) {
                try {
                  var x = onResolved(self.data)
                  if (x instanceof Promise) { // 如果onResolved的返回值是一個Promise對象,直接取它的結果做為promise2的結果
                    x.then(resolve, reject)
                  }
                  resolve(x) // 否則,以它的返回值做為promise2的結果
                } catch (e) {
                  reject(e) // 如果出錯,以捕獲到的錯誤做為promise2的結果
                }
              })
            }
          
            // 此處與前一個if塊的邏輯幾乎相同,區別在于所調用的是onRejected函數,就不再做過多解釋
            if (self.status === 'rejected') {
              return promise2 = new Promise(function(resolve, reject) {
                try {
                  var x = onRejected(self.data)
                  if (x instanceof Promise) {
                    x.then(resolve, reject)
                  }
                } catch (e) {
                  reject(e)
                }
              })
            }
          
            if (self.status === 'pending') {
            // 如果當前的Promise還處于pending狀態,我們并不能確定調用onResolved還是onRejected,
            // 只能等到Promise的狀態確定后,才能確實如何處理。
            // 所以我們需要把我們的**兩種情況**的處理邏輯做為callback放入promise1(此處即this/self)的回調數組里
            // 邏輯本身跟第一個if塊內的幾乎一致,此處不做過多解釋
              return promise2 = new Promise(function(resolve, reject) {
                self.onResolvedCallback.push(function(value) {
                  try {
                    var x = onResolved(self.data)
                    if (x instanceof Promise) {
                      x.then(resolve, reject)
                    }
                  } catch (e) {
                    reject(e)
                  }
                })
          
                self.onRejectedCallback.push(function(reason) {
                  try {
                    var x = onRejected(self.data)
                    if (x instanceof Promise) {
                      x.then(resolve, reject)
                    }
                  } catch (e) {
                    reject(e)
                  }
                })
              })
            }
          }
          
          // 為了下文方便,我們順便實現一個catch方法
          Promise.prototype.catch = function(onRejected) {
            return this.then(null, onRejected)
          }

          至此,我們基本實現了Promise標準中所涉及到的內容,但還有幾個問題:

          不同的Promise實現之間需要無縫的可交互,即Q的Promise,ES6的Promise,和我們實現的Promise之間以及其它的Promise實現,應該并且是有必要無縫相互調用的,比如:

          // 此處用MyPromise來代表我們實現的Promise
          new MyPromise(function(resolve, reject) { // 我們實現的Promise
            setTimeout(function() {
              resolve(42)
            }, 2000)
          }).then(function() {
            return new Promise.reject(2) // ES6的Promise
          }).then(function() {
            return Q.all([ // Q的Promise
              new MyPromise(resolve=>resolve(8)), // 我們實現的Promise
              new Promise.resolve(9), // ES6的Promise
              Q.resolve(9) // Q的Promise
            ])
          })

          我們前面實現的代碼并沒有處理這樣的邏輯,我們只判斷了onResolved/onRejected的返回值是否為我們實現的Promise的實例,并沒有做任何其它的判斷,所以上面這樣的代碼目前是沒有辦法在我們的Promise里正確運行的。

          下面這樣的代碼目前也是沒辦法處理的:

          new Promise(resolve=>resolve(8))
            .then()
            .then()
            .then(function foo(value) {
              alert(value)
            })

          正確的行為應該是alert出8,而如果拿我們的Promise,運行上述代碼,將會alert出undefined。這種行為稱為穿透,即8這個值會穿透兩個then(說Promise更為準確)到達最后一個then里的foo函數里,成為它的實參,最終將會alert出8。

          下面我們首先處理簡單的情況,值的穿透

          Promise值的穿透

          通過觀察,會發現我們希望下面這段代碼

          new Promise(resolve=>resolve(8))
            .then()
            .catch()
            .then(function(value) {
              alert(value)
            })

          跟下面這段代碼的行為是一樣的

          new Promise(resolve=>resolve(8))
            .then(function(value){
              return value
            })
            .catch(function(reason){
              throw reason
            })
            .then(function(value) {
              alert(value)
            })

          所以如果想要把then的實參留空且讓值可以穿透到后面,意味著then的兩個參數的默認值分別為function(value) {return value},function(reason) {throw reason}。
          所以我們只需要把then里判斷onResolved和onRejected的部分改成如下即可:

          onResolved = typeof onResolved === 'function' ? onResolved : function(value) {return value}
          onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {throw reason}

          于是Promise神奇的值的穿透也沒有那么黑魔法,只不過是then默認參數就是把值往后傳或者拋

          不同Promise的交互

          關于不同Promise間的交互,其實標準里是有說明的,其中詳細指定了如何通過then的實參返回的值來決定promise2的狀態,我們只需要按照標準把標準的內容轉成代碼即可。

          這里簡單解釋一下標準:

          即我們要把onResolved/onRejected的返回值,x,當成一個可能是Promise的對象,也即標準里所說的thenable,并以最保險的方式調用x上的then方法,如果大家都按照標準實現,那么不同的Promise之間就可以交互了。而標準為了保險起見,即使x返回了一個帶有then屬性但并不遵循Promise標準的對象(比如說這個x把它then里的兩個參數都調用了,同步或者異步調用(PS,原則上then的兩個參數需要異步調用,下文會講到),或者是出錯后又調用了它們,或者then根本不是一個函數),也能盡可能正確處理。

          關于為何需要不同的Promise實現能夠相互交互,我想原因應該是顯然的,Promise并不是JS一早就有的標準,不同第三方的實現之間是并不相互知曉的,如果你使用的某一個庫中封裝了一個Promise實現,想象一下如果它不能跟你自己使用的Promise實現交互的場景。。。

          建議各位對照著標準閱讀以下代碼,因為標準對此說明的非常詳細,所以你應該能夠在任意一個Promise實現中找到類似的代碼:

          /*
          resolvePromise函數即為根據x的值來決定promise2的狀態的函數
          也即標準中的[Promise Resolution Procedure](https://promisesaplus.com/#point-47)
          x為`promise2 = promise1.then(onResolved, onRejected)`里`onResolved/onRejected`的返回值
          `resolve`和`reject`實際上是`promise2`的`executor`的兩個實參,因為很難掛在其它的地方,所以一并傳進來。
          相信各位一定可以對照標準把標準轉換成代碼,這里就只標出代碼在標準中對應的位置,只在必要的地方做一些解釋
          */
          function resolvePromise(promise2, x, resolve, reject) {
            var then
            var thenCalledOrThrow = false
          
            if (promise2 === x) { // 對應標準2.3.1節
              return reject(new TypeError('Chaining cycle detected for promise!'))
            }
          
            if (x instanceof Promise) { // 對應標準2.3.2節
              // 如果x的狀態還沒有確定,那么它是有可能被一個thenable決定最終狀態和值的
              // 所以這里需要做一下處理,而不能一概的以為它會被一個“正常”的值resolve
              if (x.status === 'pending') {
                x.then(function(value) {
                  resolvePromise(promise2, value, resolve, reject)
                }, reject)
              } else { // 但如果這個Promise的狀態已經確定了,那么它肯定有一個“正常”的值,而不是一個thenable,所以這里直接取它的狀態
                x.then(resolve, reject)
              }
              return
            }
          
            if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { // 2.3.3
              try {
          
                // 2.3.3.1 因為x.then有可能是一個getter,這種情況下多次讀取就有可能產生副作用
                // 即要判斷它的類型,又要調用它,這就是兩次讀取
                then = x.then 
                if (typeof then === 'function') { // 2.3.3.3
                  then.call(x, function rs(y) { // 2.3.3.3.1
                    if (thenCalledOrThrow) return // 2.3.3.3.3 即這三處誰選執行就以誰的結果為準
                    thenCalledOrThrow = true
                    return resolvePromise(promise2, y, resolve, reject) // 2.3.3.3.1
                  }, function rj(r) { // 2.3.3.3.2
                    if (thenCalledOrThrow) return // 2.3.3.3.3 即這三處誰選執行就以誰的結果為準
                    thenCalledOrThrow = true
                    return reject(r)
                  })
                } else { // 2.3.3.4
                  resolve(x)
                }
              } catch (e) { // 2.3.3.2
                if (thenCalledOrThrow) return // 2.3.3.3.3 即這三處誰選執行就以誰的結果為準
                thenCalledOrThrow = true
                return reject(e)
              }
            } else { // 2.3.4
              resolve(x)
            }
          }

          然后我們使用這個函數的調用替換then里幾處判斷x是否為Promise對象的位置即可,見下方完整代碼。

          最后,我們剛剛說到,原則上,promise.then(onResolved, onRejected)里的這兩相函數需要異步調用,關于這一點,標準里也有說明

          In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.

          所以我們需要對我們的代碼做一點變動,即在四個地方加上setTimeout(fn, 0),這點會在完整的代碼中注釋,請各位自行發現。

          事實上,即使你不參照標準,最終你在自測試時也會發現如果then的參數不以異步的方式調用,有些情況下Promise會不按預期的方式行為,通過不斷的自測,最終你必然會讓then的參數異步執行,讓executor函數立即執行。本人在一開始實現Promise時就沒有參照標準,而是自己憑經驗測試,最終發現的這個問題。

          至此,我們就實現了一個的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符合標準呢?Promise有一個配套的測試腳本,只需要我們在一個CommonJS的模塊中暴露一個deferred方法(即exports.deferred方法),就可以了,代碼見上述代碼的最后。然后執行如下代碼即可執行測試:

          npm i -g promises-aplus-tests
          promises-aplus-tests Promise.js

          關于Promise的其它問題

          Promise的性能問題

          可能各位看官會覺得奇怪,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鏈?

          在一些場景下,我們可能會遇到一個較長的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!!!的后面就停掉,完全不去執行鏈式調用后面所有回調函數呢?

          一開始遇到這個問題的時候我也百思不得其解,在網上搜遍了也沒有結果,有人說可以在每個catch里面判斷Error的類型,如果自己處理不了就接著throw,也有些其它辦法,但總是要對現有代碼進行一些改動并且所有的地方都要遵循這些約定,甚是麻煩。

          然而當我從一個實現者的角度看問題時,確實找到了答案,就是在發生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()

          看起來是不是有語義的多?

          Promise鏈上返回的最后一個Promise出錯了怎么辦?

          考慮如下代碼:

          new Promise(function(resolve) {
            resolve(42)
          })
            .then(function(value) {
              alert(value)
            })

          乍一看好像沒什么問題,但運行這段代碼的話你會發現什么現象也不會發生,既不會alter出42,也不會在控制臺報錯,怎么回事呢。細看最后一行,alter被打成了alert,那為什么控制臺也沒有報錯呢,因為alert所在的函數是被包在try/catch塊里的,alert這個變量找不到就直接拋錯了,這個錯就正好成了then返回的Promise的rejection reason。

          也就是說,在Promise鏈的最后一個then里出現的錯誤,非常難以發現,有文章指出,可以在所有的Promise鏈的最后都加上一個catch,這樣出錯后就能被捕獲到,這種方法確實是可行的,但是首先在每個地方都加上幾乎相同的代碼,違背了DRY原則,其次也相當的繁瑣。另外,最后一個catch依然返回一個Promise,除非你能保證這個catch里的函數不再出錯,否則問題依然存在。在Q中有一個方法叫done,把這個方法鏈到Promise鏈的最后,它就能夠捕獲前面未處理的錯誤,這其實跟在每個鏈后面加上catch沒有太大的區別,只是由框架來做了這件事,相當于它提供了一個不會出錯的catch鏈,我們可以這么實現done方法:

          Promise.prototype.done = function(){
            return this.catch(function(e) { // 此處一定要確保這個函數不能再出錯
              console.error(e)
            })
          }

          可是,能不能在不加catch或者done的情況下,也能夠讓開發者發現Promise鏈最后的錯誤呢?答案依然是肯定的。

          我們可以在一個Promise被reject的時候檢查這個Promise的onRejectedCallback數組,如果它為空,則說明它的錯誤將沒有函數處理,這個時候,我們需要把錯誤輸出到控制臺,讓開發者可以發現。以下為具體實現:

          function reject(reason) {
            setTimeout(function() {
              if (self.status === 'pending') {
                self.status = 'rejected'
                self.data = reason
                if (self.onRejectedCallback.length === 0) {
                  console.error(reason)
                }
                for (var i = 0; i < self.rejectedFn.length; i++) {
                  self.rejectedFn[i](reason)
                }
              }
            })
          }

          上面的代碼對于以下的Promise鏈也能處理的很好:

          new Promise(function(){ // promise1
            reject(3)
          })
            .then() // returns promise2
            .then() // returns promise3
            .then() // returns promise4

          看起來,promise1,2,3,4都沒有處理函數,那是不是會在控制臺把這個錯誤輸出4次呢,并不會,實際上,promise1,2,3都隱式的有處理函數,就是then的默認參數,各位應該還記得then的默認參數最終是被push到了Promise的callback數組里。只有promise4是真的沒有任何callback,因為壓根就沒有調用它的then方法。

          事實上,Bluebird和ES6 Promise都做了類似的處理,在Promise被reject但又沒有callback時,把錯誤輸出到控制臺。

          Q使用了done方法來達成類似的目的,$q在最新的版本中也加入了類似的功能。

          Angular里的$q跟其它Promise的交互

          一般來說,我們不會在Angular里使用其它的Promise,因為Angular已經集成了$q,但有些時候我們在Angular里需要用到其它的庫(比如LeanCloud的JS SDK),而這些庫或是封裝了ES6的Promise,或者是自己實現了Promise,這時如果你在Angular里使用這些庫,就有可能發現視圖跟Model不同步。究其原因,是因為$q已經集成了Angular的digest loop機制,在Promise被resolve或reject時觸發digest,而其它的Promise顯然是不會集成的,所以如果你運行下面這樣的代碼,視圖是不會同步的:

          app.controller(function($scope) {
            Promise.resolve(42).then(function(value) {
              $scope.value = value
            })
          })

          Promise結束時并不會觸發digest,所以視圖沒有同步。$q上正好有個when方法,它可以把其它的Promise轉換成$q的Promise(有些Promise實現中提供了Promise.cast函數,用于將一個thenable轉換為它的Promise),問題就解決了:

          app.controller(function($scope, $q) {
            $q.when(Promise.resolve(42)).then(function(value) {
              $scope.value = value
            })
          })

          當然也有其它的解決方案比如在其它Promise的鏈的最后加一個digest,類似下面這樣:

          Promise.prototype.$digest = function() {
            $rootScope.$digest()
            return this
          }
          // 然后這么使用
          OtherPromise
            .resolve(42)
            .then(function(value) {
              $scope.value = value
            })
            .$digest()

          因為使用場景并不多,此處不做深入討論。

          出錯時,是用throw new Error()還是用return Promise.reject(new Error())呢?

          這里我覺得主要從性能和編碼的舒適度角度考慮:

          性能方面,throw new Error()會使代碼進入catch塊里的邏輯(還記得我們把所有的回調都包在try/catch里了吧),傳說throw用多了會影響性能,因為一但throw,代碼就有可能跳到不可預知的位置。

          但考慮到onResolved/onRejected函數是直接被包在Promise實現里的try里,出錯后就直接進入了這個try對應 的catch塊,代碼的跳躍“幅度”相對較小,我認為這里的性能損失可以忽略不記。有機會可以測試一下。

          而使用Promise.reject(new Error()),則需要構造一個新的Promise對象(里面包含2個數組,4個函數:resolve/reject,onResolved/onRejected),也會花費一定的時間和內存。

          而從編碼舒適度的角度考慮,出錯用throw,正常時用return,可以比較明顯的區分出錯與正常,throw和return又同為關鍵字,用來處理對應的情況也顯得比較對稱(-_-)。另外在一般的編輯器里,Promise.reject不會被高亮成與throw和return一樣的顏色。最后,如果開發者又不喜歡構造出一個Error對象的話,Error的高亮也沒有了。

          綜上,我覺得在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)
          })

          Promise相關的convenience method的實現

          請到這里查看Promise.race, Promise.all, Promise.resolve, Promise.reject等方法的具體實現,這里就不具體解釋了,總的來說,只要then的實現是沒有問題的,其它所有的方法都可以非常方便的依賴then來實現。

          結語

          最后,如果你覺得這篇文章對你有所幫助,歡迎分享給你的朋友或者團隊,記得注明出處哦~

          原文來源:https://github.com/xieranmaya/blog/issues/3
          站長推薦

          1.阿里云: 本站目前使用的是阿里云主機,安全/可靠/穩定。點擊領取2000元代金券、了解最新阿里云產品的各種優惠活動點擊進入

          2.騰訊云: 提供云服務器、云數據庫、云存儲、視頻與CDN、域名等服務。騰訊云各類產品的最新活動,優惠券領取點擊進入

          3.廣告聯盟: 整理了目前主流的廣告聯盟平臺,如果你有流量,可以作為參考選擇適合你的平臺點擊進入

          鏈接: http://www.modern-decoration.com.cn/article/detial/113

          Promise.then鏈式調用順序

          想用Promise異步實現一個遞歸調用的接口,用來做簡單AI的動作序列。發現一開始接觸這個then的時候,不是很清楚,參考了網上的一些寫法,改成自己的有問題,所以先靜下心來研究一下這個調用的順序問題

          這 10 個片段,有助于你理解 ES 中的 Promise

          在開發中,了解 JavaScript 和 Promise 基礎,有助于提高我們的編碼技能,今天,我們一起來看看下面的 10 片段,相信看完這 10 個片段有助于我們對 Promise 的理解。

          這幾個Promise的輸出到底是?

          我們使用構造函數方法創建一個Promise實例,立即使用 reject 回調觸發一個錯誤。catch處理程序的工作方式類似于DOM的 .addeventlistener(事件、回調)或事件發射器的 .on(事件、回調),其中可以添加多個回調。每個回調都具有相同的參數。

          ES6之Promise

          所謂的 promise,簡單的來說就是一個容器,里面保存著某個未來才會結束的事件(也就是我們的異步操作)的結果。從語法上面來說,Promise 是一個對象,從它可以獲取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理。

          Promise 中的三兄弟 .all(), .race(), .allSettled()

          從ES6 開始,我們大都使用的是 Promise.all()和Promise.race(),Promise.allSettled() 提案已經到第4階段,因此將會成為ECMAScript 2020的一部分。Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>

          從 callback 到 promise

          說起回調(callback),那可以說是 JS 最基礎的異步調用方式,是 JS 為解決阻塞請求而量身定制出的一種設計模式,在 JS 或是說前端大潮中有著舉足輕重的影響

          Promise對象 3 種妙用

          作為一個前端,說不了解 Promise 對象用法的基本不存在,這里就不對功能用法進行介紹了。但本文將會講述你可能不知道的 Promise 3 種奇妙用法。當然,每種用法都會有其適用的特殊場景。

          Callback Hell和ECMAScript6 Promise

          回調地域 既一個異步請求需要另一個異步請求結果;由于 Javascript 是單線程的,所以這里執行順序是 ajax1 -> ajax2 -> ajax3 -> ajax4;但是又由于這四個是異步操作

          手寫實現Promise的相關方法

          Promise 作為 JS 社區的異步解決方案,為開發者提供了.then()、Promise.resolve()、Promise.reject()等基本方法。除此之外,為了更方便地組合和控制多個的 Promise 實例,也提供了.all()、.race()等方法。

          Promise 使用、原理以及實現過程

          promise 是目前 JS 異步編程的主流解決方案,遵循 Promises/A+ 方案。promise 對象原型上有一個 then 方法,then 方法會返回一個新的 promise 對象,并且將回調函數 return 的結果作為該 promise resolve 的結果

          內容以共享、參考、研究為目的,不存在任何商業目的。其版權屬原作者所有,如有侵權或違規,請與小編聯系!情況屬實本人將予以刪除!

          文章投稿關于web前端網站點搜索站長推薦網站地圖站長QQ:522607023

          小程序專欄: 土味情話心理測試腦筋急轉彎幽默笑話段子句子語錄成語大全運營推廣

          国产精品高清视频免费 - 视频 - 在线观看 - 影视资讯 - 唯爱网