asynctask

轻量级异步流程控制工具

npm install asynctask
3 downloads in the last week
5 downloads in the last month

Task - 轻量级异步流程控制工具


背景

Task在Android中开发HybridApp中由于前台和终端交互方式发生变更(由终端使用webview.addJavascriptInterface注册接口改为伪协议,如重载shouldOverrideUrlLoading)的情况下封装的。

需要了解一下

  1. Android在使用addJavascriptInterface向webview注册接口的时候把Object的所有的公共方法都直接提供给JS调用,也就是说可以通过终端提供的方法进行反射拿到JAVA的顶层类执行系统代码。这里存在着非常大的安全隐患,所以建议所有前台/终端交互的方式都通过伪协议的方式进行。

  2. 实际上webview.addJavascriptInterface伪协议对于终端来说没有本质区别。JS调用终端的方法依然会block webview的渲染(整个webview卡住,因为都在一个主线程上)。

  3. shouldOverrideUrlLoading只是其中的一种实现方式,原理其实就通过一些终端内置的并且能够接收前台传参方式来进行交互。例如alertconsole.logconfirmprompt等实现。

但是对于前台来说,两种方式的切换是毁灭性的。因为addJavascriptInterface是同步返回,而伪协议是异步的。如:

    // addJavascriptInterface
    var version = window.agent.getVersion("Android");


    // 伪协议,本例子用console.log实现

    // Schema://NameSpace/Method/RequestID?key=vale
    console.log("JsBridge://agent/getVersion/1?type=Android"); // 调用终接口
    // 终端统一回调前台接口再做分发,注意此处是JAVA代码
    // window.callback(RequestID, {data...})
    webview.loadURL("javascript:window.callback('1', {ret:0, data:'4.0.4'});void(0);"); // 终端回调 

    // 最后的代码可能是这样子 agent.get** 是前台封装好的方法
    agent.getVersion("Android", function(version) {
        // others
    });

    // 甚至,而且细心的同学肯定发现,这里一次拉多个数据的操作是串行的,这无疑给本身执行的速度带来硬伤

    agent.getVersion("Android", function(version) {
        agent.getKey1(arg1, arg2, function(key1) {
            agent.getKey2(arg3, arg4, function(key2){
                    // do something
            });
        });
    });

可以看到,切换伪协议之后的前台代码,每次获取数据都需要作一系列的操作,并且这个是异步的回调。而前台代码本身也有对终端提供的方法作一定的封装并且和逻辑混杂在一起,那么这里每获取一个数据都需要传一个CallBackFun,并且这个CallBackFun需要至顶而下的传递。这无疑是对之前封装的代码一个巨大的伤害

能干什么

而用Task之后,可以使封装的代码改成异步之后更优雅,并且在和一些很恶心的逻辑混杂在一起的时候也具备可读性

    // Task 实现调用agent.getVersion
    require("Task").create(agent.getVersion("Android")).then(function(version) {
        // do something
    }).start();

    // Task 实现批量拉取数据,这里每一个获取的数据都是并行的
    require("Task").create().map(function() {
        return {
            version: agent.getVersion("Android"),
            key1: agent.getKey1(arg1, arg2), // 不改变原有封装参数结构
            key2: agent.getKey2(arg3, arg4)
        };
    }).then(function(ret) { // 这里的ret值就是map操作返回的对象,非常直观
        alert(ret.version);
        // do something
    }).start();

注意:

  1. 上述例子agent.get**需要用Task进行封装
  2. Task只是一个控制异步操作的一个工具,其中调用终端方法分发终端回调并不在Task中实现
  3. 当然,这只是前台/终端交互解决方案的异步流控制的一小部分。

实际上,Task是一个完全和业务逻辑相关的中间件,可以和任意的异步逻辑接合

起源

在JS历史上有很多异步编程工具,包括StepAsync、甚至有赵姐夫Wind.js。这些工具都是为了更方便的简化异步编程工作,并且使代码可读性增加。

Task也是其中的一员。

必须得提一下的是Wind.js给我的震撼是非常大的,正如赵姐夫所说Wind.js的唯一目的便是“改善编程体验”,但是经过Wind.js预处理过的JS代码在终端上调试难度会增加(只能打log,木有断点),最后被我舍弃了。当然,Task也能一定程度上改善异步编程的体验,这会在后续的例子里面体现出来。

