这篇文章以前在CU上,那边人气不够,重新发到这里。嘿嘿
公司的产品需要在后端维护着一次会话的状态,而当用户关闭浏览器的时候,需要及时释放资源,这以前是通过浏览器window的load和unload事件分别触发CancelCloseAction和StartCloseAction来实现的。后来发现在有些情况下,比如“杀浏览器进程”,“网络异常”等时候,后台就无法确定浏览器的状态了,以至于不能及时释放资源。所以在另一个产品里,我们就使用了“心跳”监测技术,也就是前端定时(比如15~30秒),向后端发一个心跳,如果后端在特定时间里收不到心跳,就认为浏览器关闭了,此时可以回收该会话的所有资源。一般情况下,这个机制是可以工作的,但某些case,前端Javascript的运算量可能会非常大,这时心跳信号往往被延后,以至于时有session过期的情形出现。每每此时,我们多希望Javascript是多线程的,那该多好啊,可以专门开一个线程来发心跳。
1、封装Web Worker的设想
看了网络上无数的抄来转去,实则内容同样的关于Web Worker的介绍后,我们已经知道,Web Worker的确是真正意义上的多线程了。但看着如此简单的postMessage和onmessage的例子,我实在难以确定该如何使用它,又如何用它来实现想象中的“心跳”线程呢。
另外浏览器兼容问题,IE,万恶的IE依旧特立独行地不支持Web Worker,怎么办? 好消息是从IE8开始,IE开始支持frame间通过postMessage和onmessage来通信了, 这至少可以为模拟一个Web Worker在API层面上提供了一个可能。
Java的java.lang.Thread已经是一个非常好的线程应用模型了。如果把Web Worker包装成一个java.lang.Thread会怎么样呢?我想信,至少对于Java程序员来说估计是很好用的。比如:
// 1、定义计算任务,可以理解成java.lang.Runnable var task = { context: { x:1 }, // 需要计算的东西 run: function(){ // 对context进行计算的方法 this.context.x++; // 如有需要可以向外post消息 } }; // 2、创建线程对象 var myThread = new js.lang.Thread(task); // 3、为线程对象绑定onmessage事件 myThead.onmessage = function(e){ // 处理线程运行中发出来的消息 // 如有必要终止线程this.stop(); }; // 4、启动线程 myThread.start(); // 5、如有需要可以向线程提交另一个计算任务 myThread.submitTask(task);
从以上js.lang.Thread的设想来看,包装后的Web Worker应该是非常容易用来进行多线程计算的,Thread提供的是计算能力,而数据和计算方法由调用者来决定。
2、Web Worker(Iframe Worker)应该如何工作
在第1步的设想下,Web Worker将被封装在js.lang.Thread里,那么task,即Runnable,需要发送给Worker,而Worker的代码看起来,应该会象这样:
// onmessage的实际句柄 var _onmessage = function(e){ /* * 我们期望e.data拿到的是如下一样的从var thi$ ...到 }的一个string * * var thi$ = { * * context: {}, // 计算对象 * * run : function(){ * // 计算方法 * } * } */ eval(e.data); // 非常关键 // 此时,我们有了一个thi$对象,执行计算 thi$.run(); //或者thi$.run.call(thi$); };
3、一个实验性的js.lang.Thread实现
Java程序员或着
看过我以前文章的人,基本上都可以看懂下面Thread的实现。当然有一些如J$VM,js.lang.Class之类的和我的一个开源项目有关,有兴趣的可以到
Google code
看一下。
/** * The <code>Thread</code> for easily using Web Worker, and for the * IE8/9 use a iframe simulate Web Worker * * Runnable :{ * context: xxx, * run : function * } * */ js.lang.Thread = function(Runnable){ var worker, runnable; var _onmessage = function(e){ var evt = e.getData(), data; if(evt.source == self) return; try{ data = JSON.parse(evt.data); } catch (x) { data = evt.data; } if(js.lang.Class.typeOf(data) == "array"){ // [msgId, msgData, [recvs], null, pri] switch(data[0]){ case "console_inf": J$VM.System.out.println(data[1]); break; case "console_err": J$VM.System.err.println(data[1]); break; case "console_log": J$VM.System.log.println(data[1]); break; default: var fun = "on"+data[0]; if(typeof this[fun] == "function"){ this[fun](data[1]); } } }else{ if(typeof this.onmessage == "function"){ this.onmessage(data); } } }; var _onerror = function(e){ var evt = e.getData(), data; if(evt.source == self) return; J$VM.System.err.println(evt.data); }; /** * Submit new task to the thread * * @param task It should be a <code>Runnable</code> or a * <code>context</code> in <code>Runnable</code> * @param isRunnable indicates whether the first parameter "task" * is a <code>Runnable</code> */ this.submitTask = function(task, isRunnable){ if(task == undefined || task == null) return; isRunnable = isRunnable || false; var context, run; if(isRunnable){ context = task.context; run = task.run; }else{ context = task; run = runnable.run; } var buf = new js.lang.StringBuffer(); buf.append("var thi$ = {"); buf.append("context:").append(JSON.stringify(context)); buf.append(",run:").append(run); buf.append("}"); var msg = buf.toString(); //J$VM.System.err.println("Thread post msg: "+msg); if(self.Worker){ worker.postMessage(msg); }else{ // IE must worker.postMessage(msg, "*"); } }; /** * Start the thread */ this.start = function(){ this.submitTask(runnable, true); }; /** * Stop the thread */ this.stop = function(){ if(worker.terminate){ worker.terminate(); }else{ // For the iframe worker, what should be we do ? } }; var _init = function(Runnable){ runnable = Runnable || { context:{}, run:function(){ J$VM.System.out.println("Web Worker is running"); }}; var E = js.util.Event; var path = J$VM.env["j$vm_home"]+"/classes/js/util/"; if(self.Worker){ worker = new Worker(path+"Worker.js"); }else{ // iframe ? var iframe = document.createElement("iframe"); iframe.style.cssText = "visibility:hidden;border:0;width:0;height:0;"; document.body.appendChild(iframe); var text = "<html><head>" + "<meta http-equiv='X-UA-Compatible' content='IE=edge'>" + "<meta http-equiv='Content-Type' content='text/html; charset=UTF-8'>"+ "</head></html>"; var doc = iframe.contentDocument, head, script; doc.open(); doc.write(text); doc.close(); head = doc.getElementsByTagName("head")[0]; text = js.lang.Class.getResource(J$VM.env["j$vm_home"]+"/jsre.js"); script = doc.createElement("script"); script.type = "text/javascript"; script.id = "j$vm"; // Becase we don't use src to load the iframe, but the J$VM will use // "src" attribute to indicates j$vm home. So we use a special attribute // name "crs" at here. @see also js.lang.System#_buildEnv script.setAttribute("crs",J$VM.env["j$vm_home"]+"/jsre.js"); script.setAttribute("classpath",""); script.text = text; text = js.lang.Class.getResource(path + "Worker.js"); script.text += text; head.appendChild(script); head.removeChild(script); worker = iframe.contentWindow; } E.attachEvent(worker, "message", 0, this, _onmessage); E.attachEvent(worker, "error", 0, this, _onerror); }; _init.$bind(this)(Runnable); }.$extend(js.lang.Object);
4、Worker.js的实现
Worker.js是Web Worker或IFrame Worker需要加载来处理onmessage的,在我的实现中,它并不是象js.lang.Thread一样,是一个通常意义的的类,而是简单的一个javascript文件,只是被管理在js.util的这个目录下:
-
var isWebWorker = function(){ try{return (window) ? false : true;} catch (x) {return true;} }(); var _onmessage = function(e){ if(isWebWorker){ eval(e.data); }else{ var _e = e.getData(); if(_e.source == self) return; eval(_e.data); } thi$.run.call(thi$); }; if(isWebWorker){ importScripts("../../../jsre.js"); onmessage = _onmessage; }else{ js.util.Event.attachEvent(window, "message", 0, this, _onmessage); }
5、结论
以上代码,已经发布到Google Code上,对IE8+, Firefox, Chrome浏览器进行过测试,工作正常。
以后在Javascript里,终于可以象在Java里一样使用多线程计算技术了,当然Web Worker本身的限制是不可避免的,比如不能访问DOM啦。