1. 程式人生 > >nodejs學習筆記(二)——javascript的同步非同步行為和多執行緒

nodejs學習筆記(二)——javascript的同步非同步行為和多執行緒

寫過後臺的同學一定對執行緒、執行緒池或者是多執行緒這些概念不會陌生,但是前臺在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 onclick="startWorker()">Start Worker</button>
<button onclick="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。