乐趣区

OCPromise用OC实现JS的Promise

OCPromise 是参考的 Javescript 中的 Promise 写的一套用 Objective- C 实现的异步任务队列管理的链式操作工具。

写这套库的想法是源自一次面试的失败经历:之前在工作中我使用过 React native 进行开发,因此也写过 Javascript 代码并且使用过 Promise 语法,但是在一次面试中,面试官让我手写 Promise 的实现,当时我直接懵了,才发现在开发过程中很多东西实现过一次之后,后面再用到时直接复制粘贴再改一改,结果就是这些东西根本没有变成自己的知识,甚至对它的理解也很片面。

回想起来,Promise 的调用方式还是很有意思的,链式的语法使用起来也很美观,因此我尝试用 OC 实现了 Promise 的功能,OCPromise 提供的功能可以参考这篇关于 JS-Promise 的文章:理解 Javascript 中的 Promise,我在写 OCPromise 时也是完全参照的 Promise,只不过由于语法的差异性,调用方法会略有不同。

下面我先介绍一下 OCPromise 的使用方法:

OCPromise 的创建

    OCPromise *p = Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {NSLog(@"start new Promise...");
        resolve(@123);
    });
    
    OCPromise *multiply = function(^OCPromise * _Nullable(id  _Nonnull value) {return Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {NSLog(@"calculating %ld x %ld ...", [value longValue], [value longValue]);
            resolve([NSNumber numberWithLong:[value longValue] * [value longValue]]);
        });
    });
    
    OCPromise *add = function(^OCPromise * _Nullable(id  _Nonnull value) {return Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {NSLog(@"calculating %ld + %ld ...", [value longValue], [value longValue]);
            resolve([NSNumber numberWithLong:[value longValue] + [value longValue]]);
        });
    });

使用 Promise() 函数创建的 Promise 对象可以处理独立的任务,而使用 function() 函数创建 Promise 对象时,实际 Promise 对象的创建延迟到 ^OCPromise *(id value) {} 执行时期,并且 Promise 的任务执行期间 value 可以参与内部的 Promise() 任务的执行的(也可以不参与)。

OCPromise 对象的串联

    p.
    then(multiply).
    then(add).
    then(multiply).
    then(add).
    then(function(^OCPromise * _Nullable(id  _Nonnull value) {NSLog(@"Got value: %@",value);
        return nil;
    }));

打印结果

2020-05-29 15:33:34.955691+0800 OCPromise_Example[80577:17114562] start new Promise…
2020-05-29 15:33:34.956269+0800 OCPromise_Example[80577:17114562] calculating 123 x 123 …
2020-05-29 15:33:34.957493+0800 OCPromise_Example[80577:17114562] calculating 15129 + 15129 …
2020-05-29 15:33:34.958875+0800 OCPromise_Example[80577:17114562] calculating 30258 x 30258 …
2020-05-29 15:33:34.960475+0800 OCPromise_Example[80577:17114562] calculating 915546564 + 915546564 …
2020-05-29 15:33:34.961727+0800 OCPromise_Example[80577:17114562] Got value: 1831093128

可以看到,当 Promise 执行了 resolve(),任务确实被串了起来顺序的执行。并且这里我们需要注意,使用 function() 函数构建 Promise 函数时,^OCPromise *(id value) {} 并不能通过外部进行触发执行,而是由上一个 Promise 对象执行完 resolve() 进行触发的。

OCPromise 的 reject 与 catch 与 finally

刚才示例中 Promise 都是执行的 resolve(),这表示任务处理成功,而对应的 reject() 则是触发异常情况,针对任务队列的异常捕获我们要用到 catch() 函数。
finally() 函数是在任务队列执行完毕后触发执行的,无论整个任务队列成功完成还是出现了异常都会执行,我们可以在这里进行一些最终处理,比如加载动画的关闭或者最终数据的处理等。
下面我们来看一下 reject 与 catch 的配合使用以及 finally 的使用方法

// 增加一个触发 reject 的 Promise 对象
    OCPromise *doReject = function(^OCPromise * _Nullable(id  _Nonnull value) {return Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {NSLog(@"receive %ld",[value longValue]);
            if ([value longValue] > 1000) {reject(@"opps, number is too big");
            } else {resolve(value);
            }
        });
    });

    p.
    then(multiply).
    then(doReject).
    then(add).
    catch(^(id  _Nonnull value) {NSLog(@"catch error, reason is \"%@\"",value);
    }).
    finally(^(id  _Nonnull value) {NSLog(@"final value is \"%@\"",value);
    });

打印结果

2020-05-29 16:17:49.402107+0800 OCPromise_Example[80859:17146759] start new Promise…
2020-05-29 16:17:49.402549+0800 OCPromise_Example[80859:17146759] calculating 123 x 123 …
2020-05-29 16:17:49.403076+0800 OCPromise_Example[80859:17146759] receive 15129
2020-05-29 16:17:49.403401+0800 OCPromise_Example[80859:17146759] catch error, reason is “opps, number is too big”
2020-05-29 16:17:49.403814+0800 OCPromise_Example[80859:17146759] final value is “opps, number is too big”

