视频 http://v.youku.com/v_show/id_XMjMyNzU0NjY4.html
jscex 原理探析
这是我第一次给别人的类库写文章,说实话,用 Lofn 中的阻塞原语生成的代码可不是很容易看懂(虽然代码很好写……),相比之下,
了,这个任务接下来的命运就是析构。下面是一个简单的任务实现:延迟调用回调 n 毫秒:
|
|
|
|
|
|
|
|
老赵 的 jscex 要好用得多。虽然 jscex 还没有完成(很多语法都处理不了),不过呢,第一,他已经大致能投入使用;第二,说说总比没有好,而且这个很有潜力的东西还是需要来推荐的。相信无数的 JSER 都有过这样的经历,动画,然后用 while 循环来做,结果在浏览器卡了一小会有,要运动的元素直接蹦到目标位置上去了。于是,不解,论坛提问,然后查 setTimeout 和 setInterval 的案例,无数挫折后终于成功。jscex 给他们带来了曙光:只需要写个循环,循环里放个定时器回调,接下来一切事情由 jscex 解决。然后——哇,动起来了耶!嘛,先看案例:var moveAsync = eval(Jscex.compile("$async", function(e, startPos, endPos, duration) { for (var t = 0; t < duration; t += 50) { e.style.left = startPos.x + (endPos.x - startPos.x) * (t / duration); e.style.top = startPos.y + (endPos.y - startPos.y) * (t / duration); $await(Jscex.Async.sleep(50)); } e.style.left = endPos.x; e.style.top = endPos.y; })); ps. 等价的 lofn 阻塞原语代码using async, sleep in library "async"; using asynchronous as async; var moveAsync = asynchronous function(e, startPos, endPos, duration): for(var t = 0; t < duration; t += 50): e.style.left = ...; e.style.top = ...; await.. sleep 50; end; e.style.left = endPos.x; e.style.top = endPos.y; end 和 lofn 的阻塞原语不同,jscex 生成的代码采用所谓“Monad”的技术,或者说是基于任务的异步模型。整个异步函数会被拆解成若干个任务,回调嵌入回调。只是,原来写 N 层回调的工作现在由编译器完成,不用人肉折磨。看看上面那段代码经过 jscex 倒腾过的结果:function(e, startPos, endPos, duration) { return $async.Start(this, function() { return $async.Combine( $async.Delay(function() { var t = 0; return $async.Loop( function() { return t < duration; }, function() { t += 50; }, $async.Delay(function() { e.style.left = startPos.x + (endPos.x - startPos.x) * t / duration; e.style.top = startPos.y + (endPos.y - startPos.y) * t / duration; return $async.Bind(Jscex.Async.sleep(50), function() { return $async.Normal(); }); }) ); }), $async.Delay(function() { e.style.left = endPos.x; e.style.top = endPos.y; return $async.Normal(); }) ); }); }; 这是那个 MoveAsync 函数应该有的样子,谢天谢地,我们有 jscex,否则肯定会被这一对 function 给搞晕死!异步编程向来很难,这是真话,在几乎所有的语言/平台上,我们都必须面对成堆的回调,把原本清晰的流程拆的七零八落,因为异步程序通常是:先通知某个对象“我要点东西,你给我搞来”,然后——总不能就这样挂着吧!哦,于是,回调:在用 AJAX 的时候,我们是注册 onreadystatechange;在等用户反应的时候,我们用 onclick。用上这些事件的代码大致是这样:// ...... someobj.onsuccess = function(data){ process(data) }; someobj.getData() 嗯,世界可没有那么美好,于是乎加上失败处理的代码:// ...... someobj.onsuccess = function(data){ process(data) }; someobj.onfail = function(){ alert("Fail!") } someobj.getData({timeout: 3000}) 不过我们想要的是这样的代码:var data = someobj.getData({timeout: 3000}); if(data){ process(data) } 问题的核心在于,someobj.getData 不能在当前的流程中形成阻塞,因为只是通知了 someobj “要个东西”,就忽闪而过了。上面的代码只是个很简单的案例,想一下,如果你要在一个循环中放上这样的 someobj.getData,逐个获取数据然后处理,那个复杂度,呵呵……我们希望的是,能够用某种方法来让一个流程从中间暂停(至少要长得像是暂停),然后在某个时候继续。在一些语言(比如 lua)里面叫做协同例程。协同例程里面用 coroutine.yield() 就可以把它自己挂起,然后等外界消息。C# 提供的迭代器(yield)也有相同的功效,虽然要弱一点。可惜,JavaScript 没有这种东西,我们不能在函数执行到半截的时候把它挂起,然后等到某个合适的时候继续。遇到异步问题的可不是只有我和老赵,一个叫 Jasmine 的测试框架也遇到了异步的问题,他们的方法是用 DSL(源码来自官网):describe('Spreadsheet', function() { it('should calculate the total asynchronously', function () { var spreadsheet = new Spreadsheet(); spreadsheet.fillWith(lotsOfFixureDataValues()); spreadsheet.asynchronouslyCalculateTotal(); waitsFor(function() { return spreadsheet.calculationIsComplete(); }, "Spreadsheet calculation never completed", 10000); runs(function () { expect(spreadsheet.total).toEqual(123456); }); }); }); 这样的处理方法不错,事实上,在我还没有给 lofn 做阻塞原语的时候,我也想过这样的代码:(我使用了 lofn 的惯用法:柯里化而非多参数)describe("Spradsheet") { it("should aclculate the total asynchronously") asynchronous { var speadsheet; @run { spradsheet = new Spradsheet; spradsheet.fillWith lotsOfFixureDataValues(); spradsheet.asynchronouslyCalculateTotal(); } @waits spradSheet.calculationIsComplete, timeout: 10000, onTimeout: "Spradsheet calculation never completed" @run { expect(wpradSheet.total).toEqual 123456 } } } 首先,DSL 设计的自由度很大,不同人可能有不同的“DSL 接口”,相互之间难以互通(“倒腾”);其次,这代码还是被无数的 function 拆的七零八落,感觉还是不爽。这时候,老赵横空出世,带着他高超的编译器技术来拯救人类!他的想法是:因为 JavaScript 本身动态性极强,语义分析可以说是天方夜谭,那痛苦自然难以避免。但是让人的痛苦减少到最低,总没坏处吧!于是他的 jscex 有这样的约定:用一个特殊的“关键字”(在异步调用中,是 $await)表示可能“有事情”(在异步调用中就是阻塞)的操作用编译器把普通函数转换为类似 DSL 的形式jscex 的基本部分称为任务(虽然源码中没有显式出现这个词),一个任务的范式如下:aTask = { start: function(callback){ ...... callback(result) }) } start 方法接受一个函数作为回调,在捣腾一番之后调用这个回调。注意,回调只会调用一次,而且调用完,就称任务 完成 var sleep = function(dur){ return { start: function(callback){ setTimeout(function(){ callback() }, dur) } } } // 使用 sleep(1000).start(function(){ ...... }); 既然有了这个简单的范式,自然可以想到,连续执行两个任务可以这样写(我实在是看不惯 continuus,所以 mizpell 回):var continus = function(a, b){ return { start: function(callback){ a.start(function(){ b.start(callback) }) } } } Hmmmm,不难理解吧。类似的还可以做循环,用递归实现:var loopWhile = function(conditionF, t){ var loop = { start: function(callback){ if(!conditionF()) callback() else t.start(function(){ loop.start() }) } }; return loop; } 这里假定 conditionF 是个简单的函数,不是任务。好吧,其实要是任务也很好做,这里利用了任务回调的回传:var loopWhile = function(condition, body){ var loop = { start: function(callback){ condition.start(function(continueQ){ if(conditionQ) body.start(function(){ loop.start(callback) }) else callback() }) } }; return loop; } 为了增加任务的种类,这里做一个不像任务的任务:immediate,他的作用是“执行函数;然后执行回调,没有任何延迟”:var immediate = function(f){ return { start: function(callback){ callback(f()) } } } 现在我们可以去对付最开始的案例了,不过呢,先考虑下最简单的状况吧:显示 1、2 和 3,间隔一秒。假定显示用 display 函数完成,在平常,用裸的 setTimeout 怎么做呢?嗯:var play = function(){ display(1); setTimeout(function(){ display(2); setTimeout(function(){ display(3); }, 1000) }, 1000) } 在使用了上面的任务范式后,代码是这样:var play = function(){ continus(immediate(function(){display(1)}), continus(sleep(1000), continus(immediate(function(){display(2)}), continus(sleep(1000), immediate(function(){display(3)}) ) ) ) ).start(function(){}); } ps. 现在是看到 lofn 好处的时候了var play = function: continus(immediate {display 1}, continus(sleep 1000, continus(immediate {display 2}, continus( sleep 1000, immediate {display 3} ) ) ) ).start(function{}) end 我了个去,这代码还复杂了呢,但是别看嵌套层数变大了,代码的“熵”(混乱度)实际上下降了。这种代码更适合用机器生成:jscex 就是生成这种代码(虽然具体细节不同)。而且这种长得很 lisp 的代码没有所谓的“数学的美感”吗?好,上面的案例是一个异步方法“最应该有”的样子,不过显然可以优化。有没有感觉到最后那个小尾巴(.start(function(){}))很难看?确实,这个小尾巴很难看,而且怎么说也应该放在外面吧。然后,众多的 immediate 仍然很别扭。此外,我们真实写的代码肯定有复杂的控制流程——至少说得实现 return 吧,就是随时停止整个流程。所以,前面任务的 callback 就要重构,参数从一个改成两个,第一个是流程状态:var NORMAL = 0; var RETURN = 1; 然后对应修改 continus 和 immediate:var continus = function(a, b){ return { start: function(callback){ a.start(function(phase){ if(phase == NORMAL) b.start(callback) else callback.apply(this, arguments) }) } } } var immediate = function(f){ return { start: function(callback){ callback(NORMAL, f()) } } } loopWhile 也可以同样修改:var loopWhile = function(condition, body){ var loop = { start: function(callback){ condition.start(function(phase, continueQ){ if(phaseQ === NORMAL && conditionQ) body.start(function(phase){ if(phase === NORMAL) loop.start(callback) else callback.apply(this, arguments) }) else callback.apply(this, arguments) }) } }; return loop; } 解决问题要紧,就先不考虑什么异常、break 的问题了。为了消除 immediate,接下来是一个叫 bind 的函数,他是 continus 的变体,但是比 continus 有用得多:// 定义 var bind = function(t, g){ return continus(t, g()) } 做一个辅助函数 Normal:var Normal = function(){ return { start: function(callback){ callback(NORMAL) } } } 可以推导:immediate(f) === continus(immediate(f), Normal()) continus(t1, continus(immediate(f), t2) === bind(t1, function(){f(); return t2}) 哈,这回里层的 immediate 彻底消失了!最外层的 immediate 也可以类似处理:var flow = function(g){ var t = g(); return { start: function(callback){ t.start(callback) } } } // 众人回应:直接 return g() 不就行了吗! 因此,前面的案例 play 就可以写成:var play = flow(function(){ display(1); return bind(sleep(1000), function(){ display(2); return bind(sleep(1000), function(){ display(3) return Normal() }) }) }) play().start() 呼,代码清爽多了。以上所写的这一些就是 jscex 的内核。下面比较下两者:案例jscexbindBinder.bindcontinusBinder.CombineflowBinder.Delay、Binder.Start (后者处理 this 指针的问题)loopWhileBinder.Loop可以看出,jscex 的 Binder 是一个更加灵活的方案。我所写的案例只是对 jscex 模型一个很粗略的模仿。jscex 通过 Narcissus 来解析已有的 js 代码,然后编译成使用了 Binder 的形式。jscex 源码中还包括了对 this 指针的处理、break 语句(在 Binder.Loop/loopWhile 里面加几个分支判断)、简单的异常,等等。在最后,即使 jscex 现在还没有完成,但他仍然是很有潜力的。听说 lifesinger 也在做 async,还要用所谓的“新方法”,等时候到了写横评吧。 on typeof.net: http://typeof.net/2010/12/a-simple-study-on-jscex-mechanism/