写过后台的同学一定对线程、线程池或者是多线程这些概念不会陌生,但是前台在HTML5之前很少会提及,因为在HTML5之前javascript都是单线程的。下面用一个简单的例子来说明一下单线程:
setInterval(function(){
var date = new Date();
console.log(date);
},100)
for(var i=0;i<10000000000;i++){
if(i%1000000000 == 0){
var date = new Date();
console.log("for"+date);
}
}
首先启动一个100毫秒调用一次的定时器,打印当前时间,然后再执行一个for循环,在循环里面打印当前时间,为了避免打印过多,在循环里面只有在可以整除的情况下才会打印。for循环遍历的值很大,所以整个for循环执行必然会超过100毫秒,但由于javascript的单线程特性,定时器的打印行为即使过了100毫秒也不会执行。
把上例代码保存在test2.js中,然后通过node test2.js命令来验证(可以看到for循环执行了将近一分钟才开始执行定时器的打印):
单线程会带来一个显而易见的问题,如一个函数向后台服务发送请求,而服务器可能由于各种各样的原因未响应(比如网络不通、server端IO很高或者更低级一点:server端出错了但是未进行错误处理),这样就会阻塞后续代码的执行(比如后续有渲染页面的代码,阻塞就会导致页面显示不完整),同时在等待的过程中页面会被锁定,影响用户体验。下面就给一个这样的例子:
后台java代码:
package test;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class test3
*/
@WebServlet("/test3")
public class test3 extends HttpServlet {
private static final long serialVersionUID = 1L;
/**
* Default constructor.
*/
public test3() {
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"name\":\"it is name\"}");
writer.flush();
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
}
}
在server端起一个简单的servlet,当有请求时,等待5秒(模拟后台无法立即响应的情况,方便页面观察)后返回一个json对象(key为name,value为it is name)。
前台代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="lib/jquery-3.0.0.min.js"></script>
</head>
<body>
<input type="text" id="input1" value="">
</body>
</html>
<script>
$.ajax({
type: "GET",
url: "test3",
dataType: "json",
async: false,
success: function(data){
console.log(data.name);
}
});
console.log("end");
</script>
页面显示一个输入框可供用户输入(用于验证请求阻塞时页面状态),页面初始就会向后台发起请求,在请求完成后会在控制台先打印返回json对象的值,再打印end。
访问页面,在后台未响应时(前5秒),页面不响应用户的任何操作,在本例中表现为输入框无法输入信息,控制台无信息打印,这表示打印end已经被阻塞:
等待5秒后,页面回复正常,可以正常在输入框输入信息,控制台也打印了请求的信息和end信息:
工程跑在Eclipse Java EE IDE for Web Developers.Version: Luna Service Release 2 (4.4.2) Build id: 20150219-0600下,server中间件用的是tomcat7.0,如果在本地想要运行本例,还需要下载jquery-3.0.0.min.js(前台依赖)和javax.servlet-3.0.jar(后台依赖),以下是工程的目录结构:
上例的阻塞状态显然用户是无法接受的(比如淘宝购物时查询某一个商品详细信息时卡住了,整个购物过程就被阻塞了,是不是感觉要起飞?),不过幸运的是这个问题在上古时期就有方法解决了,就是用例子中的ajax,即异步请求(上例中的阻塞是故意设置了async: false,改成了同步请求)。
把前台代码部分的async: false,这一句删除再访问页面,此时发起请求时,页面的操作并没有被阻塞,后续的打印end也没有被阻塞(先于请求响应消息打印,即打印end先执行了):
当然,单线程的阻塞并不是仅仅体现在与后端的交互上,还包括前台自己的处理逻辑,下面看一下runoob回调函数章节的例子(同步读取文件):
var fs = require("fs");
var data = fs.readFileSync('input.txt');
console.log(data.toString());
console.log("end");
把上例代码保存在test3.js中,然后通过node test3.js命令来验证(同步读取文件,文件内容会先于end打印):
再看一下异步读取文件例子:
var fs = require("fs");
fs.readFile('input.txt', function (err, data) {
if (err) return console.error(err);
console.log(data.toString());
});
console.log("end");
把代码保存在test3.js中,然后通过node test3.js命令来验证(异步读取文件,文件内容会后于end打印):
我们可以根据实际业务场景的需要来选择使用同步还是异步,但不论哪种请求,实际上仍未摆脱单线程的束缚,而HTML5则提供了真正意义的多线程——worker。下面是一个从w3school拿来的worker的例子:
html页面:
<!DOCTYPE html>
<html>
<body>
<p>Count numbers: <output id="result"></output></p>
<button οnclick="startWorker()">Start Worker</button>
<button οnclick="stopWorker()">Stop Worker</button>
<br /><br />
<script>
var w;
function startWorker() {
if (typeof(Worker)!=="undefined") {
if (typeof(w)=="undefined") {
w=new Worker("js/test4.js");
}
w.onmessage = function (event) {
document.getElementById("result").innerHTML=event.data;
};
} else {
document.getElementById("result").innerHTML="Sorry, your browser does not support Web Workers...";
}
}
function stopWorker() {
w.terminate();
}
</script>
</body>
</html>
需要另起线程执行的javascript(需要new出Worker对象的代码,对应上面的test4.js):
var i=0;
function timedCount(){
i=i+1;
postMessage(i);
setTimeout("timedCount()",500);
}
timedCount();
上例的功能为在页面上提供启动worker和停止worker的功能,worker会每隔500毫秒返回执行的结果,由启动worker的javascript来处理结果(本例为在页面上直接显示)。worker的运行与页面的操作不会互相影响,即使我们把定时器的时间间隔调整为10毫秒,页面也不会发生阻塞,以下为执行结果:
worker的数据提交(启动线程向主线程返回数据):postMessage(obj)
worker的数据接收(主线程接收启动线程返回数据):worker.onMessage = function(event)(//传递的obj数据可以通过event.data获取),这里的worker是new出来的Worker对象。
注意:本例的代码必须运行在一个web应用中,直接在浏览器中访问静态页面会报错(点击启动worker时):
此外,本例的代码也不可以在nodejs中运行,nodejs无法识别Worker,且nodejs有自己的多线程库cluster。