重審promise的用法

白色風車發表於2016-12-29

重審promise的用法

參考文章: Promises: All the wrong ways

使用Promise已經很長時間了,體驗著promise的強大的同時,時不時也會有不太自然的感覺, 但是也說不清哪裡有問題。

仔細想想,也許就是因為其強大,看什麼都像是promise了,所以沒有停下來思考一下到底某些場景使用promise是否可合適,以後也給自己提個醒,別輕易對某些東西太exciting了。

promise的原始本質是把一個可能的將來值封裝成一個無需關心時間的抽象值,把非同步資料的使用方式和同步資料的使用方式統一起來,使得我們更容易的構建或者理解程式的邏輯。

所以promise是一個值,雖然要獲取它封裝的原始值需要呼叫他的介面方法,但是它仍然是一個值,那麼適合它使用的場景應該是值通常的使用場景。

所以我們關心的部分應該是,值的構造、傳遞和儲存。對於promise物件本身而言,作為一個抽象型別一定會遵循一定的標準,所以我們還需要關心值的型別確認。

從構造promise說起

值的傳遞和儲存應該很好理解,而promise的構造其實會是使用Promise的大頭。

常見的構造方式可以有下面幾種,


//直接呼叫其他人提供的介面生成一個promise物件
var promise = promiseMaker(...);

//由promise物件本身的方法衍生出新的promise物件
var newPromise = promise.then(function(){...});

//通過工具類方法把原有的promise物件加工生成新的promise物件
var timeoutPromise = Promise.timeout(2000, promise);

var promise = Promise.resolve(thenable);

//使用原生構造方法構建promise物件
var promise = new Promise(function(resolve, reject){...});

可以看出原生的構造方法使用起來最麻煩,需要知道的細節最多,也會使得程式碼更加複雜,應該儘量避免使用。

第二,由於promise的非同步性,儘量注意簡潔,不要使用多餘的promise來連結生成新promise,使用的多了效能還是會有一點損失的。

第三,對於第三方返回的所謂的promise,我們不能輕易相信其合法性,保險起見可以使用Promise.resolve包一下。

最後, 把promise分成deferdefer.promise是anti-pattern,不要使用該方式生成promise,或者利用原生構造方法使用該方式。

promise是個值

很容易就會把promise看成是一個非同步執行的邏輯,有開始有結束,一次性的過程。而且在使用原生構造方法時,腦袋裡面也是這樣的思維模式。

因此,很容易把好多一次性的過程場景都看成是promise了(我們已經幹過好多這樣的事),這裡就是對promise too exciting了。

promise抽象的是一個將來的值,用它來抽象過程其實是不合適的,我們有其他的模型作這樣的事情,並且其他的模型會提供更豐富的處理方式,promise僅僅只有then方法,功能上太簡單了。

事件機制,觀察者模式...還是使用合適的工具做合適的事情吧。

回想一下,為了能結束某一過程,我們多少次把建構函式裡的 resolvereject方法拆出去了,覺得很彆扭,但就是不能跳出promise的框框看看還有什麼更合適的處理方式。

流程控制不是promise的責任


 p
  .then(step1)
  .then(step2)
  .then(step3)
  ...

上面的寫法很熟悉吧。他比callback的寫法也漂亮多了,但是還是回到promise的抽象上,這樣的寫法賦予promise的職責有點過重了,他不在單純是封裝值,還承擔業務流程控制的職責。

什麼樣的使用方式是符合其抽象的呢?


var result = p
    .then(convert1)
    .then(convert2)
    .then(convert3);

還是應該把promise視為值較為恰當,then這裡起著值轉換的作用。

講究一點的話,上面then中接受的方法應該都是純方法無副作用的。

這意味著,拿promise鏈來一步一步賦值一個公共的變數,比如賦值一個全域性物件,這樣的做法也是違背promise原本抽象的精神的。

所以對於非同步流控制(上述賦值公共物件的操作也屬於該範疇)應該採取其他機制來進行。

這裡新標準裡的generator或者還沒廣泛支援的await/async,應該是更適合幹流程控制這類事的,程式碼寫起來也會和原來同步執行邏輯的程式碼差不多,也更好理解和reason。


var makeResult = run(function gen*(arg){
      var result = {};
      var argB = yield promiseA(arg);
      result.b = yield promiseB(argB);
      try{
         result.c = yield promiseC():
      }catch(e){
         console.warn(e);
         result.c = 'defaultC';
      }

       result.d = 'd';
       return result;
});

makeResult(arg).then(console.log.bind(console));
//result:
//{
//    b: ..,
//   c: ...,
//    d: ...
//}

上面的run方法的簡單實現如下,


function run(genMaker){
  var generator = genMaker.apply(this, arguments);
  function handle(result){
    if(result.done){
      return Promise.resolve(result.value);
    }
    return Promise.resolve(result.value)
            .then(function(res){
              return handle(generator.next(res));
            })
            .catch(function(err){
              return handle(generator.throw(err));
            });
  }

  try{
    return handle(generator.next()); 
  }catch(ex){
    return Promise.reject(ex);
  }
}

回到我們自己的專案裡,server端的非同步操作邏輯會更復雜,需要流控制的場景會更多一些,好在當前的node版本已經支援使用generator了,所以鼓勵server端多嘗試該方式編碼。

前端的非同步操作邏輯相對簡單一些,目前還沒有看到使用需求,以後可以新增編譯過程來支援generator的使用。

相關文章