函數語言程式設計之Promise的奇幻漂流

17點發表於2018-07-08

上一篇我們講了同步鏈式處理資料函子的概念。這一節,我們來講非同步。用到的概念很簡單,不需要有函數語言程式設計的基礎。當然如果你看了那篇 《在你身邊你左右 --函數語言程式設計別煩惱》 會更容易理解。這一篇我們會完成一個Promise程式碼的編寫。本文會從實現一個只有十幾行程式碼能夠解決非同步鏈式呼叫問題的簡單的Promise開始。然後逐漸完善增加功能。

  • 實現簡單的非同步Promise函子
  • 能夠同時呼叫同一Promise函子
  • 增加reject回撥函式
  • 增加Promise狀態

本文程式碼在我的github

1 實現簡單的Promise函子

我們先來回顧一下同步鏈式呼叫。

class Functor{
       constructor (value) {
          this.value = value ;
       }      
       map (fn) {
         return Functor.of(fn(this.value))
       }
    }
Functor.of = function (val) {
     return new Functor(val);
}

Functor.of(100).map(add1).map(add1).map(minus10)

// var  a = Functor.of(100);
// var  b = a.map(add1);
// var  c = b.map(add1);
// var  d = c.map(minus10);

複製程式碼

函數語言程式設計之Promise的奇幻漂流

  • 函子的核心就是每個functor都是一個新的物件
  • 通過map中傳遞進去的函式fn去處理資料
  • 用得到的值去生成新的函子

那麼如果當a的值是非同步產生的,我們該何如傳入this.value值呢?

function executor(resolve){
  setTimeout(()=>{resolve(100)},500)
}
複製程式碼

我們模擬一下通過setTimeout500毫秒後拿到資料100。其實也很簡單,我們可以傳進去一個resolve回撥函式去處理這個資料。

class Functor {
   constructor (executor) {
      let _this = this;
      this.value = undefined;

      function resolve(value){
          _this.value = value;
      }
      executor(resolve)
   } 
}

var a = new Functor(executor);

複製程式碼
  • 我們講executor傳入並立即執行
  • 在resolve回撥函式中我們能夠拿到value值
  • 我們定義resolve回撥函式講value的值賦給this.value

這樣我們就輕鬆的完成了a這個物件的賦值。那麼我們怎麼用方法去處理這個資料呢?

  • 顯然在拿到回撥函式值之後,我們應該能讓map裡的fn去繼續處理資料
  • 處理完這個資料,我們交給下一個函式的resolve去繼續處理
  • 所以我們定義了一個callback函式,
  • 在呼叫map時,將就包含fn處理資料,和執行下一個物件的resolve的函式賦值給它
  • 然後在自己的resolve拿到值之後,我們執行這個callback
class Functor {
   constructor (executor) {
      let _this = this;
      this.value = undefined;
      this.callback = null;
      function resolve(value){
          _this.value = value;
          _this.callback()
      }
      executor(resolve)
   } 
  
   map (fn) {
       let  self = this;
       return new Functor((resolve) => {
          self.callback = function(){
              let data =  fn(self.value)   
              resolve(data)
           }
       })
   }    
}
new Functor(executor).map(add1).map(add1)
複製程式碼

現在我們已經實現了非同步的鏈式呼叫,我們來具體分析一下,都發生了什麼。

函數語言程式設計之Promise的奇幻漂流

  • (1)a = new Functor(executor)的時候,我們進行了初始化, executor(resolve)開始執行
  • (2)b =a.map(add1)的時,先進行了初始化 new Functor(),然後執行 executor(resolve)
  • (3)b中executor(resolve)執行結束,將一個函式賦值a中的callback

