本文适合对于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是对作者最大的鼓励。
评论