本文适合对于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需要实现的功能为:

阿里云-推广AD
  • 如果参数数组中所有的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是对作者最大的鼓励。