在实现异步编程这个问题上已经有很多不同的方案,Task使用的是Promise异步模型。个人认为Promise中规定的是对一个异步操作的模型,操作的结果通过成功和失败两种状态的回调来赋予后续操作。其中有标准Promises/A甚至Promises/A+,而Task只是实现了Promises/A的一部分,并进行了一些修改以适用更复杂的场景。

特性

Task是一个非常轻量级的工具,所以本身支持特性并不多。

  1. 支持链式调用。
  2. 支持.then(onFulfilled,onRejected)方法。
  3. 支持.resolve(msg).reject(msg)执行成功/失败通知。
  4. 支持Task的嵌套。
  5. Task对象执行完之后自动销毁。
  6. 在所有msg传递的过程中,如果msg是Task的实例,就会把当前的Task Block住,当前的父Task会调用msg中的子Task的start方法启动子Task,等待子Task执行完后,把子Task的结果作为父Task下一个操作的输入

    这一点需要非常注意,是Task控制异步流的核心所在。

API

正如前面所述,Task是一个轻量级的工具,提供的接口也非常简单易懂,容易上手。

Task.create(msg)

可以通过Task.create方法创建Task实例,当然也可以用new关键字来创建实例。但是非常推荐用Task.create来创建实例。

    /**
     * @param {Task|Function|Others} msg 初始化消息 
     */
    // Task = require("Task");

    // Task.create
    var task = Task.create(msg);
    // new 关键字
    var task = new Task(msg);

    // msg是一个Function
    var taskFun = Task.create(function() {
            return "TaskFun"; 
    });

    // msg是一个Task实例
    var taskTk = Task.create(Task.create("taskTk"));

注意:

  1. 初始化的消息msg可以传入Task|Function的实例或者其他。msg这是task的第一个操作
  2. msg是Task的实例,则task启动后会先执行msg传递的Task实例。再进行下一个操作。
  3. msg是Function则task启动后会优先处理这个Fun,拿到返回值后再执行下一个操作。
  4. msg是非Task、Function的对象,则直接执行下一个操作的成功回调。

.start()

启动Task。Task开始执行。

    // 启动Task实例
    task.start();

很遗憾,所有Task的实例必须手动启动。主要是为了支持Task的嵌套特性(建立父子Task之间的关系)。

.then(onFulfilled, onRejected)

Task中最基本的方法,相对于上一个操作,其中onFulfilled是操作的成功回调,参数是操作结果,onRejected是操作的失败回调,参数是操作失败抛出的错误信息。

    // then的用法,每一个then都是一个操作,并且可以获取到之前的操作的结果
    task.then(function(ret) {
        // do something
        ...
        return ret1; // 直接使用return作为下一个操作的输入,符合编程习惯
    }, function(error){
        // do something
    }).then(function(ret1){

    });

    // 一个简单的实例。
    // 例子中并没有出现失败回调,实际上在前台操作一般逻辑的时候,很少用到失败回调。
    // 除了请求XHR、JSONP、Image等会出现失败需要处理回调,其他情况失败回调可以为空

    Task.create(1).then(function(ret){ // 这里的ret的值是create时候传的参数

        console.log("task start ret : " + ret); // ret is 1

        var x = ret + 1; // ret is 2

        // return 一个Task的实例
        // 会block父Task下一个操作的执行,等这个子Task执行完后才执行父Task的下一个操作
        return Task.create(function(){ 
            return x + 1; // return value is 3
        }).then(function(childTaskRet){

            console.log("childtask onFulfilled ret : " + childTaskRet); // childTaskRet is 3

            return childTaskRet + 1;
        });

    }).then(function(ret){ // 获取上一个操作的结果,这个操作结果是上面返回子Task的结果

        console.log("task onFulfilled ret : " + ret); // ret is 4

    }).start(); // 别忘记启动task

.map(dataMap|function)

map方法是基于then方法扩展出来,主要是用于批量拉数据的操作。而每个操作都是并行的。