注意:這時map中this指向的是a函子,但是 new Functor((resolve) => {}中resolve是B的

  • (4)最後return 一個新的函子b
  • (5)c =b.map(add1)的時,同樣,給b中的callback賦值
  • (6)然後返回一個新的函子c,此時沒有map的呼叫,c中的callback就是null

我們再來分析一下非同步結束之後,回撥函式中的resolve是如何執行的。

函數語言程式設計之Promise的奇幻漂流

  • (1)resolve 先_this.value = value;把a中的value進行修改
  • (2)在執行_this.callback(),先let data = fn(self.value) 計算出處理後的data
  • (3)呼叫b中的resolve函式繼續處理
  • (4)b中也是,先給value賦值,然後處理資料
  • (5)再呼叫c中的resolve,並把處理好的資料傳給他
  • (6)先給C中value賦值,然後再處理資料,最後呼叫callback時因為不是函式會報錯,之後我們會解決

本節程式碼:promise1.js

嗯,這就是promise作為函子實現的處理非同步操作的基本原理。它已經能夠解決了簡單的非同步呼叫問題。雖然程式碼不多,但這是promise處理非同步呼叫的核心。接下來我們會不斷繼續實現其他功能。

2 同時呼叫同一個Promise函子

如果我們像下面同時呼叫a這個函子。你會發現,它實際上只執行了c。

var a = new Functor(executor);
var b = a.map(add);
var c = a.map(minus);
複製程式碼

原因很簡單,因為上面我們學過,b先給a的callback賦值,然後c又給a的callback賦值。所以把b給覆蓋掉了就不會執行啦。解決這個問題很簡單,我們只需要讓callback變成一個陣列就解決啦。

class MyPromise {
   constructor (executor) {
      let _this = this;
      this.value = undefined;
      this.callbacks = [];
      function resolve(value){
          _this.value = value;
          _this.callbacks.forEach(item => item())
      }
      executor(resolve)
   } 
  
   then (fn) {
       return new MyPromise((resolve) => {
          this.callbacks.push (()=>{
              let data =  fn(this.value) 
              console.log(data)         
              resolve(data)
           })
       })
   }    
}

var a = new MyPromise(executor);
var b = a.then(add).then(minus);
var c = a.then(minus);

複製程式碼
  • 我們定義了callbacks陣列,每次的呼叫a的then方法時。都將其存到callbacks陣列中。
  • 當回撥函式拿到值時,在resolve中遍歷執行每個函式。
  • 如果callbacks是空,forEach就不會執行,這也解決了之前把錯的問題
  • 然後我們進一步改了函子的名字(MyPromise),將map改成then
  • 簡化了return中,let self = this;

3 增加reject回撥函式

我們都知道,在非同步呼叫的時候,我們往往不能拿到資料,返回一個錯誤的資訊。這一小節,我們對錯誤進行處理。

function executor(resolve,reject){
  fs.readFile('./data.txt',(err, data)=>{
    if(err){ 
       console.log(err)
       reject(err)
    }else {
       resolve(data)
    }
  })
}
複製程式碼
  • 我們現在用node非同步讀取一個檔案
  • 成功執行 resolve(data),失敗執行 reject(err)

函數語言程式設計之Promise的奇幻漂流

現在我們定義出這個reject

class MyPromise {
  constructor (executor) {
    let _this = this;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    function resolve(value){
      _this.value = value;
      _this.onResolvedCallbacks.forEach(item => item())
    }
    function reject(reason){
      _this.reason = reason;
      _this.onRejectedCallbacks.forEach(item => item());
    }
    executor(resolve, reject);
  } 
  then (fn,fn2) {
    return new MyPromise((resolve,reject) => {
      this.onResolvedCallbacks.push (()=>{
        let data =  fn(this.value) 
        console.log(data)         
        resolve(data)
      })
      this.onRejectedCallbacks.push (()=>{
        let reason =  fn2(this.reason) 
        console.log(reason)         
        reject(reason)
      })
    })
  }    
}
複製程式碼
  • 其實很簡單,就是我們就是在executor多傳遞進去一個reject
  • 根據非同步執行的結果去判斷執行resolve,還是reject
  • 然後我們在MyPromise為reject定義出和resolve同樣的方法
  • 然後我們在then的時候應該傳進去兩個引數,fn,fn2

本節程式碼:promise3.js

這時候將executor函式封裝到asyncReadFile非同步讀取檔案的函式

function asyncReadFile(url){
  return new MyPromise((resolve,reject) => {
    fs.readFile(url, (err, data) => {
      if(err){ 
         console.log(err)
         reject(err)
      }else {
         resolve(data)
      }
    })
  })
}
var a = asyncReadFile('./data.txt');
a.then(add,mismanage).then(minus,mismanage);
複製程式碼

這就是我們平時封裝非同步Promise函式的過程。但這是過程有沒有覺得在哪見過。如果之前executor中的'./data.txt'我們是通過引數傳進去的那麼這個過程不就是上一節我們提到的柯里化。

本節程式碼:promise4.js

我們再來總結一下上面的過程。

  • 我們先進行了初始化,去執行傳進來的 executor函式,並把處理的函式push進入callback陣列中
  • 在reslove或reject執行時,我們去執行callback中的函式

函數語言程式設計之Promise的奇幻漂流

  • 我們可以看到同樣一個函子a在不同時期有著不一樣的狀態。
  • 顯然如果在reslove()或者 reject( )之後我們再新增then()方法是不會有作用的

那麼我們如何解決reslove之後a函子的then呼叫問題呢,其實reslove之後,我們已經有了value值,那不就是我們最開始講的普通函子的鏈式呼叫嗎?所以現在我們只需要標記出,函子此時的狀態,再決定如何呼叫then就好啦

4 增加Promise狀態

  • 我們定義進行中的狀態為pending
  • 已成功執行後為fulfilled
  • 失敗為rejected
class MyPromise {
  constructor (executor) {
    let _this = this;
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    function resolve(value){
      if (_this.status === 'pending') {
        _this.status = 'fulfilled';
        _this.value = value;
        _this.onResolvedCallbacks.forEach(item => item())
      }
    }
    function reject(reason){
      if (_this.status === 'pending') {
        _this.status = 'rejected';  
        _this.reason = reason;
        _this.onRejectedCallbacks.forEach(item => item());
      }
    }
    executor(resolve, reject);
  } 
  then (fn,fn2) {
     return new MyPromise((resolve,reject) => {
      if(this.status === 'pending'){
        this.onResolvedCallbacks.push (()=>{
          let data =  fn(this.value) 
          console.log(data)         
          resolve(data)
        })
        this.onRejectedCallbacks.push (()=>{
          let reason =  fn2(this.reason) 
          console.log(reason)         
          reject(reason)
        })
      }
      if(this.status === 'fulfilled'){
          let x = fn(this.value)
          resolve(x)
      }
      if(this.status === 'rejected'){
          let x = fn2(this.value)
          reject(x)
      }
    })
  }    
}

var a = asyncReadFile('./data.txt');
a.then(add,mismanage).then(add,mismanage).then(add,mismanage);
複製程式碼

我們分析一下上面這個過程

函數語言程式設計之Promise的奇幻漂流

其實就多了一個引數,然後判斷了一下,很簡單。那麼我們現在來分析一下,當我們呼叫fulfilled狀態下的a的執行過程

setTimeout(()=>{ d = a.then(add);} ,2000)
value:"1"
複製程式碼

函數語言程式設計之Promise的奇幻漂流

  • (1)先執行new MyPromise(),初始化d
  • (2)然後執行 executor(resolve, reject);fn開始執行,算出新的值x
  • (3)傳給d的resolve執行,
  • (4)修改stauts和value的狀態
  • (5)return 出新的函子,可以繼續鏈式呼叫

我們來想一個問題,如果(2)中fn是一個非同步操作,d後邊繼續呼叫then方法,此刻pending狀態就不會改變,直到resolve執行。那麼then的方法就會加到callback上。就又回到我們之前處理非同步的狀態啦。所以這就是為什麼Promise能夠解決回撥地獄

參考程式碼:promise5.js

好了,我們現在來看傳進去的方法fn(this.value) ,我們需要用上篇講的Maybe函子去過濾一下。

5 Maybe函子優化

 then (onResolved,onRejected) {
     
     onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
     onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}

     return new MyPromise((resolve,reject) => {
      if(this.status === 'pending'){
        this.onResolvedCallbacks.push (()=>{
          let x =  onResolved(this.value) 
          resolve(x)
        })
        this.onRejectedCallbacks.push (()=>{
          let x =  onRejected(this.reason)
          reject(x)
        })
      }
      if(this.status === 'fulfilled'){
          let x = onResolved(this.value)
          resolve(x)
      }
      if(this.status === 'rejected'){
          let x = onRejected(this.value)
          reject(x)
      }
    })
  }    
複製程式碼
  • Maybe函子很簡單,對onResolved和onRejected進行一下過濾

參考程式碼:promise6.js

這一篇先寫到這裡吧。最後總結一下,Promise的功能很強大,就是少年派的奇幻漂流一樣。雖然旅程絢爛多彩,但始終陪伴你的只有那隻老虎。Promise也是一樣,只要掌握其核心函子的概念,其他問題就比較好理解啦。這裡只實現了一個簡單的Promise,更強大的功能,我們慢慢加吧。

相關文章