关于Web Worker应用的一个想法和实现
关于Web Worker应用的一个想法和实现公司的产品需要在后端维护着一次会话的状态,而当用户关闭浏览器的时候,需要及时释放资源,这以前是通过浏览器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程序员来说估计是很好用的。比如:
Js代码// 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);
// 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的代码看起来,应该会象这样:
Js代码
// 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$);
};
// 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看一下。
Js代码/**
* 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"){
// , null, pri]
switch(data){
case "console_inf":
J$VM.System.out.println(data);
break;
case "console_err":
J$VM.System.err.println(data);
break;
case "console_log":
J$VM.System.log.println(data);
break;
default:
var fun = "on"+data;
if(typeof this == "function"){
this(data);
}
}
}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");
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);
/**
* 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"){
// , null, pri]
switch(data){
case "console_inf":
J$VM.System.out.println(data);
break;
case "console_err":
J$VM.System.err.println(data);
break;
case "console_log":
J$VM.System.log.println(data);
break;
default:
var fun = "on"+data;
if(typeof this == "function"){
this(data);
}
}
}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");
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的这个目录下:
Js代码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);
}
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啦。
谢谢分享
页:
[1]