Web Worker介绍、js调试及浏览器兼容实现
这几天主要围绕webworker的实现来了。所以也下了点功夫,主要是对webworker的介绍/实现原理/调试几个方面进行了不算深入的了解,并最终成功封装了自己的跨浏览器webworker模块。虽然还比较粗糙,但是已经具备了基本功能,可以让使用者不用关心浏览器的兼容问题,不妨试一下。下面就我最近对webworker的理解做一下梳理。
一、Web Worker的由来
众所周知,javascript的运行都是在单线程中完成的,没有多线程的概念,顶多也就是一个异步加载和setTimeout或者setInterval的使用来模拟。至于具体浏览器内部是如何运作的可以看看jQuery作者John Resig对浏览器的运行时工作的分析文章,具体请移步到http://ejohn.org/blog/how-javascript-timers-work/观摩学习。那重点是今后有没有一个开辟多线程工作的可能呢?比如将一些复杂计算比如3D或者数据处理放到一个线程,webUI和事件的处理放在一个线程,更或者其他的监控放一个线程等等。HTML5肩负救世主的使命,理所应当的必须完成这个光荣的任务,为我们带来一线生机–Web Worker。2008年提出HTML5草案开始到现在其差不多已经定型到能够在独立的运行时环境(不同于页面主线程运行时)中运行,并且通过消息机制完成主线程了worker线程之间的数据通信。
二、Web Worker介绍
Web Worker的创建是在主线程当中通过传入文件的url来实现的。如下所示:
var webworker=new Worker('myWebWorkerFile.js');
返回的webworker对象,该对象可以完成与worker线程的通信,主要通过webworker.postMessage()向线程发送消息,通过webworker.onmessage=function(evt){}来监听worker线程发送到主线程的消息,通过webworker.onerror=function(evt){}来监听线程中的错误消息,通过webworker.terminate()来告诉worker线程立即终止执行。由于安全性等方面的考虑,用于创建webworker线程的js文件必须和当前页面遵循同源策略,也就是说不能跨域请求js文件;另外,线程之间的消息传递是不能有Function对象的,就是说不能够传递函数,在safari浏览器中只能传递数字/字符串等基本值(连Object都不行)。并且所有线程之间的数据并不是常规的引用方式,都是一份独立的拷贝。因此一般情况下,我们会将传递的参数转化为字符串进行传递,可以通过JSON.stringify()和JSON.parse()来处理。
那么在myWebWorkerFile.js文件当中的js代码如何完成与主线程的通信呢?其实一样简单,通过this.postMessage()和this.onmessage=function(evt){}来完成的。其实这个不是重点,重点是在myWebWorker当中的运行时环境是怎样的?又有什么API或者对象可以操作呢?
上文已经提到,Web Worker线程的运行时环境是独立于主线程的运行时环境的,所以线程中是无法访问主线程的对象的。线程中的全局宿主对象是DedicatedWorkerGlobalScope。作用上下文的主要常见对象如下:
Infinity: Infinity Array: function Array() { [native code] } ArrayBuffer: function ArrayBuffer() { [native code] } Blob: function Blob() { [native code] } Boolean: function Boolean() { [native code] } DataView: function DataView() { [native code] } Date: function Date() { [native code] } Error: function Error() { [native code] } EvalError: function EvalError() { [native code] } EventSource: function EventSource() { [native code] } FileError: function FileError() { [native code] } FileException: function FileException() { [native code] } FileReader: function FileReader() { [native code] } FileReaderSync: function FileReaderSync() { [native code] } Float32Array: function Float32Array() { [native code] } Float64Array: function Float64Array() { [native code] } Function: function Function() { [native code] } JSON: JSON Math: MathConstructor MessageChannel: function MessageChannel() { [native code] } MessageEvent: function MessageEvent() { [native code] } NaN: NaN Number: function Number() { [native code] } Object: function Object() { [native code] } RangeError: function RangeError() { [native code] } ReferenceError: function ReferenceError() { [native code] } RegExp: function RegExp() { [native code] } String: function String() { [native code] } SyntaxError: function SyntaxError() { [native code] } TEMPORARY: 0 TypeError: function TypeError() { [native code] } URIError: function URIError() { [native code] } URL: function URL() { [native code] } WebSocket: function WebSocket() { [native code] } WorkerLocation: function WorkerLocation() { [native code] } XMLHttpRequest: function XMLHttpRequest() { [native code] } addEventListener: function addEventListener() { [native code] } clearInterval: function clearInterval() { [native code] } clearTimeout: function clearTimeout() { [native code] } close: function close() { [native code] } constructor: function DedicatedWorkerContext() { [native code] } decodeURI: function decodeURI() { [native code] } decodeURIComponent: function decodeURIComponent() { [native code] } dispatchEvent: function dispatchEvent() { [native code] } encodeURI: function encodeURI() { [native code] } encodeURIComponent: function encodeURIComponent() { [native code] } escape: function escape() { [native code] } eval: function eval() { [native code] } hasOwnProperty: function hasOwnProperty() { [native code] } importScripts: function importScripts() { [native code] } isFinite: function isFinite() { [native code] } isNaN: function isNaN() { [native code] } isPrototypeOf: function isPrototypeOf() { [native code] } location: WorkerLocation navigator: WorkerNavigator onerror: null onmessage: null parseFloat: function parseFloat() { [native code] } parseInt: function parseInt() { [native code] } postMessage: function postMessage() { [native code] } propertyIsEnumerable: function propertyIsEnumerable() { [native code] } removeEventListener: function removeEventListener() { [native code] } self: DedicatedWorkerContext setInterval: function setInterval() { [native code] } setTimeout: function setTimeout() { [native code] } toLocaleString: function toLocaleString() { [native code] } toString: function toString() { [native code] } undefined: undefined unescape: function unescape() { [native code] } valueOf: function valueOf() { [native code] }
从上面可以看出在线程的作用域当中是不能访问DOM和BOM对象的,只是增加了Location和navigator的只读访问,并且navigator封装成了WorkerNavigator对象,更改部分属性。另外可以发现self成了相当于this的全局引用,并且能够正常使用定时器和XMLHttpRequest异步操作,还可以通过importScripts()方法来异步加载js文件,以及可以通过在线程中调用close()方法终止线程。所以除了缺失了DOM操作能力以外,还是拥有非常强大的js逻辑运算处理的能力的,相当于nodejs一样的运行环境了(不是很恰当)。所以从这个角度来看,Web Worker承担的分工就基本明确了采用新线程的目地是是处理一些比较耗时的后台运算,防止主线程的UI阻塞。
另外,Web Worker还支持共享线程,达到它允许同域中的多个应用程序使用同一个提供公共服务的共享线程。
三、Web Worker调试
由于本身标准都还是草案,W3C也是在不断的完善标准的制定。所以支持的浏览器在某些不明确的标准下实现的方式也不一样,这将为我们的调试带来不便。由于Web Worker线程是无法操作DOM的,所以就没办法采用alert/console等输出信息,并且目前大多数浏览器(目前chrome已支持)都还无法提供类似常规js脚本调试的工具来调试webworker线程的js文件,所以很有必要对如何调试线程代码展开讨论。
其实很容易就能想到的是通过将调试请求发送到主线程,主线程通过接受worker线程发送的日志输出请求进行打印log。呵呵,这和Nicholas C. Zakas几年前的想法不谋而合。。。他在文章当中也说明了此方法的使用方式。主要实现的代码如下:
//主线程 var worker = new Worker("worker.js"); worker.onmessage = function(event){ switch (event.data.type){ case "debug": console.log(event.data.message); break; //other types of data } }; worker.postMessage({ type: "start", value: 12345 }); //web worker.js self.onmessage = function(event){ if (event.data.type == "start"){ process(event.data.value); } }; function process(number){ self.postMessage({ type: "debug", message: "Starting processing..." }); //code self.postMessage({ type: "debug", message: "Processing finished" }); }
值得庆幸的是,目前chrome浏览器的debug工具已经支持调试Web Worker线程中的代码了。
具体操作方法,就是在script对应的tab页的右下角勾选Worker的调试选项,如图所示:
当你重新刷新页面的时候,会弹出一个如下图所示的新的调试窗口,类似于主线程的调试窗口,所以大家可以很熟练的操作了,感谢google,感谢chromium。
四、Web Worker的浏览器兼容实现
至此,已经有两种方式可以调试了,所以还算不错的。那么我们如何解决不支持Web Worker的浏览器实现呢?呵呵从文章开头其实已经暗示过了,也只能通过setTimeout这个定时器来将线程内容载入队列,当浏览器空闲时便执行相关代码内容。那么,这种实现方式在执行代码时仍然会阻塞主线程影响用户体验。可那又怎么办呢?这不是没办法的办法么?既然定了,那也要保证我们开发者解放出来啊,至少不用针对浏览器的支持情况去写两份不一样的代码吧?那这样岂不是很浪费。到此,我的Web Worker模块的意义诞生了,就是要解放开发者,让他写一份代码,在不同浏览器当中均能正常运行,从而不用考虑兼容性问题。当然如果要方便,那就必须在一定的规则下来玩。也就是说我会给开发者封装统一的API来调用。从上面的分析来看,最主要的就是线程之间的通信API了。
对于线程内执行的代码我提供了全局this.innerAttach(eventName, handleFunction)来监听主线程发来的消息,提供this.innerNotify(eventName, data)来发送消息到主线程;对于主线程提供了WebWorker对象attach/notify方法来监听worker线程的消息以及发送消息给worker线程。具体代码如下,大家也可以上github查看我的Web Worker模块,目前还比较粗糙,只是简单的功能实现,线程共享之类还未考虑。
/** *@fileoverview the webworker plugin for 模拟webworker实现 *@author ginano *@website http://www.ginano.net *@date 20130228 */ //当作为普通页面脚本环境执行时 if('function' === typeof define){ define('modules/webworker',[ 'modules/config', 'modules/class', 'modules/util', 'modules/notify' ],function(Config,Class,Util,Notify){ //为了更容易实现,必须要同时支持JSON并且文件不跨域访问(由于目前的webworker文件是固定的,所以要保证和本文件的作用域相同) var isSupport='function'===typeof window['Worker'] && Config.host==location.host && JSON, workerLength=0; var WebWorker=new Class('modules/webworker',{ /** *初始化 *@method init */ public__init:function(modulename,factory){ //主体部分 this.MainNotify=Notify.create(); //webworker线程 this.ViceNotify=Notify.create(); this.workerName=modulename; this.factory=factory; this.buildWorker(); }, /** *新建一个webworker的构造器 */ public__buildWorker:function(){ var self=this; //如果原生支持 if(isSupport){ this._worker=new Worker(location.protocol+'//'+Config.host+Config.rootPath+'modules/js/webworker.js'); //增加调试功能 this.MainNotify.attach('loginfo',function(info){ Util.log(info); }); this._worker.onmessage=function(evt){ var _data=JSON.parse(evt.data); self.MainNotify.notify(_data.eventType,_data.data); }; this._worker.onerror=function(evt){ Util.log(JSON.stringify(evt)); }; this._worker.postMessage(self.factory.toString()); }else{ this._worker=setTimeout(function(){ self.factory.apply(self); },0); } }, public__terminate:function(){ if(isSupport){ this._worker.terminate(); }else{ clearTimeout(this._worker); } }, //区分用于线程的接口 public__innerNotify:function(eventName,data){ var self=this; self.MainNotify.notify(eventName,data); }, //用于线程的接口 public__innerAttach:function(eventName,fun){ this.ViceNotify.attach(eventName,fun); }, /** *接受消息 */ public__attach:function(eventName,fun){ this.MainNotify.attach(eventName,fun); }, /** *向线程发送消息 */ public__notify:function(eventName,data){ var self=this, args=arguments; if(isSupport){ this._worker.postMessage(JSON.stringify({ eventType:eventName, data:data })); }else{ if(self.notifyed){ self.ViceNotify.notify(eventName,data); }else{ setTimeout(function(){ self.notifyed=true; self.ViceNotify.notify(eventName,data); },0); } } }, log:function(info){ Util.log(info); } }); return WebWorker; }); }else{ //以WebWorker的形式加载本文将时 (function(){ var self=this, factory, notify; notify={ events:{}, innerAttach:function(eventName,fun){ this.events[eventName]=fun; }, innerNotify:function(eventName,data){ if(this.events[eventName]){ this.events[eventName].call(null,data); } } }; //this=window //肯定是由系统最先触发来执行初始化工厂 self.onmessage=function(event){ factory=Function('return '+event.data+';')(); self.onmessage=function(event){ var from=JSON.parse(event.data); notify.innerNotify(from.eventType,from.data); } //初始化执行 factory.apply(self); }; /** *向主线程发送消息 */ self.innerNotify=function(eventName,data){ self.postMessage(JSON.stringify({ eventType:eventName, data:data })); }; /** *监听主线程过来的消息 */ self.innerAttach=function(eventName,fun){ notify.innerAttach(eventName,fun); }; /** * 调试信息 * @param {Object} info */ self.log=function(info){ self.innerNotify('loginfo',info); } })(); }
那么,现在大家使用WebWorker时就不用专门写个文件来完成了,可以按照平时的模块写法来写就可以了。可以单独作为一个文件,也可以放在任意位置。如下所示例子,该例子在github上有测试页面:
define(['modules/webworker'],function(WebWorker){ var test=WebWorker.createInstance('test',function(){ var _this=this; _this.log({a:1,b:[],c:true,d:new Date()}); this.innerAttach('test',function(data){ //平方 var r=0; for(var i=0;i<=data;i++){ r+=i*0.001/2*1+2+3; _this.log(r); } _this.innerNotify('testresult', r); }); }); test.attach('testresult',function(result){ alert(result); }); test.notify('test',9); }).excute();
通过搜索貌似有人写过一个fakeworker模块来模拟的,和我这个大同小异,不过大家可以参考一下。(转自:http://www.ginano.net/html5-web-worker-introduction-debug)