then方法的return值,只会判断是否Task的实例,即只能单个操作。而map方法会判断这个对象(map)里面的每个key-value中的value是否是Task的实例。

    // map 方法例子
    task.map({ // 参数直接是一个Object
        key1: value1,
        key2: value2
    }).map(function(ret){ // 参数是一个Function,然后返回一个Object
        // 对获取到的数据进行操作
        return {
            key3: ret.key1,
            key4: ret.key2
        };
    }).then(function(ret) {
        console.log(ret.key3);
        // do something
    });

    // 使用map方法批量获取数据
    Task.create().map(function() {
        return { // 在这个Object中的Task获取数据是异步的,互不干扰的
            str: "Task result : ", // 非Task的值会直接返回
            pow: Task.create(2).then(function (ret) { // Task实例
                return Math.pow(2, ret); // return 4
            }),
            add: Task.create(Task.create(1).then(function (ret) { // 嵌套的Task实例
                return ret + ret; // return 2 
            })).then(function(parentRet) {
                return parentRet + parentRet; // return 4
            })
        };
    }).then(function (data) { // 这里data参数就是map方法的return值,非常的直观
        console.log(data.str + (data.pow + data.add)); // "Task result : 8"
    }).start()

.resolve(msg) & .reject(msg)

resolvereject 是Task里面手动触发回调的主要方法。但是实际上一般只在封装Adapter的时候需要使用。

  1. .resolve(msg).parentResolve(msg)

    对于本操作,已经成功获取到数据,并且手动执行[父Task]下一个操作成功处理。

  2. .reject(msg).parentReject(msg)

    resolve相对,本操作操作失败,并且手动执行[父Task]下一个操作失败处理

r[parentR]esolver[parentR]eject中传递的msg则分别作为onFulfilledonRejected的参数。

    // 拉取一个百度图片的异步回调例子
    Task.create("http://www.baidu.com/img/bdlogo.gif").then(function(url) {
        var t = this; // 保存引用
        return Task.create(function() {

            var i = new Image();
            i.onload = function() {
                t.resolve(i); // 这里是用本Task的引用直接调用resolve
            };
            i.onerror = function() {
                t.reject("loadError");  // 这里是用本Task的引用直接调用resolve
            };

            i.src = url;

            this.async = true; // 设置异步回调标志位
        });
    }).then(function(img) { // 成功回调里面获取到的是一个Image实例
        console.log("load img success!!" + img.src);
    }, function(errmsg) { // 获取失败,
        console.log("load img error" + errmsg);
    }).start();

一般情况下,在.then方法里面使用return默认是触发成功回调,但是这个过程是同步的,如果是需要异步进行回调会需要用到.resolve(msg).reject(msg)。同时,需要把task的async属性设置为true,表示当前task需要异步返回

    // 在一般场景更推荐封装一个Adapter,这样可以促进代码的重用,并且更好的发挥Task的能力
    var loadImgAdapter = function (src) {
        return Task.create().then(function () {
            var self = this,
                i = new Image();

            i.onload = function() {
                self.parentResolve(i); // 通知父Task触发成功回调
            };
            i.onerror = function() {
                self.parentReject("loadError"); // 通知父Task触发成功回调
            };

            i.src = src;

            this.async = true; // 设置异步回调标志位
        });
    };
    // 通过Adapter拉取图片
    Task.create(loadImgAdapter("http://www.baidu.com/img/bdlogo.gif")).then(function(img) {
        console.log("loadImgAdapter load img success!!" + img.src);
    }, function(errmsg) { // 获取失败,
        console.log("loadImgAdapter load img error" + errmsg);
    }).start();

    // 也可以用map批量拉取图片
    Task.create().map({
        "bdlogo": loadImgAdapter("http://www.baidu.com/img/bdlogo.gif"), // 直接调用Adapter
        "search_logo":  loadImgAdapter("http://tb2.bdstatic.com/tb/static-common/img/search_logo_7098cbef.png")
    }).then(function(success) {
        console.log("loadImgAdapter load img success!!" + success.bdlogo.src);
    }, function(error) {
        // 如果这里拉取图片失败,则会在error里面获知。
        error.search_logo && console.log("loadImgAdapter load img success!!" + error.search_logo); // 加载图片失败的log
    }).start();

