OCPromise-Promise of JS with OC

Posted Jun 16, 20209 min read

OCPromise is a set of chain operation tools for asynchronous task queue management implemented in Objective-C written by Promise in the reference Javescript.

The idea of writing this library is derived from the failure experience of an interview:I used React native for development in my work before, so I also wrote Javascript code and used Promise syntax, but in an interview, the interviewer asked me to write The realization of Promise, I was ignorant at the time, only to find that after the implementation of many things in the development process, copy and paste directly and then change it later, the result is that these things have not become their own knowledge, even right Its understanding is also one-sided.

In retrospect, the calling method of Promise is still very interesting, and the chained syntax is also very beautiful to use, so I tried to implement the Promise function with OC. For the function provided by OCPromise, please refer to this article about JS-Promise: understand Promise in Javascript , I also refer to Promise completely when writing OCPromise, but because of the difference in syntax, the calling method will be slightly different.

Let me first introduce how to use OCPromise:

OCPromise creation

    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 objects created using the Promise() function can handle independent tasks, while when using the function() function to create a Promise object, the actual Promise object creation is delayed until ^OCPromise *(id value) {} execution time, and the Promise task During execution, value can participate in the execution of the internal Promise() task(or not).

Concatenation of OCPromise objects

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

Print result

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

It can be seen that when Promise executes resolve(), the tasks are indeed executed in series. And here we need to note that when using the function() function to build the Promise function, ^OCPromise *(id value) {} cannot be triggered by external execution, but is triggered by the last Promise object executing resolve().

OC Promise reject and catch and finally

The Promise in the example just executed resolve(), which means that the task was successfully processed, and the corresponding reject() is to trigger the exception. For the exception capture of the task queue, we need to use the catch() function.
The finally() function is triggered after the task queue is completed. No matter the entire task queue is successfully completed or an exception occurs, it will be executed. We can do some final processing here, such as the closing of the loading animation or the processing of the final data.
Let's take a look at the cooperative use of reject and catch and how to use finally

//Add a Promise object that triggers rejection
    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);
    });

Print result

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"

When doReject is executed, the input value is greater than 1000, and reject() is executed, so the subsequent add is not executed, and the catch and finally are directly executed.

OCPromise static methods

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 and OCPromise. reject are actually two simple triggers, which are shortcuts for creating a single finger responsibility module. Promise objects created by these two static methods are not affected by external conditions and can only trigger normal execution/A pattern that throws an exception.
Application scenarios such as OCPromise.resolve can be used as the trigger function of the task queue:

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

or:

    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 receives a set of arrays containing Promise objects, and packages these Promise objects to generate a new Promise object. When this Promise is triggered to execute, the tasks in the internal Promise array start to execute asynchronously and concurrently, and all tasks are completed. When the callback is completed, this part is similar to GCD's dispatch_group_notify.
As long as one of the multiple tasks has an exception, it will execute reject and throw the first exception.
If an empty array is received, resolve is executed directly.

    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

)

As you can see, the Promise array is asynchronous and concurrent, so the order of task execution is random and not fixed. The three tasks take 4 seconds, 1 second, and 3 seconds, respectively. The final execution takes 4 seconds and returns In a result array, the order of the values in the array is the same as the order of tasks in the Promise array.
In order to ensure the order of the values in the result array and support the storage of nil values, in fact, the array returned here is a custom NSObject object, and implements some simple calling methods of the array:the value is obtained by subscript value[0], Value[1], objectAtIndex, forin, enumerateObjectsUsingBlock.
Promise array also supports direct pass value, which will be converted into OCPromise.resolve internally.

    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 is also a collection of multi-task concurrent processing. The creation process of Promise is the same as OCPromise.all, except that the completion condition is no longer that all tasks are completed, but a competitive mode. When any task is completed first, no matter whether it is successful or failed , Will directly call back the result, and the remaining task results will be discarded and no longer processed.

    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);
    });

Two different return results:

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

Application scenarios include, for example, requesting the same resource through different interfaces, or adding a timeout operation for a time-consuming operation.

    OCPromise *dealTask   = Promise(^(resolve _Nonnull resolve, reject _Nonnull reject) {
        sleep(5); //Simulate time-consuming operation
        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

The above is the basic function provided by OCPromise, there are a few points to note:

  1. Tasks in OCPromise In order to ensure thread safety and reflect the characteristics of asynchronous, all tasks are executed in sub-threads, so the external callback function needs to pay attention to thread problems. In addition, catch and finally callbacks do not involve the transfer of task results , Internally forced to switch back to the main thread for execution. To monitor the results of the task resolve I provide another function, which will be introduced in the next article.
  2. The creation of OCPromise is mentioned above. Only Promise() and function() are provided for creation. Since the creation of function() function Promise is delayed until the last Promise is completed, the construction method of function() cannot be Used in the creation of the first task.
  3. If neither the resolve method nor the reject method is implemented in Promise(), it will cause the interruption of the transfer of the task queue, and the object cannot be released, resulting in a memory leak.
  4. The result returned by Promise executed by OCPromise.all is an array object. You need to obtain the result of the corresponding task according to the subscript. You cannot use it directly as the result of the task. Even if the incoming Promise array is an empty array, the returned one is an empty array object. If the subscript exceeds the boundary when the value is taken, nil will be returned, and it will not crash.
  5. If you need to collect the processing result at the end of the task queue, you need to connect .then() at the end. Since you need to receive the result of the previous task, you need to use the function() function, and function() does not need to be inside. Create a Promise and return nil directly, so you can't connect .then() later, you can only connect .catch() or .finally(), and these three implementations need to guarantee a strict order, that is.().catch().finally(), you can omit any of them, but you can't repeat it.

The above is the basic usage of the OCPromise library. It may not be as flexible as the Javascript Promise. I hope you can give more advice! In the next article, I will introduce some extensions to the basic implementation of OCPromise. Thank you!

OCPromise-Advanced Usage

github: OCPromise