线程与进程
众所周知,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命令的语法糖。