中关村村草 发表于 2012-01-19 21:10

关于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啦。

如果有一天21 发表于 2012-01-19 22:25

谢谢分享
页: [1]
查看完整版本: 关于Web Worker应用的一个想法和实现