執行緒與程式
眾所周知,Javascript的執行環境是'單執行緒'。執行緒的定義又是什麼呢?在說到執行緒之前我們先了解下程式。
程式:程式指正在執行的程式。確切的來說,當一個程式進入記憶體執行,即變成一個程式,程式是處於執行過程中的程式,並且具有一定獨立功能。
執行緒:執行緒是程式中的一個執行單元,負責當前程式中程式的執行,一個程式中至少有一個執行緒。一個程式中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。
一個程式執行後至少有一個程式,一個程式可以包含多個執行緒。
多執行緒對於高併發量就有益,但多執行緒程式會出現多個執行緒對一個資源進行訪問的問題。具體的解決方案是利用鎖。單執行緒就不會出現此類的問題。但單執行緒如果出現堵塞,就會而獨佔cpu導致其他程式碼不能執行。其解決方案就是非同步程式設計。node是單執行緒,Java是多執行緒。
nodejs裡面的非同步。
nodejs裡面大部分的api都是非同步,少量的api是同步的。I/O操作會比較耗時但不會獨佔CPU,典型的I/O比如檔案讀寫,遠端資料庫讀寫,網路請求等.耗時的解決方案就是非同步。在node.js程式裡面,有一個使用者執行緒(javascript所宣稱的單執行緒)和一個非同步執行緒池(使用者無法直接訪問), 如果跑在非同步執行緒上的程式碼是阻塞的,那麼這種非同步根本就起不到消除阻塞的作用.原因就是阻塞程式碼會霸佔cpu,導致本程式所有程式碼都等待不管是哪個執行緒。但是node.js裡面的I/O API都是不會霸佔CPU的,所以是非阻塞的,就不會出現這個問題。這就是node.js的最引以為傲的特性之一:非同步非阻塞I/O.
javascript非同步程式設計的方式
- 回撥函式。
回撥函式用來定義一次性響應的邏輯。比如說對於資料庫查詢,可以指定一個回撥函式來處理如何處理查詢結果。
function f2(){
console.log("f1任務程式碼")
}
function f1(callback){
setTimeout(function () {
// f1的任務程式碼
callback();
}, 1000);
}
f1(f2);
複製程式碼
這裡的非同步api是瀏覽器提供的setTimeout,回撥函式callback會被放入事件佇列裡面,不會阻塞cpu,也就是說程式碼會繼續往下執行.等setTimeOut完成後,再執行callback。
var http = require("http");
var fs = require("fs");
http.createServer(function (req, res) {
if(req.url == '/'){
fs.readFile('./title.json',function (err, data) {
if(err){
console.log(err);
res.end('Server error');
}else {
var titles = JSON.parse(data.toString());
fs.readFile('./template.html',function (err, data) {
if(err){
console.log(err);
res.end('Server error');
}else {
var templ = data.toString();
var html = templ.replace('%', titles.join('<li></li>'));
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(html);
}
})
}
})
}
}).listen(8000, "127.0.0.1");
複製程式碼
上面的例子就是node的一些I/O操作。回撥函式層層巢狀,這也就是傳說中的'callback hell'.寫起來很爽,簡單、容易理解.但維護的人估計得瘋。解決方法是建立中間函式以減少巢狀,或者是node中慣用手法減少if/else引起的巢狀:儘早在函式中返回。詳情請見node in action一書中3-2;
- 事件監聽。
本質也是一個回撥,但它和一個概念實體(事件)有關聯。點選滑鼠是一個事件。當然在伺服器端,當有HTTP請求過來的時候,HTTP伺服器會發出一個請求事件。你所要做的就是監聽那個請求事件,並新增一些響應邏輯。
談到事件,我們就必須要說到觀察者模式(observer pattern)。這種模式在程式設計屆無處不在。阮一峰給出的定義是:
我們假定,存在一個"訊號中心",某個任務執行完成,就向訊號中心"釋出"(publish)一個訊號,其他任務可以向訊號中心"訂閱"(subscribe)這個訊號,從而知道什麼時候自己可以開始執行。這就叫做"釋出/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。
我一直堅信程式是生活的抽象。不要以為你每天面對的是一個機器,其實它也是一個世界。坐臥鋪車,列車員收走車票,我們訂閱這個事件,就相當於訂閱者,列車員相當於觀察者。等車到了站,釋出這個事件,列車員就會來叫我們。我們執行下車這個回撥。
這種模式有多種的實現,而他的應用就不勝列舉了,比如rxjs裡面的響應式資料,redux裡面的subscribe等等.
node裡面的http伺服器例項就是一個事件發射器,也是這種模式的應用。
該例項來自於node in action.
var events = require("events");
var net = require("net");
var channel = new events.EventEmitter();
channel.clients = {};
channel.subscriptions = {};
channel.on('join',function (id, client) {
this.clients[id] = client;
this.subscriptions[id] = function (senderId, message) {
if(id != senderId){
this.clients[id].write(message);
}
};
var welcome = "Welcome!\n" + "Guests online " + this.listeners('broadcast').length;
client.write(welcome + '\n');
this.on('broadcast', this.subscriptions[id]);
});
//當有使用者離開時,通知其他使用者;
channel.on('leave',function (id) {
channel.removeListener('broadcast',this.subscriptions[id]);
channel.emit('broadcast',id, id + "has left the chat.\n");
});
var server = net.createServer(function (client) {
var id = client.remoteAddress + ': ' + client.remotePort;
// client.on('connect', function () {
channel.emit('join', id, client);
// });
client.on('data', function (data) {
var data = data.toString();
channel.emit('broadcast', id, data);
});
client.on('close',function () {
channel.emit('leave',id);
})
});
server.listen(8888);
複製程式碼
這裡channel物件繼承自EventEmitter,利用on訂閱事件,emit釋出事件。執行相應的回撥邏輯。EventEmitter核心是事件觸發與事件監聽器功能的封裝,大多數模組多是繼承它。fs,net,http.
- Promise物件。
Promises物件是CommonJS工作組提出的一種規範,目的是為非同步程式設計提供統一介面。簡單說,它的思想是,每一個非同步任務返回一個Promise物件,該物件有一個then方法,允許指定回撥函式。 這裡簡單介紹下Promise.想要就具體介紹可以瞭解下你不知道的JavaScript 中和阮一峰的es6入門
Promise只是改善了回撥函式的寫法,使回撥函式變成了鏈式寫法。並且可以執行多次的回撥。廢話不多說,直接上程式碼。
const promise = new Promise(function(resolve, reject) {
// ... 這裡可以做一些非同步的操作;
if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
複製程式碼
- Generator函式.
執行Generator函式後會返回一個遍歷器物件(Iterator)。本質上Generator是一個狀態機,每次的遍歷Generator函式返回的都是函式內部的每個狀態。
function* gen(){
yield 1;
yield 2;
return 1;
}
var g = gen();
gen.next() // {value:1,done:false}
gen.next() // {value:2,done:false}
gen.next() // {value:3,done:true}
複製程式碼
呼叫Generator函式,返回遍歷器物件,代表Generator函式內部指標。每次呼叫next方法,就會返回一個value和done屬性的物件,value是yield後面表示式的值。done表示便利是否結束。
Generator函式的非同步程式設計;
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
複製程式碼
這裡的fetch請求資料。相當於ajax. result.value是fetch(url)返回的值是一個Promise. 所以可以用then語法。這裡注意下yield語句本身沒有返回值。在next中引數將作為上一個yield的返回值。
- async和await async函式是Generator函式的語法糖。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
// Generator函式版本
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
// async函式版本。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
複製程式碼
我們可以發現sync函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。 但有一些區別async函式的返回值是 Promise 物件,這比 Generator 函式的返回值是 Iterator 物件方便多了。你可以用then方法指定下一步的操作。
進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。