在执行到 doReject 时入参 value 大于 1000,执行了 reject(),因此后面的 add 并没有执行,直接执行了 catch 和 finally。

OCPromise 的静态方法

OCPromise.resolve

    OCPromise.resolve(@"Just do it!").then(function(^OCPromise * _Nullable(id  _Nonnull value) {NSLog(@"%@",value);
        return nil;
    }));

2020-05-29 16:29:39.036376+0800 OCPromise_Example[80944:17156364] Just do it!

OCPromise. reject

    OCPromise.reject(@"Oops!").catch(^(id  _Nonnull value) {NSLog(@"%@",value);
    });

2020-05-29 16:31:39.463002+0800 OCPromise_Example[80971:17158013] Oops!

OCPromise.resolve 和 OCPromise. reject 其实就是两个简单的触发器,是创建单一指责任务模块的快捷方式,由这两个静态方法创建的 Promise 对象不受外部条件的影响,并且仅能触发正常执行 / 抛出异常一种模式。
应用场景例如 OCPromise.resolve 可以作为任务队列的触发函数:

    OCPromise.resolve(@123).then(multiply).then(add);

或者:

    p.then(function(^OCPromise * _Nullable(id  _Nonnull value) {if ([value longValue]>1000) {return OCPromise.resolve(value);    //here!!!} else {return OCPromise.reject(@"Oops,got error");    //here!!!}
    })).then(function(^OCPromise * _Nullable(id  _Nonnull value) {NSLog(@"got value %@", value);
        return nil;
    })).catch(^(id  _Nonnull value) {NSLog(@"catch error %@",value);
    });

OCPromise.all

OCPromise.all 接收一组容纳 Promise 对象的数组,并将这些 Promise 对象打包生成一个新的 Promise 对象,这个 Promise 被触发执行时,内部的 Promise 数组中的任务开始异步并发执行,并在所有任务都完成时回调完成,这部分和 GCD 的 dispatch_group_notify 类似。
多个任务中只要有一个任务出现异常,则会执行 reject 抛出第一个发生的异常。
如果接收到一个空数组则直接执行 resolve。

    OCPromise *task1 = function(^OCPromise * _Nullable(id  _Nonnull value) {return Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {NSLog(@"task1 needs sleep 4sec");
            sleep(4);
            NSLog(@"task1 woke up");
            resolve([NSString stringWithFormat:@"task1 checked %@",value]);
        });
    });
    
    OCPromise *task2 = Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {NSLog(@"task2 needs sleep 1sec");
        sleep(1);
        NSLog(@"task2 woke up");
        resolve(@"task2 is fine");
    });
    
    OCPromise *task3 = function(^OCPromise * _Nullable(id  _Nonnull value) {return Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {NSLog(@"task3 needs sleep 3sec");
            sleep(3);
            NSLog(@"task3 wokeup");
            resolve([NSString stringWithFormat:@"task3 ignored %@",value]);
        });
    });
    
    OCPromise *all = OCPromise.all(@[task1, task2, task3]);
    
    OCPromise.resolve(@"the wallet").then(all).then(function(^OCPromise * _Nullable(id  _Nonnull value) {NSLog(@"got value %@", value);
        return nil;
    }));

2020-06-01 17:51:42.608045+0800 OCPromise_Example[89417:18881922] task1 needs sleep 4sec
2020-06-01 17:51:42.608099+0800 OCPromise_Example[89417:18881919] task3 needs sleep 3sec
2020-06-01 17:51:42.608132+0800 OCPromise_Example[89417:18881925] task2 needs sleep 1sec
2020-06-01 17:51:43.609261+0800 OCPromise_Example[89417:18881925] task2 woke up
2020-06-01 17:51:45.609812+0800 OCPromise_Example[89417:18881919] task3 wokeup
2020-06-01 17:51:46.612289+0800 OCPromise_Example[89417:18881922] task1 woke up
2020-06-01 17:51:46.613935+0800 OCPromise_Example[89417:18881920] got value (

task1 checked the wallet,
task2 is fine,
task3 ignored the wallet

)

可以看到,Promise 数组由于是异步并发的,所以任务执行的顺序是随机不固定的,三个任务耗时分别是 4 秒、1 秒、3 秒,最终执行完毕时耗时 4 秒,并返回一个结果数组,数组内值的顺序和 Promise 数组的任务顺序一致。
为保证结果数组内值的顺序,并且支持存储 nil 值,其实这里返回的数组是一个自定义的 NSObject 对象,并实现了数组的一些简单调用方法:通过下标进行取值 value[0]、value[1],objectAtIndex,forin,enumerateObjectsUsingBlock。
Promise 数组内也支持直接传值,内部会转成 OCPromise.resolve。

    OCPromise *all = OCPromise.all(@[@"Goodjob", @666, OCPromise.resolve(nil)]);
    all.then(function(^OCPromise * _Nullable(id  _Nonnull value) {NSLog(@"first obj %@", value[0]);
        NSLog(@"second obj %@", [value objectAtIndex:1]);
        for (id obj in value) {NSLog(@"forin obj %@",obj);
        }
        [value enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {NSLog(@"enumerate block at %ld obj %@",idx, obj);
        }];
        return nil;
    }));

