目录
一,浏览器的多线程讨论
我们都知道浏览器端JavaScript是以单线程的方式执行的,也就是说JavaScript和UI渲染占用同一个主线程,那就意味着,如果JavaScript进行高负载的数据处理,UI渲染就很有可能被阻断,浏览器就会出现卡顿,也就是常说的假死
为了解决这些问题,JavaScript也是提供了异步操作,例如setTimeout,setInterval,ajax,i/o等,例如settimeout,会将计时交给计时线程中,然后继续在消息队列中依次拿任务执行,当计时线程完成后会将执行后的回调函数(任务)添加到消息队列中
就想流水线一样,如图
但是!我们又说浏览器js是单线程的,而例如setTimeout是放在计时线程中的,是不是有点自我矛盾的感觉
其实,setInterval和setTimeout并不是js的多线程,这两个函数根本上其实是事件触发函数,想证明setInterval和setTimeout不是多线程很简单,你可以试试这样一段代码
setTimeout(function() {
while(true){}
}, 0)
setTimeout(function() {
alert("foo")
}, 1000)
如何你的界面上有按钮,有动画,那么不出意外的,你的动画不懂了,按钮也无法点击了,此时浏览器就出现了假死的情况
为了解释上面的问题,我们来深入解析一下浏览器。浏览器中有三个常驻的线程,分别是JS引擎,界面渲染,事件响应。由于这三个线程同时要访问DOM树,所以为了线程安全,浏览器内部需要做互斥:
当JS引擎在执行代码的时候,界面渲染和事件响应两个线程是被暂停的。所以当JS出现死循环,浏览器无法响应点击,也无法更新界面。现在的浏览器的JS引擎都是单线程的,尽管多线程功能强大,但是线程同步比较复杂,并且危险,稍有不慎就会崩溃死锁。单线程的好处不必考虑线程同步这样的复杂问题,简单而安全
JS引擎基于事件来执行代码。事件响应线程在接到事件后,把响应的代码放到JS引擎的队列中,JS引擎按顺序执行代码。在JS引擎没有代码可以执行的时候,事件线程和渲染线程得以有机会运行,这也就解释了浏览器假死的根本原因,事件线程和渲染线程的执行时机被占用
就像上文中我的描述,setTimeout,setInterval并不是多线程,只是一个定时的事件触发器,它们在合适的时间(时间结束后)把定时器的回调事件塞到JS引擎的队列中。
举个例子,setTimeout(aFunction, 0),这行代码看似的意思是在0秒之后执行aFunction, 但这并不意味着立即执行。其它真正的意思是立刻把aFunction的代码放到当前JS引擎的队列中。所以当前代码块执行完成之前,aFunction的代码是得不到执行的。
比如这段代码,一定是world先出来,hello后出来。尽管setTimeout的参数是0,但这并不意味着立即执行
setTimeout(function() {
alert("hello");
}, 0)
alert("world")
就如上文中消息队列说所示图片一样,setTimeout会被计时“线程”接受,然后立即塞入队列中,而在被计时线程接受的过程中,world已经被执行了,hello也侍被塞到了world之后
但是,ajax有有点不一样,为什么没有听到说ajax会 阻塞页面渲染呢?
事实上异步Ajax确实用了多线程,只是Ajax请求的Http连接部分由浏览器另外开了一个线程执行,执行完毕之后给JS引擎发送一个事件,这时候异步请求的回调代码得以执行。它的执行流程是这样的
至此有没有得出什么结论
js确确实实是单线程,但是js通过使用浏览器的多线程实现了异步等操作
其实这就是一个很迷惑的问题,一直说js是单线程,但是学到浏览器的事件循环又一直有计时线程之类的,其实是借助了浏览器的多线程,但是对于js来说,自己还是一个单线程,计时任务分配出去了,但事实回调还是在,例如setTimeout依然只是一个事件调用,在一些大量计算的时候还是会导致死锁
二,浏览器web worker,真正的多线程
在HTML5中,引入了Web Worker这个概念。它能够在另外一个线程中执行计算密集的JS代码而不引起页面卡死,这是真正的多线程
通过类似定时器,回调函数等异步编程方式在平常的工作中已经足够,但是如果做复杂运算,这种方式的不足就逐渐体现出来,比如settimeout拿到的值并不正确,或者页面有复杂运算的时候很容易触发假死状态,异步代码会影响主线程的代码执行,异步终究还是单线程,不能从根本上解决问题。
多线程(Web Worker)就应运而生,它是HTML5标准的一部分,这一规范定义了一套 API,允许一段JavaScript程序运行在主线程之外的另外一个线程中。将一些任务分配给后者运行。在主线程运行的同时,Worker子线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程就不会被阻塞
1. web worker详解
web worker开发手册https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers
worker是window对象的一个方法,可以通过以下方式来检测你的浏览器是否支持worker
if (window.Worker) {……}else{alert('不支持web worker')}
创建一个新的 worker 很简单。你需要做的是调用Worker() 的构造器,指定一个脚本的 URI 来执行 worker 线程
var myWorker = new Worker('worker.js');
worker通过postMessage() 方法和onmessage事件进行数据通信。主线程和子线程是双向的,都可以发送和监听事件,举个例子
myWorker.postMessage('hello, world'); // 发送
worker.onmessage = function (event) { // 接收
console.log('Received message ' + event.data);}
postMessage所传的数据都是拷贝传递,数据子线程也是类似
当子线程运行结束后,使用完毕,为了节省系统资源,可以手动关闭子线程。如果worker没有监听消息,那么当所有任务执行完毕(包括计数器)后,它就会自动关闭
// 在主线程中关闭
worker.terminate();
// 在子线程里线程
close();
2. 使用限制
Worker子线程所在的全局对象,与主线程不在同一个上下文环境,所以无法读取 DOM 对象,也无法使用document、window、parent这些对象,global对象的指向有变更,window需要改写成self,当然也不能不能执行alert()方法和confirm()等方法,只能读取部分navigator对象内的数据
分配给Worker 线程运行的脚本文件(worker.js),必须与主线程同源。这里的同源限制包括协议、域名和端口,不支持本地地址(file://)
3.使用场景
Web Worke设计的初衷就是用来做计算耗时任务,大数据的处理,而这种计算放在worker中并不会中断前台用户的操作,避免代浏览器假死。例如处理ajax返回的大批量数据,读取用户上传文件等。worker中除了缺失了DOM和BOM操作能力以外,还是拥有非常强大的js逻辑运算处理的能力的,相当于nodejs一个级别的的运行环境