Hello World
Spiga

Jscex与Promise/A那些事

2012-06-25 19:13 by 老赵, 4402 visits

任何异步编程的类库要做的第一件事往往便是统一异步编程的模型,例如Jscex异步模块自带一个类似于.NET中的异步任务模型。围绕统一的模型,开发人员便可以尽情地提供各种扩展,例如Jscex异步增强模块中的whenAllwhenAny一样。换句话说,假如要混用两种异步编程模型,往往需要将其中一种适配至另外一种,因此异步增强模块中也提供了fromCallbackfromStandard辅助,能够轻易地将最简单的(也是Node.js里使用的)两种异步函数接口绑定为异步任务。那么Promise/A呢?它也是种目前运用十分广泛的异步编程模型,Jscex对它有什么特别的支持吗?当然有,但方式有所不同,更为直接。

Promise/A现在为CommonJS的草案之一,提出了一种Promise模型的设计及API表现。虽说它离“标准”还有很长一段距离,但其实很多类库都已经实现了这个规范了,例如著名的jQuery,node-promise,还有用来编写Win8中Metro应用的HTML5开发平台。当然严格来说,它们都是基于Promise/A规范的一套“扩展实现”,但既然有了共有的子集,那事情就已经好办多了。例如,之前在一个QQ群上某同学建议我提供一个类似于fromStandard一样的fromPromise辅助方法。这当然没问题,其实很简单,接下来也会做,但Jscex考虑地更多。

或者说,就是多问了几个为什么:

为什么需要fromPromise辅助方法?因为用户使用了Promise异步模型,而Jscex希望提供更好的辅助环境。为什么对方不使用Jscex自带的异步任务模型?因为用户可能已经有部分代码采用了Promise模型。为什么它要使用Promise这种已经较为成熟且复杂的异步模型?因为用户可能已经有了一个围绕着Promise模型开发的应用程序,甚至是一个已经拥有大量辅助方法支持的应用开发框架(例如Win8),而在这个情况下再结合Jscex的异步任务模型,则需要来回转换,显得略为冗余。那么,Jscex能否直接对Promise异步模型提供支持呢?

当然可以,从一开始Jscex就是这么设计的,且看这个示例

Jscex.Promise.create = function (init) {
    var dfd = new $.Deferred();
    init(dfd.resolve, dfd.reject);
    return dfd.promise();
}

var oneRoundTripAsync = eval(Jscex.compile("promise", function () {
    $await($("#block").animate({ left: "200px" }, 1000).promise());
    $await($("#block").animate({ left: "0px" }, 1000).promise());
}));

var roundTripsAsync = eval(Jscex.compile("promise", function (n) {
    for (var i = 0; i < n; i++) {
        $await(oneRoundTripAsync());
    }
}));

这是用jQuery自带的animate方法创建动画的示例。正如我之前说的那样,各个模型其实都是基于Promise/A的“扩展”,因此Jscex无法提供一种另所有人都满意的Promise模型,于是它谁都不去迎合,将构造Promise对象的任务交给使用者——这便是上面代码中提供Jscex.Promise.create方法的原因。之后便可以像使用Jscex异步模型那样创建和使用异步方法了,区别仅仅是:

  • 使用promise作为构造器的名称。
  • 异步方法返回的都将是Promise对象。
  • $await操作接受的参数也是Promise对象。
  • 执行异步方法之后,异步操作已经直接启动了,而无需调用start方法。

而想在Win8里开发也一样,首先提供一个用于创建Promise对象的工厂方法:

Jscex.Promise.create = function (init) {
    return new WinJS.Promise(init);
}

然后便可以将以下这段使用回调实现的“提示”、“显示选择器”、“显示图片”这个事务:

var MessageDialog = Windows.UI.Popups.MessageDialog;
var UICommand = Windows.UI.Popups.UICommand;
var FileOpenPicker = Windows.Storage.Pickers.FileOpenPicker;
var PickerViewMode = Windows.Storage.Pickers.PickerViewMode;
var PickerLocationId = Windows.Storage.Pickers.PickerLocationId;
var FileAccessMode = Windows.Storage.FileAccessMode;

WinJS.Namespace.define("MyApp", {
    showPhoto: function () {
        var dlg = new MessageDialog("Do you want to open a file?");
        dlg.commands.push(new UICommand("Yes", null, "Yes"));
        dlg.commands.push(new UICommand("No", null, "No"));

        dlg.showAsync().then(function (result) {
            if (result.id == "Yes") {
                var picker = new FileOpenPicker();
                picker.viewMode = PickerViewMode.thumbnail;
                picker.suggestedStartLocation = PickerLocationId.picturesLibrary;
                picker.fileTypeFilter.push(".jpg");

                picker.pickSingleFileAsync().then(function (file) {
                    if (file != null) {
                        $("#myImg")[0].src = URL.createObjectURL(file);
                    }
                });
            }
        });
    }
});

