您的位置:首页 > Web前端 > JavaScript

自己动手实现promise

2017-07-22 10:31 771 查看
本文适合对于promise的实现原理感兴趣的同学,由于使用PHP实现promise,故需要具备一定的PHP基础知识。

一、背景

大家都知道,异步编程在web领域内越来越多地运用,但异步回调代码的写法十分恶心,逐层嵌套,不便于阅读。为了解决这个问题,js实现了promise模式,但大多数开发者只知道promise的表面用法,不知其底层实现逻辑。笔者采用PHP实现了自己的promise,借着此过程,与大家分享promise的实现原理。

二、Promise用法

setTimeout(function() {
   console.log("timeout1");
   setTimeout(function(){
       console.log("timeout2");
       setTimeout(function(){
           console.log("timeout3");
       }, 1000);
   }, 1000);
}, 1000);

通常处理异步的函数采用回调写法,其形式为如上面代码,1000ms超时后终端输出timeout1,随后进入第二层超时函数,再过1000ms之后终端输出timeout2,随后进入第三层超时函数,1000ms之后终端输出timeout3。很明显,函数嵌套会越来越深,对应的功能promise代码实现如下:

var promise = new Promise(function(resolve, reject){
   setTimeout(function(){
       console.log("timeout1");
       resolve();
   }, 1000);
});
promise.then(function(value){
   setTimeout(function(){
       console.log("timeout2");
       resolve();
   }, 1000);
}).catch(function(error){
   console.error(error);
}).then(function(value){
   setTimeout(function(){
       console.log("timeout3");
       resolve();
   }, 1000);
}).catch(function(error){
   console.error(error);
});

promise对象传入回调函数,回调函数接口包括执行成功时调用的resolve函数和执行失败时调用的reject函数。then方法指定resolve函数的实现,catch指定reject函数的实现。这样就把本该在函数体内嵌套的resolve函数和reject函数放到函数外,最终串在一起,保证函数的嵌套层数不会增加。

三、Promise实现

promise完整实现包括很多功能,本文主要介绍promise、all、race这些常用的函数实现,使用这些基础组件读者可以快速实现更加高级的接口。

注:上面的catch在此处改用otherwise。

1.promise

promise最基本的功能如上所述,难点在于如何将resolve和reject与后面then和catch指定的函数实现绑定起来。我们知道,promise支持同步或异步调用resolve和reject,如何保证then或catch调用的先后顺序极为关键。回顾promise构造形式:

new Promise(function($resolve, $reject){
   //$resolve or $reject
   ...
})

执行promise构造函数传入的参数(参数为一个回调函数),使得resolve或reject被调用。

同步调用场景下resolve和reject真正被调用时其还未绑定到then或otherwise中指定的函数实现,因此需要保存传入resolve或者reject中的参数,这样then或otherwise方法执行时将参数传入其指定的resolve或reject函数执行即可。

异步场景下resolve或reject真正被调用时,then或otherwise已经执行完成,故需要保存then或otherwise指定的resolve或reject函数实现,等到resolve或reject调用时,调用其绑定的函数即可。

这样,实现promise的基本代码框架就出来了:

   public function __construct(callable $callback)
   {
       //给callback传入自定义的resolve和reject函数,旨在获取调用时的参数
       $callback([$this, "resolve"], [$this, "reject"]);
   }

   public function resolve($value = null)
   {
       //resolve或者reject函数实现为空,表示同步调用resolve或者reject,
       //此时保存参数值,参数值表明这是一个已完成的promise
       if ($this->callbackList === []) {
           $this->result = new FulfilledPromise($value);
           return;
       }
       //异步调用场景下,callbackList保存了后面then或otherwise的一系列
       //函数体列表,逐个判断需要调用then还是otherwise,函数调用结果作为下
       //一轮调用的参数值。
       $expect = PromiseCallback::THEN;
       foreach ($this->callbackList as $callback) {
           $type = $callback->getType();
           $fn = $callback->getFn();
           if ($type === $expect) {
               if ($value instanceof PromiseResult) {
                   $value = $value->getValue();
               }
               $value = call_user_func($fn, $value);
               if ($value instanceof RejectedPromise) {
                   $expect = PromiseCallback::OTHERWISE;
               } else {
                   $expect = PromiseCallback::THEN;
               }
           }
       }

       //result保存为FulfilledPromise或者RejectedPromise
       if ($value instanceof PromiseResult) {
           $this->result = $value;
       } else {
           $this->result = new FulfilledPromise($value);
       }
   }

   public function reject($value = null)
   {
   //reject实现与resolve类似,不同之处在于首轮函数调用需要的是
   //PromiseCallback::OTHERWISE类型的函数
      ...
   }
   
   public function then(callable $resolve)
   {
       try {
       //result为FulfilledPromise时,表示同步调用,直接调用resolve处理
           if ($this->result instanceof FulfilledPromise) {
               $value = $resolve($this->result->getValue());
               if ($value instanceof PromiseResult) {
                   $this->result = $value;
               } else {
                   $this->result = new FulfilledPromise($value);
               }
           } else if ($this->result == null) {
           //result为null表示异步调用,保存resolve函数至函数列表
               $this->callbackList[] = new PromiseCallback(PromiseCallback::THEN, $resolve);
           }
       } catch (\Exception $e) {
           var_dump($e);
       }
       return $this;
   }

   public function otherwise(callable $reject)
   {
       //实现与then类似,除了类型判断刚好相反
       ...
   }

按照上述代码,不管同步还是异步调用场景,最终都会逐层调用then或otherwise中的resolve或reject直到结束。

