JS学习笔记07-线程
本文主要记录运行环境和特别函数以及web worker:
- 运行环境
- 特别函数
- web worker
一、运行环境
JavaScript只在主线程上运行。也就是说JavaScript同时只能执行一个同步任务,其他同步任务都必须在后面排队等待。
为了利用多核CPU的计算能力,HTML5 提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以这个新标准并没有改变JavaScript单线程的本质。
JavaScript运行时除了一个正在运行的主线程,引擎根据异步任务的类型会存在多个任务队列(里面是各种需要当前程序处理的异步任务)。JavaScript会不停地检查,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程。这种循环检查的机制叫事件循环 (Event Loop)。
首先,主线程会去执行所有的同步任务,一直等到同步任务全部执行完。
其次,去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完后下一个异步任务再进入主线程开始执行。
最后,一旦任务队列清空,程序就结束执行。
1.1 异步操作的设计方法
-
回调函数是异步操作最基本的方法
function func1(callback) { // todo callback(); } function func2() { // todo } func1(func2); -
发布/订阅是异步操作很常见的方法
// func1进行如下改写 function f1() { setTimeout(function () { // todo jQuery.publish('done'); }, 1000); } // func2向信号中心jQuery订阅done信号。 jQuery.subscribe('done', func2);
1.2 异步操作的流程控制
如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。
-
串行执行
可以编写一个流程控制函数,让它来控制异步任务。一个任务完成以后,再执行另一个,最后执行final函数。这就叫串行执行。缺点是执行时间很长。
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } // 串行函数,它会依次执行异步任务 function exec(item) { if(item) { async(item, function(result) { results.push(result); return series(items.shift()); }); } else { return final(results[results.length - 1]); } } exec(items.shift()); -
并行执行
可以编写一个流程控制函数,让它来控制异步任务。所有异步任务同时执行,等到全部完成以后,最后执行final函数。这就叫并行执行。缺点是如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。
var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } // 并行函数,会同时发起六个异步任务 function exec(arrs){ arrs.forEach(function(item) { async(item, function(result){ results.push(result); if(results.length === items.length) { final(results[results.length - 1]); } }) }); } exec(items.shift()); -
混合执行
并行与串行的结合就是设置一个门槛,每次最多只能并行执行
n个异步任务,这样就避免过分占用系统资源。var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; var running = 0; var limit = 2;// 通过调节limit变量,达到效率和资源的最佳平衡 function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } function exec() { while(running < limit && items.length > 0) { var item = items.shift(); async(item, function(result) { results.push(result); running--; if(items.length > 0) { exec(); } else if(running == 0) { final(results); } }); running++; } } exec();
以上就是有多个异步操作下流程控制的设计方案。
二、特别函数
2.1 Promise
Promise对象是JavaScript的异步操作解决方案(包含设计方法和流程控制),为异步操作提供统一接口。它可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
Promise原本只是社区提出的一个构想,目前JavaScript原生支持Promise对象。
Promise接受一个回调函数作参数,该函数的两个参数resolve和reject是函数,由JavaScript引擎提供。ES6出现了generator以及async/await语法,也是基于Promise实现的。
Promise实例的回调函数属于微型异步任务,会在同步任务之后且常规异步任务之前执行。正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着微任务的执行时间一定早于正常任务。
Promise原型提供了如下的方法:
all方法会并发保证运行多个Promise实例,全部运行完成后返回一个新的Promise实例。成功时返回的是个数组对象,失败时返回最先被reject失败状态的值。race方法会并发赛跑运行多个Promise实例,最先运行完成后返回一个新的Promise实例。成功和失败返回的均是最先执行完的对应的状态值。then方法用于指定当前Promise实例状态发生改变时的回调函数,然后返回一个新的Promise实例。catch方法是then(null, failureCallback)的缩略形式。finally方法指定不管Promise实例最后状态如何都会执行的操作。
2.2 定时器
setTimeout函数用来指定某个函数在固定时间之后执行,然后结束。返回一个整数,表示定时器的编号。使用clearTimeout进行清除。
setInterval函数用来指定某个函数在固定时间之后执行,循环有效。返回一个整数,表示定时器的编号。使用clearInterval进行清除。
setTimeout和setInterval的机制是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了就执行对应的代码,如果不到就继续等待。这意味着setTimeout和setInterval指定的回调函数必须等到本轮事件循环的所有同步任务都执行完后才会开始执行。由于前面的任务到底需要多少时间执行完是不确定的,所以没有办法保证两者的定时时间是绝对的。因JS是单线程,如果主线程存在时间超过定时时间的任务,那只有在当前主线程所有同步任务执行完成后,此时才会将任务函数提到主线程上,以用来运行当前的定时任务函数。
两者的语法用法完全一致:
参数:定时函数接受多个参数,第一个参数
func是将要推迟执行的函数名,第二个参数delay是推迟执行的毫秒数(如果省略,则默认为0),后续的参数是func的参数数值。this指向:定时函数使得回掉函数内部的
this关键字指向全局环境,而不是定义时所在的那个对象。
应用:debounce函数(防抖动函数)
有时不希望回调函数被频繁调用。如用户填入网页输入框的内容,希望通过Ajax方法传回服务器,jQuery 的写法如下:
$('textarea').on('keydown', ajaxAction);
这样写有一个很大的缺点,就是如果用户连续击键就会连续触发keydown事件,造成大量的Ajax通信。正确的做法应该是设置一个门槛值,表示两次Ajax通信的最小间隔时间。如果在间隔时间内,发生新的keydown事件则不触发 Ajax 通信,并且重新开始计时。如果过了指定时间没有发生新的keydown事件,再将数据发送出去。
$('textarea').on('keydown', debounce(ajaxAction, 2500));
function debounce(fn, delay){
var timer = null; // 声明计时器
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
三、Web Worker
Web Worker的作用,就是为JS创造多线程环境。在主线程运行的同时,Worker线程在后台运行,Worker线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。等到 Worker线程完成计算任务,再把结果返回给主线程。
Web Worker有以下几个使用注意点:
- 对象限制:无法使用
document、window、parent这些对象以及各种DOM。但是Worker线程可以使用navigator对象和location对象。Worker的全局对象WorkerGlobalScope,不同于网页的全局对象Window,它很多接口拿不到。 - 文件限制:Worker 线程无法读取本地文件,即不能打开本机的文件系统(
file://),它所加载的脚本,必须来自网络,并且分配给Worker线程运行的脚本文件,必须与主线程的脚本文件同源。
3.1 主线程基本用法
-
主线程采用
new命令,调用Worker()构造函数,新建一个 Worker 线程。var myWorker = new Worker('worker.js', { name : 'myWorker' });Worker()构造函数的第一个参数(必须的)是一个脚本文件,该文件就是Worker线程所要执行的任务。由于Worker不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker就会失败。Worker()构造函数的第二个参数(可选的)是配置对象。它的作用是指定Worker线程的名称。 -
主线程调用
worker.postMessage()方法,向Worker发送消息。worker.postMessage('Hello World'); worker.postMessage({method: 'echo', args: ['Work']});postMessage参数可以是各种数据类型,包括二进制数据。
-
主线程通过
worker.onmessage方法,接收Worker的消息。通过worker.onerror方法,接收Worker的异常。function doAction(data) { // todo } worker.onmessage = function (event) { doAction(event.data); } worker.onerror(function (event) { console.log([ 'ERROR: Line ', event.lineno, ' in ', event.filename, ': ', event.message ].join('')); });事件对象的
data属性可以获取Worker发来的数据。 -
主线程在Worker完成任务以后就可以把它关掉。
worker.terminate();
3.2 子线程基本用法
self.name属性为Worker的名字。它由构造函数指定。
self.importScripts()用于Worker内部加载JS脚本。
self.addEventListener()指定监听函数。监听函数的参数是个事件对象,它的data属性为发来的数据。
self.postMessage()方法用于子线程向主线程发送消息。
self.close()方法用于在Worker内部关闭自身。var self = this; self.importScripts('script1.js', 'script2.js'); // 用于监听消息 self.addEventListener('message', function (e) { var data = e.data; switch (data.cmd) { case 'start': self.postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': self.postMessage('WORKER STOPPED: ' + data.msg); self.close(); // Terminates the worker. break; default: self.postMessage('Unknown command: ' + data.msg); }; }, false); // 用于监听错误 worker.addEventListener('error', function (event) { // todo });
3.3 通信注意事项
正常情况下浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。但是拷贝方式发送二进制数据,会造成性能问题。如主线程向Worker发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。这种方法不会直接转移数据的控制权。
为了解决这个问题JS使用一种转移数据的方法Transferable Objects。JavaScript允许主线程把二进制数据直接转移给子线程,但是一旦转移后主线程就无法再使用这些二进制数据,这是为了防止出现多个线程同时修改数据的麻烦局面(本质就是值拷贝而不是地址拷贝)。这使得主线程可以快速把数据交给Worker,对于影像处理、声音处理、3D 运算等就非常方便,不会产生性能负担。这种方法会直接转移数据的控制权。
// 正常发送消息
worker.postMessage('Hello World');
// 特殊发送消息原型为worker.postMessage(arrayBuffer, [arrayBuffer]);
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
3.4 创建注意事项
通常情况下,Worker载入的是一个单独的JavaScript脚本文件,但是也可以载入与主线程在同一个网页的代码。
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
</body>
</html>
上面是一段嵌入网页的脚本,注意必须指定script标签的type属性是一个浏览器不认识的值(上例是app/worker)。然后读取这一段嵌入页面的脚本,用Worker来处理。
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.onmessage = function (e) {
// e.data === 'some message'
};
上面代码中先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成URL,再让Worker加载这个 URL。这样就做到了主线程和Worker的代码都在同一个网页上面。

浙公网安备 33010602011771号