一种模仿线程的Javascript异步模型设计&实现
2013-01-25 08:40 Justany_WhiteSnow 阅读(2317) 评论(4) 编辑 收藏 举报jQuery中所支持的异步模型为:
- Callbacks,回调函数列队。
- Deferred,延迟执行对象。
- Promise,是Deferred只暴露非状态改变方法的对象。
这些模型都很漂亮,但我想要一种更帅气的异步模型。
Thread?
我们知道链式操作是可以很好的表征运行顺序的(可以参考我的文章《jQuery链式操作》),然而通常基于回调函数或者基于事件监听的异步模型中,代码的执行顺序不清晰。
Callbacks模型实际上类似一个自定义事件的回调函数队列,当触发该事件(调用Callbacks.fire())时,则回调队列中的所有回调函数。
Deferred是个延迟执行对象,可以注册Deferred成功、失败或进行中状态的回调函数,然后通过触发相应的事件来回调函数。
这两种异步模型都类似于事件监听异步模型,实质上顺序依然是分离的。
当然Promise看似能提供我需要的东西,比如Promise.then().then().then()。但是,Promise虽然成功用链式操作明确了异步编程的顺序执行,但是没有循环,成功和失败分支是通过内部代码确定的。
个人认为,Promise是为了规范化后端nodejs中I/O操作异步模型的,因为I/O状态只有成功和失败两种状态,所以他是非常成功的。
但在前端,要么只有成功根本没有失败,要么不止只有两种状态,不应当固定只提供三种状态的方案,我觉得应该提供可表征多状态的异步方案。
这个大家可以在something more看到。
我想要一种类似于线程的模型,我们在这里称为Thread,也就是他能顺序执行、也能循环执行、当然还有分支执行。
顺序执行
线程的顺序执行流程,也就是类似于:
do1();
do2();
do3();
这样就是依次执行do1,do2,do3。因为这是异步模型,所以我们希望能添加wait方法,即类似于:
do1(); wait(1000); //等待1000ms do2(); wait(1000); //等待1000ms do3(); wait(1000); //等待1000ms
不使用编译方法的话,使用链式操作来表征顺序,则实现后的样子应当是这样的:
Thread(). //获取线程 then(do1). //然后执行do1 wait(1000). //等待1000ms then(do2). //然后执行do2 wait(1000). //等待1000ms then(do3). //然后执行do3 wait(1000); //等待1000ms
循环执行
循环这很好理解,比如for循环:
for(; true;){ dosomething(); wait(1000); }
进行无限次循环执行do,并且每次都延迟1000ms。则其链式表达应当是这样的:
Thread(). //获取线程 loop(-1). //循环开始,正数则表示循环正数次,负数则表示循环无限次 then(dosomething). //然后执行do wait(1000). //等待1000ms loopEnd(); //循环结束
这个可以参考后面的例子。
分支执行
分支也就是if...else,比如:
if(true){ doSccess(); }else{ doFail(); }
那么其链式实现应当是:
Thread(). //获得线程 right(true). //如果表达式正确 then(doSccess). //执行doSccess left(). //否则 then(doFail). //执行doFail leftEnd(). //left分支结束 rightEnd(); //right分支结束
声明变量
声明变量也就是:
var a = "hello world!";
可被其它函数使用。那么我们的实现是:
Thread(). //得到线程 define("hello world!"). //将回调函数第一个参数设为hello world! then(function(a){alert(a);}); //获取变量a,alert出来
顺序执行实现方案
Thread实际上是一个打包函数Fn队列。
所谓打包函数就是将回调函数打包后产生的新的函数,举个例子:
function package(callback){
return function(){
callback();
// 干其他事情
}
}
这样我们就将callback函数打包起来了。
Thread提供一个fire方法来触发线程取出一个打包函数然后执行,打包函数执行以后回调Thread的fire方法。
那么我们就可以顺序执行函数了。
现在只要打包的时候设置setTimeout执行,则这个线程就能实现wait方法了。
循环执行实现方案
循环Loop是一个Thread的变形,只不过在执行里面的打包函数的时候使用另外一种方案,通过添加一个指针取出,执行完后触发Loop继续,移动指针取出下一个打包函数。
分支执行实现方案
分支Right和Left也是Thread的一种变形,开启分支的时候,主Thread会创建两个分支Right线程和Left线程,打包一个触发分支Thread的函数推入队列,然后当执行到该函数的时候判断触发哪个分支执行。
其中一个队列执行结束后回调主Thread,通知进行下一步。
例子
由于该方案和wind-asycn非常相似,所以我们拿wind.js中的clock例子进行改造看看其中的差别吧。
wind.js中的例子:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Clock - Wind.js Sample</title> <meta http-equiv="X-UA-Compatible" content="IE=9" /> <script src="http://www.cnblogs.com/../src/wind-core.js"></script> <script src="http://www.cnblogs.com/../src/wind-compiler.js"></script> <script src="http://www.cnblogs.com/../src/wind-builderbase.js"></script> <script src="http://www.cnblogs.com/../src/wind-async.js"></script> <script> var drawHand = function(value, length) { ctx.beginPath(); var angle = Math.PI * 2 * value / 60; var x = Math.sin(angle) * length; var y = Math.cos(angle) * length; ctx.moveTo(100, 100); ctx.lineTo(100 + x, 100 - y); ctx.stroke(); } var drawClock = function(time) { if (!ctx) { var h = time.getHours(); var m = time.getMinutes(); var s = time.getSeconds(); var text = ((h >= 10) ? h : "0" + h) + ":" + ((h >= 10) ? m : "0" + m) + ":" + ((h >= 10) ? s : "0" + s); document.getElementById("clockText").innerHTML = text; return; } ctx.clearRect(0, 0, 200, 200); ctx.beginPath(); ctx.arc(100, 100, 90, 0, Math.PI * 2, false); for (var i = 0; i < 60; i += 5) { var angle = Math.PI * 2 * i / 60; var x = Math.sin(angle); var y = Math.cos(angle); ctx.moveTo(100 + x * 85, 100 - y * 85); ctx.lineTo(100 + x * 90, 100 - y * 90); } ctx.stroke(); drawHand(time.getSeconds(), 80); drawHand(time.getMinutes() + time.getSeconds() * 1.0 / 60, 60); drawHand(time.getHours() % 12 * 5 + time.getMinutes() * 1.0 / 12, 40); } var drawClockAsync = eval(Wind.compile("async", function(interval) { while (true) { drawClock(new Date()); $await(Wind.Async.sleep(interval)); } })); </script> </head> <body> <canvas id="clockCanvas" height="200" width="200"> <div id="clockText" style="font-size:20pt;"></div> </canvas> <script> var canvas = document.getElementById("clockCanvas"); var ctx = canvas.getContext ? canvas.getContext("2d") : null; drawClockAsync(1000).start(); </script> </body> </html>
我的例子:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>Clock - asThread.js Sample</title> <meta http-equiv="X-UA-Compatible" content="IE=9" /> <!-- 例子修改自wind.js --> <script src="asThread.js"></script> <script> var drawHand = function(value, length) { ctx.beginPath(); var angle = Math.PI * 2 * value / 60; var x = Math.sin(angle) * length; var y = Math.cos(angle) * length; ctx.moveTo(100, 100); ctx.lineTo(100 + x, 100 - y); ctx.stroke(); } var drawClock = function() { var time = new Date() if (!ctx) { var h = time.getHours(); var m = time.getMinutes(); var s = time.getSeconds(); var text = ((h >= 10) ? h : "0" + h) + ":" + ((h >= 10) ? m : "0" + m) + ":" + ((h >= 10) ? s : "0" + s); document.getElementById("clockText").innerHTML = text; return; } ctx.clearRect(0, 0, 200, 200); ctx.beginPath(); ctx.arc(100, 100, 90, 0, Math.PI * 2, false); for (var i = 0; i < 60; i += 5) { var angle = Math.PI * 2 * i / 60; var x = Math.sin(angle); var y = Math.cos(angle); ctx.moveTo(100 + x * 85, 100 - y * 85); ctx.lineTo(100 + x * 90, 100 - y * 90); } ctx.stroke(); drawHand(time.getSeconds(), 80); drawHand(time.getMinutes() + time.getSeconds() * 1.0 / 60, 60); drawHand(time.getHours() % 12 * 5 + time.getMinutes() * 1.0 / 12, 40); } Thread(). // 使用主线程线程 loop(-1). // 负数表示循环无限多次,如果是正数n,则表示n次循环 then(drawClock). // 循环中运行drawClock wait(1000). // 然后等待1000ms loopEnd(); // 循环结束 // 线程定义结束 </script> </head> <body> <canvas id="clockCanvas" height="200" width="200"> <div id="clockText" style="font-size:20pt;"></div> </canvas> <script> var canvas = document.getElementById("clockCanvas"); var ctx = canvas.getContext ? canvas.getContext("2d") : null; Thread().run(); // 运行线程 </script> </body> </html>
Something more?
- 将事件当成分支处理
我们提供了on方法将事件转成分支来执行。
举个例子页面有个按钮“点我”,但是我们希望打开页面5秒内单击没有效,5秒后显示“请点击按钮”后,单击才会出现“你成功点击了”。
使用on分支是这样的:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>on - asThread.js Sample</title> <meta http-equiv="X-UA-Compatible" content="IE=9" /> <script src="asThread.js"></script> </head> <body> <button id = "b">点我</button> <script> var ele = document.getElementById("b"); Thread(). // 获得线程 then(function(){alert("请点击按钮")}, 5000). //然后等5秒显示"请点击按钮" on(ele, "click"). // 事件分支On开始,如果ele触发了click事件 then(function(){alert("你成功点击了")}). //那么执行你成功点击了 onEnd(). // 事件分支On结束 then(function(){alert("都说可以的了")}). // 然后弹出"都说可以的了" run(); //启动线程 </script> </body> </html>自定义事件也可以哦,只要在.on时候传进去注册监听函数,和删除监听函数就行了。比如:
function addEvent(__elem, __type, __handler){ //添加监听 } function removeEvent(__elem, __type, __handler){ //删除监听 } Thread(). on(ele, "success", addEvent, removeEvent). then(function(){alert("成功!")}). onEnd(). run();当然实际上我们还可以注册多个事件分支。事件分支是并列的,也就是平级的事件分支没有现有顺序,所以我们能这样:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>on - asThread.js Sample</title> <meta http-equiv="X-UA-Compatible" content="IE=9" /> <script src="asThread.js"></script> </head> <body> <button id = "b">点我</button> <button id = "c">点我</button> <script> var ele0 = document.getElementById("b"), ele1 = document.getElementById("c"); Thread(). // 获得线程 then(function(){alert("请点击按钮")}, 5000). //然后等5秒显示"请点击按钮" on(ele0, "click"). // 事件分支On开始,如果ele0触发了click事件 then(function(){alert("你成功点击了")}). //那么执行你成功点击了 on(ele1, "click"). // 事件分支On开始,如果ele1触发了click事件 then(function(){alert("你成功点击了")}). //那么执行你成功点击了 onEnd(). // 事件分支On结束 then(function(){alert("都说可以的了")}). // 然后弹出"都说可以的了" run(); //启动线程 </script> </body> </html>
- 开辟多个线程
一个线程不够用?只要输入名字就能开辟或者得到线程了。
系统会自动初始化一个主线程,当不传参数时就直接返回主线程:
Thread() //得到主线程但如果主线程正在用想开辟一个线程时,只要给个名字就行,比如:
Thread("hello") //得到名字是hello的线程那么下次再想用该线程时只要输入相同的名字就行了:
Thread("hello") //得到hello线程默认只最多只提供10个线程,所以用完记得删掉:
Thread("hello").del();
- setImmediate
IE10已经提供了setImmediate方法,而其他现代浏览器也可以模拟该方法,其原理是推倒线程末端,使得浏览器画面能渲染,得到比setTimeout(0)更快的响应。
我们通过接口.imm来提供这一功能。比如:
Thread(). imm(function(){alert("hello world")}). run();这方法和.then(fn)不太一样,.then(fn)是可能阻塞当前浏览器线程的,但.imm(fn)是将处理推到浏览器引擎列队末端,排到队了在运行。
所以如果你使用多个Thread(伪多线程),而又希望确保线程是并行运行的,那么请使用.imm来替代.then。
当然对于老版IE,只能用setTimeout(0)替代了。
- 分支参数可以是函数
分支Right传的参数如果只是布尔值肯定很不爽,因为这意味着分支是静态的,在初始化时候就决定了,但我们希望分支能在执行到的时候再判断是走Right还是Left,所以我们提供了传参可以是函数(但是函数返回值需要是布尔值,否则……╮(╯▽╰)╭也会转成布尔值的……哈哈)。比如:
fucntion foo(boolean){ return !boolean; } Thread(). define(true). right(foo). then(function(){/*这里不会运行到*/}). rightEnd(). run();
Enjoy yourself!!
项目地址
https://github.com/miniflycn/asThread