实现为:

WinJS.Namespace.define("MyApp", {
    showPhoto: eval(Jscex.compile("promise", function () {
        var dlg = new MessageDialog("Do you want to open a file?");
        dlg.commands.push(new UICommand("Yes", null, "Yes"));
        dlg.commands.push(new UICommand("No", null, "No"));

        var result = $await(dlg.showAsync());
        if (result.id == "Yes") {
            var picker = new FileOpenPicker();
            picker.viewMode = PickerViewMode.thumbnail;
            picker.suggestedStartLocation = PickerLocationId.picturesLibrary;
            picker.fileTypeFilter.push(".jpg");

            var file = $await(picker.pickSingleFileAsync());
            if (file != null) {
                $("#myImg").src = URL.createObjectURL(file);
            }
        }
    }))
});

Jscex这不又瞬间支持了Win8开发了吗?此时所有的Jscex异步函数都会返回一个Promise对象,它和WinJS中各种表达异步操作的Promise对象完全相同,也可以和Promise.join以及Promise.any共同使用。而且,实现一个支持Promise的Jscex构造器只需要30多行代码,其中相当部分还是函数定义等架子代码,创建一个Jscex构造器的大部分代码都已经由构造器基础模块提供了。换句话说,假如您的应用中已经有个深入骨髓的异步模型,也只需30多行代码,便可以直接在Jscex中使用了。

这也是Jscex的精妙之处之一:一个简单统一的结构,可以实现出各种灵活的功能。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