2020-06-01 18:08:47.032494+0800 OCPromise_Example[89678:18896195] first obj Goodjob
2020-06-01 18:08:47.033058+0800 OCPromise_Example[89678:18896195] second obj 666
2020-06-01 18:08:47.033555+0800 OCPromise_Example[89678:18896195] forin obj Goodjob
2020-06-01 18:08:47.034477+0800 OCPromise_Example[89678:18896195] forin obj 666
2020-06-01 18:08:47.035326+0800 OCPromise_Example[89678:18896195] forin obj (null)
2020-06-01 18:08:47.036120+0800 OCPromise_Example[89678:18896195] enumerate block at 0 obj Goodjob
2020-06-01 18:08:47.036878+0800 OCPromise_Example[89678:18896195] enumerate block at 1 obj 666
2020-06-01 18:08:47.037486+0800 OCPromise_Example[89678:18896195] enumerate block at 2 obj (null)

OCPromise.race

OCPromise.race 也是多任务并发处理的集合,Promise 的创建过程和 OCPromise.all 相同,只不过完成的条件不再是所有任务全部完成,而是竞争模式,当任意一个任务率先完成,无论成功还是失败,都会直接将该结果回调,其余任务结果则丢弃不再处理。

    OCPromise.race(@[@666, OCPromise.reject(@"oops")]).then(function(^OCPromise * _Nullable(id  _Nonnull value) {NSLog(@"got value %@", value);
        return nil;
    })).catch(^(id _Nonnull value) {NSLog(@"got error %@", value);
    });

两次不同的返回结果:

2020-05-29 18:14:43.770779+0800 OCPromise_Example[81758:17233041] got value 666

2020-05-29 18:13:13.503533+0800 OCPromise_Example[81745:17231231] got error oops

应用的场景例如通过不同的接口请求相同的资源,或者为某个耗时操作添加超时操作等。

    OCPromise *dealTask = Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {sleep(5);  // 模拟耗时操作
        resolve(@"done");
    });
    
    OCPromise *timeout = Promise(^(resolve  _Nonnull resolve, reject  _Nonnull reject) {dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{reject(@"time out");
        });
    });
    NSLog(@"task start");
    OCPromise.race(@[dealTask, timeout]).then(function(^OCPromise * _Nullable(id  _Nonnull value) {NSLog(@"result is %@", value);
        return nil;
    })).catch(^(id _Nonnull value) {NSLog(@"%@", value);
    });

2020-05-29 18:22:01.598934+0800 OCPromise_Example[81826:17238770] task start
2020-05-29 18:22:04.601462+0800 OCPromise_Example[81826:17238841] time out

以上就是 OCPromise 提供的基本功能了,有几点需要注意的:

  1. OCPromise 内的任务为保证线程安全及体现异步的特点,所有的任务都是在子线程执行的,因此对外的回调函数需注意线程问题,另外 catch 和 finally 的回调因为不涉及任务结果的传递,在内部强制切回了主线程进行执行。针对任务 resolve 的结果进行监听我提供了另外一个函数,将在下一篇文章进行介绍。
  2. OCPromise 的创建在上面提过,仅提供 Promise() 及 function() 两种方式进行创建,由于 function() 函数 Promise 的创建延迟到上一个 Promise 执行完毕时,因此 function() 的构建方式不能用在首任务的创建。
  3. 如果在 Promise() 中既不实现 resolve 方法也不实现 reject 方法,则会造成任务队列传递的中断,对象无法释放而产生内存泄漏。
  4. 通过 OCPromise.all 执行的 Promise 返回的结果都是一个数组对象,需按照下标获取对应任务的结果,不能直接做为任务结果使用。即便传入的 Promise 数组是个空数组,返回的也是一个空数组对象,取值时如果下标越界则返回 nil,不会崩溃。
  5. 任务队列的末尾如果需要收集处理结果时,需在末尾处连接.then(),由于需要接收上一个任务的结果,所以需要用到 function() 函数,而 function() 内部则不需要再创建 Promise,直接 return nil 即可,这样实现的话后面就不能再连接.then() 了,只能连接.catch() 或.finally(),并且这三种实现需保证严格的顺序,即.then().catch().finally(),可以省略其中的任意几项,但不能重复实现。

以上就是 OCPromise 库的基本用法,可能没有 Javascript 的 Promise 那么灵活,希望看到的您能多给提提意见!下一篇文章我介绍一下针对 OCPromise 的基本实现做的一些扩展。谢谢!

OCPromise- 进阶用法

github:OCPromise

退出移动版