2.all

all需要实现的功能为:

如果参数数组中所有的promise都调用resolve,将所有resolve调用的参数包装成对应数组传送给then中的resolve实现处理

一旦有一个promise调用的是reject,将reject调用的参数传送给otherwise中的reject实现处理。

因此,需要统计每个promise执行的结果,但是这里又会涉及到同步和异步场景下每个promise调用的问题。

对应的解决方案为:

针对每个promise,分别执行then和otherwise,在resolve函数中判断所有promise的结果是否已经完成,如果已经执行完成,传入结果数组回调all[promises]->then->otherwise中的resolve函数。

如果promise的otherwise函数被调用,则回调reject函数。

为了保证后续的then和otherwise都可以被调用,只需要返回promise对象,复用promise的逻辑即可。

实现代码逻辑为:

public function __construct($promises)
{
   if (is_array($promises))
       $this->promises = $promises;
   else
       $this->promises = [];
}

public function then($callback)
{
...
//返回promise对象,使得后续的then和otherwise可以复用promise的逻辑
   return new Promise(function ($resolve, $reject) use($callback) {
       for ($i = 0; $i < count($this->promises); $i++) {
           $promise = $this->promises[$i];
           if ($promise instanceof Promise) {
               $promise->then(function ($value) use ($resolve, $i, $callback) {
                   $this->resolvedValue[$i] = $value;
                   //收集所有的resolvedValue结果,一旦全部收集到,
                   //调用callback,此时then方法中的resolve被调用。
                   //同时为了保证后续的then和otherwise可以调用,
                   //需要调用resolve
                   if (count($this->resolvedValue) == count($this->promises)) {
                       if ($this->arrived === false) {
                           $this->arrived = true;
                           ksort($this->resolvedValue);
                           $value = call_user_func($callback, $this->resolvedValue);
                           $resolve($value);
                       }
                   }
               })->otherwise(function ($value) use ($reject) {
               //otherwise逻辑里直接调用reject
                   if ($this->arrived === false) {
                       $this->arrived = true;
                       $reject($value);
                   }
               });
           } else if ($promise instanceof RejectedPromise) {
           //一旦promises中存在RejectPromise,直接调用reject
               if ($this->arrived === false) {
                   $this->arrived = true;
                   $reject($promise->getValue());
                   return;
               }
           } else {
           //其他类型的入参按照resolve来处理
               if ($promise instanceof FulfilledPromise) {
                   $promise = $promise->getValue();
               }
               $this->resolvedValue[$i] = $promise;
               if (count($this->resolvedValue) == count($this->promises)) {
                   if ($this->arrived === false) {
                       $this->arrived = true;
                       ksort($this->resolvedValue);
                       $value = call_user_func($callback, $this->resolvedValue);
                       $resolve($value);
                   }
               }
           }
       }
   });

}

3.race

race需要实现的功能为:

参数中所有的promise同时执行,最先完成的promise执行后面的then和otherwise逻辑。 和all的实现不同的是,race只需要处理最先调用resolve或者reject的promise即可。可以采取的策略为:

委托每个promise执行then和otherwise,在resolve和reject函数中通过唯一标记判断是否有其他promise已经完成,如果没有,则置标记为已完成,使用此结果进行后续逻辑。反之不作任何操作。

实现代码逻辑为:

public function then($callback)
{
...
//返回promise对象,使得后续的then和otherwise可以复用promise的逻辑
   return new Promise(function ($resolve, $reject) use ($callback) {
       foreach ($this->promises as $promise) {
           if ($promise instanceof Promise) {
           //委托每个promise调用then和otherwise
               $promise->then(function ($value) use ($callback, $resolve, $reject) {
               //$this->arrived标记结果是否已经产生,用于互斥
                   if ($this->arrived === false) {
                       $this->arrived = true;
                       //回到callback并根据结果类型调用reject或resolve
                       //用于后续then和otherwise的处理
                       $value = call_user_func($callback, $value);
                       if ($value instanceof FulfilledPromise) {
                           $resolve($value->getValue());
                       } else if ($value instanceof RejectedPromise) {
                           $reject($value->getValue());
                       } else {
                           $resolve($value);
                       }
                   }
               })->otherwise(function ($value) use ($reject) {
               //otherwise中直接调用reject,此时then中的callback不会调用
                   if ($this->arrived === false) {
                       $this->arrived = true;
                       $reject($value);
                   }
               });
           } else if ($promise instanceof RejectedPromise) {
           //一旦promises中存在RejectPromise,直接调用reject
               if ($this->arrived === false) {
                   $this->arrived = true;
                   $reject($promise->getValue());
               }
           } else {
           //其他类型的入参按照resolve来处理
               if ($this->arrived === false) {
                   $this->arrived = true;
                   if ($promise instanceof FulfilledPromise) {
                       $promise = $promise->getValue();
                   }
                   $value = call_user_func($callback, $promise);
                   if ($value instanceof FulfilledPromise) {
                       $resolve($value->getValue());
                   } else if ($value instanceof RejectedPromise) {
                       $reject($value->getValue());
                   } else {
                       $resolve($value);
                   }
               }
           }
       }
   });
}

四、Promise源码

根据上述原理,初步实现了自己的promise,源代码的github地址为https://github.com/xhjcehust/PHPromise,可以安装README.md中的指引安装环境,执行单元测试,验证效果。关于代码有任何建议欢迎反馈,如果觉得代码不错,fork或star是对作者最大的鼓励。

想要获取最新技术文章?欢迎订阅微信公众号----软件编程之路

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息