44 条回复

  1. rich
    141.88.235.*
    链接

    rich 2012-06-25 21:08:30

    老赵,你是我偶像之一,虽然我看不大懂你的文章

  2. testzhangsan
    116.30.93.*
    链接

    testzhangsan 2012-06-25 21:50:48

    @rich

    有时候我很苦恼,每次看老赵的博客,都有一种自卑感,不知道是自己理解能力有问题,还是老赵故意把文章写得高深,就不能通俗一点写吗,好让更多初级码农也懂嘛!

  3. 链接

    Paul 2012-06-25 22:01:09

    很好很强大~~~

  4. rich
    141.88.235.*
    链接

    rich 2012-06-25 22:14:28

    @testzhangsan

    同感,不过老赵应该自己觉得已经写的够入门了吧,我以前也跟老赵博客,越到后面感觉越不靠谱,后来干脆不跟了,不过倒是时常来看看,也算了解下技术趋势吧。

    另,我钦佩老赵是因为他的价值观,技术倒是其次。在这个物欲横流的社会,能这么执着的有所追求,令人钦佩!

  5. 老赵
    admin
    链接

    老赵 2012-06-25 22:14:49

    @testzhangsan

    哪里写得高深了?哪里看不懂?看不懂就提问啊。

  6. 老赵
    admin
    链接

    老赵 2012-06-25 22:25:08

    @rich: 我以前也跟老赵博客,越到后面感觉越不靠谱,后来干脆不跟了,不过倒是时常来看看,也算了解下技术趋势吧。

    哪方面不靠谱?以前哪些是靠谱的?

  7. rich
    141.88.235.*
    链接

    rich 2012-06-25 22:26:32

    老赵,我的意思是,越来越觉得自己不靠谱,不懂得太多,跟不上你啊

  8. 老赵
    admin
    链接

    老赵 2012-06-25 22:57:56

    @rich

    那就说说到底哪里看不懂咯。

  9. rich
    85.182.37.*
    链接

    rich 2012-06-26 03:49:13

    @老赵

    那我就问了,什么是Promise模型,什么是Promise对象,和那个Task对象又有什么区别?

  10. 老赵
    admin
    链接

    老赵 2012-06-26 09:39:29

    @rich

    链接都给了,为什么都不点过去看看?这种东西肯定没必要在文章里都讲一遍啊。你不是看不懂,而是缺少背景知识,而且没有点开链接看的习惯……

  11. guest
    123.117.37.*
    链接

    guest 2012-06-26 23:17:57

    请问下该博客用的是什么数据库呀?mono+ ?

  12. 老赵
    admin
    链接

    老赵 2012-06-27 09:55:27

    @guest

    MongoDB

  13. Sean
    211.144.200.*
    链接

    Sean 2012-06-27 16:40:27

    您好,正在使用Jscex开发一个项目. 感谢您为简化JS开发作出的贡献。 我现在有个基本问题,查了文档没有找到答案。我想在原生代码中调用jscex的异步代码,希望可以同步返回,现在好像不支持这样的?

    var userInfo = GetUserInfo(uid);//**希望在这里同步返回**。 _现在的情况是异步返回。_
    
    function GetUserInfo(uid){
        var pullUserTask =  eval(Jscex.compile("async", function (ct) {
          var apiResult = new APIRequestResult();
          ....
          return apiResult;
       }));
       new pullUserTask().start();
    }
    

    谢谢。

  14. 老赵
    admin
    链接

    老赵 2012-06-27 16:48:46

    @Sean

    下次发到邮件列表里去吧……

    所有的“同步式的异步调用”一定要在eval(Jscex.compile())里面才能实现,你这个显然已经是在外面了……我感觉你好像没理解Jscex是怎么用的,其实很简单的就是:

    var GetUserInfoAsync = eval(Jscex.compile("async", function (ct) {
        ...
        return apiResult;
    }));
    
    // 另一个Jscex方法里:
    var anotherOneAsync = eval(Jscex.compile("async", function (ct) {
        var result = $await(GetUserInfoAsync(1));
    }));
    
  15. Sean
    211.144.200.*
    链接

    Sean 2012-06-27 17:06:54

    @老赵

    谢谢,我明白你的意思。 但是我需要在eval(Jscex.compile())外面做一些处理(当然这个逻辑可以修改),为什么Task.whenAll在这种逻辑下面不能工作呢?从用户的角度来讲,如果compile之外也可以使用Task.whenAll的话,会更加方便些。我不是特别愿意把所有代码都放在一个comopile里面。这也完全迎合了.Net里面的Task设计理念。

  16. 老赵
    admin
    链接

    老赵 2012-06-27 19:28:28

    @Sean

    没听懂你的意思。compile之外当然可以用Task.whenAll,whenAll只是把一堆Task对象变成一个Task对象而已。你要在外面用的话,也就调用start()就行了。

    Jscex完全符合C#的规则,你可以认为eval(Jscex.compile())其实就相当于C#里的async关键字,只有在async关键字修饰的方法里才能使用await形成“同步”的效果。

  17. 浪雪
    210.13.83.*
    链接

    浪雪 2012-06-28 11:04:59

    老赵的博客一向都通俗易懂,有前因和后果。

  18. youyou
    220.231.59.*
    链接

    youyou 2012-06-28 21:57:18

    老赵你好。我是个菜鸟一直不懂异步编程模型,请问能不能推荐入门书籍。还有我要学习Jscex框架应该从哪入手呢?

  19. 老赵
    admin
    链接

    老赵 2012-06-28 23:58:08

    @youyou

    就普通的JavaScript书籍吧,有名一些的,学习Jscex只有文档可以看……

  20. youyou
    114.112.44.*
    链接

    youyou 2012-06-29 23:59:32

    @老赵

    谢谢,我会好好学学的。我的前段技术不好。

  21. 方舟子
    219.135.147.*
    链接

    方舟子 2012-07-09 12:32:06

    小兄弟这文章涉嫌代笔啊.....

  22. 链接

    rex 2012-07-14 03:07:57

    Jeffrey,有没有打算在你这个个人网站上开个小小的论坛啊?这样的话其他人也可以发起新话题,当然以不喧宾夺主为前提。

  23. 老赵
    admin
    链接

    老赵 2012-07-14 22:15:23

    @rex

    我这个是博客啊,不打算开论坛……

  24. 链接

    rex 2012-07-16 23:36:22

    I have sent you an email to jeffz-at-live.com yesterday. Please let me know if you are using another email. Thanks.

  25. 老赵
    admin
    链接

    老赵 2012-07-17 11:03:52

    @rex

    关于什么内容的啊?

  26. rex
    67.182.140.*
    链接

    rex 2012-07-17 12:48:37

    了解一下合作意向

  27. rich
    141.88.235.*
    链接

    rich 2012-07-18 15:55:34

    @老赵

    拜读了infoq上你的专访,各种高级,各种不懂,问你一个低级的哈

    try 
    {
       var content = $await(read(src, content));
       console.log("内容读取成功");
       $await(write(target, content));
       console.log("内容读取成功");
    }
    catch (ex)
    {
       $await(submitError(ex));
       console.log("错误提交成功");
    }
    

    JavaScript只支持单线程,空出JavaScript线程执行其余操作以后,内容读取操作的线程放到哪里去执行?

  28. noeek
    114.80.133.*
    链接

    noeek 2012-07-18 17:48:44

    老赵先生大陆第二哦 http://sofish.de/file/demo/github/

  29. 老赵
    admin
    链接

    老赵 2012-07-18 22:38:18

    @rich

    内容读取是一个“异步操作”,发起后理论上来说可以认为“不占线程”,直到读完之后再用回调函数传回结果。

    还有这跟你贴的这段代码没有关系,或者我没听懂你到底想问什么……

  30. 老赵
    admin
    链接

    老赵 2012-07-18 22:38:56

    @noeek

    是按照关注人数多少排序的,不是贡献度啊活跃度什么的,意义不大。

  31. rich
    85.182.44.*
    链接

    rich 2012-07-19 03:36:11

    @老赵

    内容读取是一个“异步操作”,发起后理论上来说可以认为“不占线程”,直到读完之后再用回调函数传回结果。

    这个"不占线程"我就不理解了,难道内容读取不用线程的吗?如果用,因为javascript只能有一个线程,那么这个用于内容读取的线程放到哪里去执行?

  32. 链接

    rex 2012-07-19 08:19:16

    Jeffrey,刚才查了一下我的email,发现还没有收到你的回复。请问你列在github上的地址jeffz-at-live-com还在用吗?如果你用其他email的话,请直接回复到rex-at-fastmessenger-com。我们先用email私下联系一下,交换一下看法,如果事情复杂的话,我们再直接电话交流。

  33. 老赵
    admin
    链接

    老赵 2012-07-19 23:42:15

    @rich: 这个"不占线程"我就不理解了,难道内容读取不用线程的吗?

    还真不用占线程,所以异步操作伸缩性好啊,可以同时有好多个IO操作进行,否则每个操作一个线程,每个线程1M地址空间,还要调度开销,你自己算算看。

    异步操作有多种形式,我就拿IOCP做比方,可以简单理解为需要IO操作时,把各种信息,比如要读取哪些数据交给驱动,驱动去“做”,做的时候就是硬件在忙,不关操作系统啊线程啊什么事情,做完之后操作系统再得到通知,再分配个线程做后续工作。

    再不懂的话,自己去找点资料看吧。

  34. 老赵
    admin
    链接

    老赵 2012-07-19 23:43:16

    @rex

    最近忙,周末我仔细看看,不过我搜了下好像没有收到这个地址来的邮件。

  35. 链接

    rex 2012-07-20 03:48:39

    @老赵

    没问题,不是紧急的事。我又往你的live-com-email重发了一次,你再看不到的话,直接回我上面列出的email就是了。主要是要建立个私下接触的管道。

    我也是day job很忙,只有很少的时间做个人兴趣的东西。这个周末会从stack trace的角度来评估一下JSCEX。我虽然不大懂JavaScript,但总觉得它既然是single stack的语言,那外部的library去实现语言级别的功能,肯定会有所区别的。

    如果我的感觉正确的话,那就是说JSCEX实际上是用the second stack trace(用timer触发的吧?)来回到the first stack trace中断的地方。由于JavaScript实际上只有一个stack trace,那么2nd stack的返回值是永远回不到1st stack的吧?因为1st stack都已经不存在了。

  36. 链接

    rex 2012-07-21 00:53:16

    @老赵

    你发来的邮件我能收到,也已经回复了。

    是不是要把我的地址在你的 live.com 里设成 known recipient?

  37. tokimeki
    114.34.164.*
    链接

    tokimeki 2012-07-22 22:08:09

    @老赵

    經過你這樣解釋,我算是比較明白 Task.Factory.FromAsync 實質上應該怎麼用比較妥當了。

    過去我都是把 BeginXXX 跟 EndXXX 直接當參數丟進去。

    照你的提示,實際上應該寫個 EndXXX 的 Wrapper 函數,把非同步回調後的動作寫進去,再把這個 Wrapper 函數取代 EndXXX 函數傳進 Task.Factory.FromAsync 中才對。

    明天來試試看這個寫法~

  38. 老赵
    admin
    链接

    老赵 2012-07-24 10:17:00

    @tokimeki

    不是吧,就是应该直接丢进去啊,什么叫做把非同步回调后的动作写进去?还有你是看我哪句解释?

  39. tokimeki
    114.34.164.*
    链接

    tokimeki 2012-07-24 22:27:14

    @老赵

    底下這句:

    比如要读取哪些数据交给驱动,驱动去“做”,做的时候就是硬件在忙,不关操作系统啊线程啊什么事情,做完之后操作系统再得到通知,再分配个线程做后续工作。

    Task.Factory.FromAsync 做的事情就是包裝 BeginXXX 跟 EndXXX,使其成為 Task 的形式。

    說的詳細點,應該是把 EndXXX 包裝成 Task,然後呼叫 BeginXXX 發起異步,傳回包裝好的 EndXXX 的 Task。

    在 MSDN 的說明備註有這一句:這個方法會擲回任何由 beginMethod 所擲回的例外狀況。

    我是在想,如果是傳回包裝好的 EndXXX 的 Task,那我是不是可以寫一個 EndXXX 的 Wrapper 函數,在此函數裡面直接呼叫原來的 EndXXX,並接收其傳回值做後續的處裡?

    從 Task.Factory.FromAsync 的各種多載形式來看,其參數大多是對應 BeginXXX 的參數,不過我可以透過 State 把需要的 callback 函數及其參數放進去,可以達到我要的效果。

    重點在這裡:不需要另外再為後續處裡產生新的 Task,該 Task 執行完畢時就是後續處理執行完畢了。

    不過我還需要驗證 Task.Factory.FromAsync 產生的線程是否會共用的問題,如果是共用的,那這樣做等於會阻塞其他 Task.Factory.FromAsync 產生的 Task。

  40. 老赵
    admin
    链接

    老赵 2012-07-26 00:51:26

    @tokimeki

    听不懂你再说什么……不就是把BeginXyz和EndXyz丢进去就可以直接用了么,一个异步Task是由Begin和End共同组成的,End返回的结果会成为Task的结果,所以可以直接用来await,你还要为EndXyz写个Wrapper做什么?你什么callback之类的不又变成回调了么,FromBeginEnd就是让你构造一个Task配合await用的。

    至于线程什么的就看Begin/End本身怎么做的啊,原本就是个简单透明的编译器小把戏,怎么被你说的那么复杂。

  41. tokimeki
    114.34.164.*
    链接

    tokimeki 2012-07-26 01:15:21

    @老赵

    其實我本來也沒想的那麼複雜。

    一个异步Task是由Begin和End共同组成的,End返回的结果会成为Task的结果 => 所以 EndXXX 其實是被包在 Task 裡在執行的。

    編譯器耍了個花招讓這個 Task 在執行完 Task.Factory.FromAsync 返回(傳回該 Task 物件,但 Task.Result 不一定立刻可以取得)。

    而我關注是不是可以利用這個把戲,在後續處理中如果需要根據 EndXXX 的傳回值分派不同處理方式時,可以減少生成一次 Task 的機會。

    舉個例子來說,比如說從 UDP 上監聽封包做解碼,假設封包結構有 10 種,我每解碼完一個封包結構,就把解完的物件根據該封包類型去觸發不同的行為(這裡的觸發動作會是一個傳回 Task 物件的函數表),那麼我是否可以利用 Task.Factory.FromAsync 的特性,直接把這個轉發分派的動作跟 EndXXX 合併在一起?

    為了達成這樣的要求,就必須寫一個 EndXXX 的 Wrapper 將原本的 EndXXX 和後續的解碼、轉發分派寫在一起。

    而為了能夠重用,我希望這種 EndXXX 的 Wrapper 針對傳輸方式只寫一次,所以我看了 Task.Factory.FromAsync 的泛型參數簽名,發現其泛型參數對於 EndXXX 只有對其傳回值有對應,因此我把解碼、轉發分派做成 Action 的 callback 形式放在 State 裡面傳遞。

  42. tokimeki
    114.34.164.*
    链接

    tokimeki 2012-07-26 01:18:41

    補充:

    我認為我這樣的做法跟 await 其實蠻接近的,差別只在於後續處裡跑在哪個線程而已。

    await 的後續處裡會回到原來的線程上,而我提的方式則會在執行 EndXXX 那個線程上繼續執行後續處理。

  43. 链接

    rex 2012-07-26 03:40:56

    你们大段大段的讨论,让我在边上看着直着急,为什么不画个图呢?

  44. 老赵
    admin
    链接

    老赵 2012-07-26 10:11:18

    @tokimeki

    一个是await,一个是回调,根本两码事情,哪里接近了?await是一种编程模型,如果你不需要这种模型,你连FromBeginEnd都不需要,直接调用Begin和End方法即可,何必一边FromBeginEnd,又在End里做点花活?

    至于你说await会把结果放回原来线程,还有节省Task数量之类的,建议去看一下去年Build大会上关于这方面的性能的一个Session,具体我记不得了可以搜下,都是已知的情况和现成的解决方案,不用自己猜……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我