不用担心控制.resolve(msg).reject(msg)会很麻烦,因为这些都推荐封装在一个Adapter里面,而前台切换伪协议的时候也只是写了一次,在Jsbridge的Adapter里面。

为什么没有if、timeout等控制操作的方法

这个是个好问题,一个异步流程控制工具里面竟然没有这些方法。

一开始Task就是为了一个"轻"字存在,在第一版的时候甚至连.map()方法都没有。在Task里面,必须保证每一个方法都是有用的并且是必须的

对提供的接口做减法,是我很推崇的做法。所以Task只是拥有极少的接口,但是已经够满足绝大部分的场景。

而且,Task的代码是非常的简单。为其写扩展也是非常容易的。如果有必要,可以直接在上面做扩展支持。

.then().map()本身也是一个用策略模式封装的一个扩展。


切伪协议Task解决方案

Task主要充当的是一个调节前台/终端交互的一个中介。但是他到底只是一个中介,本身不负责请求回调逻辑。所以这里需要和前台/终端交互的工具(包括请求和回调),整合在一起。



    // 一个回调接受器可能是这样
    define('Event', function (require, exports) {
        var ID = 0
        var listener = {};

        exports.add = function (fun) {
            listener[++ID] = fun;
            return ID;
        };
        // 终端通过 require("Event").emit(id, args...); 来回调前台
        exports.emit = function (id) {
            listener[id] && listener[id].apply(null, Array.prototype.slice.call(arguments, 1));
        };
    }


    // 一个发送请求的封装工具肯能是这样
    define("JSBridgeAdapter", function(require, exports) {
        exports.send = function(namespace, fn, args) {
            return Task.create(function () {
                var self = this; // 保存对当前task的引用
                this.async = true; // 采用异步返回

                // 这里每次都返回一个标志当前请求的id
                var id = require("Event").add(function(data) {
                    // 处理终端返回结果,例如耗时上报,错误上报等
                    ...

                    self.parentResolve(data); // 回调父Task
                });

                var uri = "jsbridge://" + [encodeURIComponent(namespace), encodeURIComponent(fn), id || 0].join("/");

                // 拼凑参数
                ...
                   console.log(uri); // 请求终端接口
            });

        };
    });

经过上述这么简单的步骤,就完成了Task对JSBridge的接合。

    // 然后就可以调用终端方法了
    Task.create(require("JSBridgeAdapter").send("agent", "getVersion")).then(function (version) {
        // version 参数就是调用终端方法agent.getVersion()获取到的结果
        // do something
    }).start();

很多时候,我们都有自己封装好的组件,如果每次都想上面那样子调用的确有点麻烦,而且对编辑器提示/跳转支持的也不好,当然,要避免过度封装。

    // 这是一个封装的组件的例子
    define("Agent", function(){
        return {
            getVersion: function(key) {
                return require("JSBridgeAdapter").send("agent", "getVersion", [key]); // 这里返回的是一个Task实例
            },
            ... // other Codes
        };
    });

可以看到,Task方案不用传递CallBackFun,也就是不会破坏原有封装组件的参数结构

甚至更近一步,做缓存机制,因为getVersion这种公共接口可能被调用很多次

    define("Agent", function(){
        var cache = {};
        return {
            getVersion: function(key) {
                // 如果缓存里面有,则直接返回缓存的数据, 如果缓存里面没有返回一个Task实例去获取数据
                if(cache[key]) {
                    return cache[key];
                } else {
                    return Task.create(require("JSBridgeAdapter").send("agent", "getVersion"), [key]).then(function(version) {

                        cache[key] = version; // 做一个缓存,获取到值之后,再次调用这个接口就不用再向终端拉数据了
                        return version; // 这里把version返回出去
                    }); 
                }
            },
            ... // other Codes
        };
    });

这样就更放心的调用这个接口了

    // 封装好之后的调用方法,就达到本文开始的效果了。
    Task.create(require("Agent").getVersion("Android")).then(function (version) {
        // version 参数就是调用终端方法agent.getVersion()获取到的结果
        // do something
    }).start();

还在等什么,赶紧搞起吧

Task方案已经在手Q4.6 AppStore前台切伪协议的时候使用。

Source

Task源码可以从Github上获取。目前Task还在不断的更新中。

npm loves you