JavaScript已然成为了多媒体、多任务、多内核网络世界中的一种单线程语言。其利用事件模型处理异步触发任务的行为成就了JavaScript作为开发语言的利器。如何深入理解和掌握JavaScript异步编程变得尤为重要!!!《JavaScript异步编程设计快速响应的网络应用》提供了一些方法和灵感。
一、深入理解JavaScript事件
1. 事件的调度
JavaScript事件处理器在线程空闲之前不会运行(空闲时运行)。
var start = new Date();
setTimeout(function() {
var end = new Date();
console.log(‘Time elapsed:‘, + (end - start), ‘ms‘);
}, 500);
while(new Date - start < 1000) {};
// 结果:Time elapsed: 1001 ms(至少是1000)
因为setTimeout回调在while循环结束运行之前不可能被触发!
调用setTimeout时,会有一个延时事件排入队列。然后继续执行下一行代码,直到再没有任何代码(处理器空闲时),才执行setTimeout回调函数(前提已到达其延迟时间)。
JavaScript代码永远不会被中断,这是因为代码在运行期间内只需要安排队事件即可,而这些事件在代码运行结束之前不会被触发!
2. 异步函数的类型
JavaScript异步函数可分为两大类:I/O函数(非阻塞)和计时函数
/* test.js */
var obj = {};
console.log(obj);
obj.foo = ‘bar‘;
WebKit的console.log由于表现出异步行为而让很多开发者惊诧不已。在Chrome或Safari中,以下这段代码会在控制台记录{foo:bar}
。
WebKit的console.log并没有立即拍摄对象快照,相反,它只存储了一个指向对象的引用,然后在代码返回事件队列时才去拍摄快照。
Node的console.log是另一回事,它是严格同步的,因此同样的代码输出的却为{}
注意:在控制台记录{foo:bar},是在先执行后打开控制台!我们通过console调试代码时,要格外注意。
3. 异步函数的编写
调用一个函数(异步函数)时,程序只在该函数返回之后才能继续。这个函数会到导致将来再运行另一个函数(回调函数)。
JavaScript并没有提供一种机制以阻止函数在其异步操作结束之前返回。
有些函数既返回有用的值,又要取用回调。这种情况下,切记回调有可能被同步调用(返值之前),也有可能被异步调用(返值之后)。
永远不要定义一个潜在同步而返值却有可能用于回调的函数(回调依赖返回值)。
function test(callback) {
var obj = {
sendData: function() {
console.log(arguments);
}
};
callback(); // setTimeout(callback, 0); 正确写法
return obj;
}
var obj = test(function(){
obj.sendData("test callback"); // 返值用于了回调的函数中
});
如果一个函数既返回值又运行回调,则需确保回调在返值之后才运行!!
4. 异步错误的处理
try{
setTimeout(function() {
throw new Error("Catch me if you can!");
}, 0);
} catch(e) {
console.log(e);
}
try/catch语句只能捕获setTimeout函数自身内部发生的错误!
所以,只能在回调内部处理源于回调的异步错误。
setTimeout(function() {
try{
throw new Error("Catch me if you can!");
} catch(e) {
console.log(e);
}
}, 0);
对于未捕获异常的处理:
(1)浏览器环境中
window.onerror = function(err) {
return true; // 彻底忽略所有错误
}
(2)Node环境中
process.on(‘uncaughtException‘, function(err) {
console.log(err); // 避免程序关闭
})
5. 嵌套式回调的解嵌套
JavaScript中最常见的反模式做法是,回调内部再嵌套回调。
请避免两层以上的函数嵌套。关键是找到一种在激活异步调用之函数的外部存储异步结果的方式,这样回调本身就没有必要再嵌套了。
二、分布式事件
事件的蝴蝶偶然扇动了下翅膀,整个应用到处都引发了反应。
这里描述的方式为发布/订阅模式,即观察者模式。曾在我的博客中介绍过:JavaScript设计模式–观察者模式
1. Node中的EventEmitter对象
ode里面的许多对象都会分发事件:一个net.Server对象会在每次有新连接时分发一个事件, 一个fs.readStream对象会在文件被打开的时候发出一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。 你可以通过require("events")
来访问该模块。
// 加载EventEmitter类
var EventEmitter = require(‘events‘).EventEmitter;
var emitter = new EventEmitter();
// 监听事件
emitter.on("myCustomerEvent", function(message) {
console.log(message);
});
// 触发事件
emitter.emit("myCustomerEvent", "ligang");
2. 实现自己的事件发布系统
function MyEvents() {
this._events = {};
/**
* 事件监听
* @param names 事件名称
* @param callback 事件处理函数
* @param data 注册事件时传递的参数,在callback 中用this.data 获取该值
*/
this.on = function(names, callback,data) {
// 支持多个事件,共享一个处理函数
// 多个事件使用“逗号、空格、分号”间隔
var nameList = names.split(/[\,\s\;]/);
var index = nameList.length;
while (index) {
index--;
var name = nameList[index];
if (!this._events[name]) {
this._events[name] = [];
}
this._events[name].push({callback:callback,data:data});
}
};
/**
* 事件移除
* @param name 事件名称
* @param callback 事件处理函数
*/
this.off = function(name, callback) {
// 不传入任何事件名,移除全部事件
if (!name) {
this._events = {};
return;
}
var event = this._events[name];
// 不存在当前事件,直接返回
if (!event) {
return;
}
// 支持同一事件,被多次绑定
if (!callback) {
delete this._events[name];
} else {
var length = event.length;
while (length > 0) {
length--;
if (event[length].callback === callback) {
event.splice(length, 1);
}
}
}
};
/**
* 触发事件
* Eg:A.B.C
* 触发顺序:A.B.C ==> A.B ==> A
* @param name 事件名称
* @param args 参数
*/
this.emit = function(name, args) {
var handleEvent = name,
namesAry = handleEvent.split(".");
for(var i = 0, len = namesAry.length; i < len; i++) {
var event = this._events[handleEvent];
if (event) {
var j = 0, length = event.length;
while (j < length) {
event[j].callback(args);
j++;
}
}
namesAry.pop();
handleEvent = namesAry.join(".");
}
};
}
3. 同步性
$("input[type=‘submit‘]")
.on("click", function(){
console.log("click");
}).trigger("click"); // 触发事件
console.log("lalala");
// 输出结果为:click lalala
这证明了click事件的处理函数因为trigger方法而立即被激活。事实上,只要触发了jQuery事件,就会不被中断地按顺序执行其所有事件处理函数。
需要明确一点,如果用户点击submit按钮时,这确实是一个异步事件!!!
4. jQuery自定义事件
自定义事件是jQuery被低估的功能之一,它简化了强大分布式事件系统向任何Web应用程序的移植,而且无需额外的库。
补充一下:冒泡
只要某个DOM元素触发了某个事件,其父元素就会接着触发这个事件,接着是父元素的父元素,以此类推,一直追溯到根元素document;除非在这条冒泡之路的某个地方调用了事件的stopPropagation方法(如果事件处理函数返回false,则jQuery会替我们自动调用stopPropagation方法)。需要注意的是,blur、focus、mouseenter、mouseleave不支持冒泡。
示例:jQuery自定义事件同样支持冒泡
$(".pt-login-logo-signin, document").on("fizz", function(){
console.log("fizz");
}).trigger("fizz");
有时我们不想让其冒泡,幸运的是jQuery提供了对应的方法triggerHandler()
:
这个特别的方法将会触发指定的事件类型上所有绑定的处理函数。但不会执行浏览器默认动作,也不会产生事件冒泡。
这个方法的行为表现与trigger类似,但有以下三个主要区别:
* 第一,他不会触发浏览器默认事件。
* 第二,只触发jQuery对象集合中第一个元素的事件处理函数。
* 第三,这个方法的返回的是事件处理函数的返回值,而不是据有可链性的jQuery对象。此外,如果最开始的jQuery对象集合为空,则这个方法返回 undefined
// 浏览器默认动作将不会被触发,只会触发你绑定的动作。即鼠标光标不能聚焦到input元素上
$("input").triggerHandler("focus");
三、Promise对象和Deferred对象
示例:进度通知
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>progress Demo</title>
<script src="../../lib/jquery/dist/jquery.min.js"></script>
</head>
<body>
<input type="text" id="number">
<span id="tips"></span>
<script>
var $input = $("#number"),
$tips = $("#tips");
var def = $.Deferred();
var goalCount = 20;
def.progress(function(currentCount){
var percentComplete = Math.floor(currentCount / goalCount * 100);
$tips.text(percentComplete + "% complete");
});
def.done(function(){
$tips.text("good job!");
});
$input.on("keypress", function(){
var count = $(this).val().split("").length;
if(count >= goalCount) {
def.resolve();
}
// notify,调用一个给定args的递延对象上的进行中的回调(progressCallbacks)
def.notify(count);
});
</script>
</body>
</html>
四、Async.js的工作流控制
1. 异步函数按顺序运行
假设我们希望某一组异步函数能依次运行。
funcs[0](function(){
funcs[1](function(){
funcs[2](function(){
...
});
});
});
// async.js
var async = require("async");
var start = new Date().getTime();
async.series([
function(callback){
setTimeout(callback, 100);
}, function(callback){
setTimeout(callback, 200);
}, function(callback){
setTimeout(callback, 300);
}
],function(err, result){
console.log(new Date().getTime() - start + "ms"); // 612
});
async.series([
function(callback){
callback(null, ‘one‘);
}, function(callback){
callback(null, ‘two‘);
}
],function(err, result){
console.log(result); // ["one", "two"]
});
2. 异步函数并行运行
var async = require("async");
var start = new Date().getTime();
async.parallel([
function(callback){
setTimeout(callback, 100);
}, function(callback){
setTimeout(callback, 200);
}, function(callback){
setTimeout(callback, 300);
}
],function(err, result){
console.log(new Date().getTime() - start + "ms"); // 312
});
3. 极简主义Step的工作流控制
var fs = require("fs");
var path = require("path");
var Step = require("step"); // https://github.com/creationix/step
// 按顺序执行
Step(function readSelf() {
fs.readFile(__filename, this);
}, function capitalize(err, text) {
if (err) throw err;
return new Buffer(text).toString().toUpperCase();
}, function showIt(err, newText) {
if (err) throw err;
console.log(newText);
}
);
// 并发执行
Step(
// Loads two files in parallel
function loadStuff() {
console.log(".."+__dirname)
fs.readFile(__dirname + "/a.txt", ‘UTF-8‘, this.parallel());
fs.readFile(__dirname + "/b.txt", this.parallel());
},
// Show the result when done
function showStuff(err, a, b) {
if (err) throw err;
console.log(a);
console.log("=============");
console.log(new Buffer(b).toString());
}
);
// 动态
Step(
function readDir() {
fs.readdir(__dirname, this);
},
function readFiles(err, results) {
if (err) throw err;
// Create a new group
var group = this.group();
results.forEach(function (filename) {
if (/\.js$/.test(filename)) {
fs.readFile(__dirname + "/" + filename, ‘utf8‘, group());
}
});
},
function showAll(err , files) {
if (err) throw err;
console.dir(files);
}
);
五、worker对象的多线程技术
我们会经常看到,在JavaScript中事件是多线程技术的替代品;但是其更准确来说,事件只能代替一种特殊的多线程。
在JavaScript中我们可以利用worker单开一个单独的线程,其交互方式类似于I/O操作。
注意:同一个进程内的多个线程之间可以分享状态,而彼此独立的进程之间则不能。
1. 网页版worker对象
想要生成worker对象,只需以脚本URL为参数来调用全局Worker构造函数即可。
/* main.js */
var worker = new Worker("sub.js");
// 创建worker对象
worker.addEventListener("message", function(e){
// 接收sub消息
console.log(e.data);
});
// 给sub发送消息
worker.postMessage("football");
worker.postMessage("baseball");
/* sub.js */
/**
* 在worker线程中,我们可以做一些耗时较大的计算,但是其计算结果要发送给主线程,由主线程去更新页面.
* 为什么不在worker线程中直接更新页面呢?
* 主要是为了保护JavaScript异步抽象概念,使其免受影响.
* 如果worker对象可以改变页面,最终的下场可能就像java一样,必须将DOM操作代码封装成互斥量和信号量,避免竞争状态.
* 基于类似情况,worker对象中也看不到全局的window对象和主线程及其他worker线程中的其他任何对象.
* worker对象只能看到自己的全局对象self,以及self以捆绑的所有东西.
* 包括:setTimeout,XMLHttpRequest对象等
*/
self.addEventListener("message", function(e){
self.postMessage(e.data);
});
2. cluster带来的Node版worker
var cluster = require("cluster");
if(cluster.isMaster) {
var coreCount = require("os").cpus().length;
for(var i = 0; i < coreCount; i++) {
var worker = cluster.fork();
worker.send("Hello worker!");
worker.on("message", function (message) {
// Node基于worker对象发送自己的消息,命令格式为
// {cmd: ‘online‘, _queryId: 1, _workerId: 1}
if(message._queryId) return;
console.log(message);
});
}
}else {
process.send("Hello, main process!");
process.on("message", function (message) {
console.log(message);
})
}
注意:cluster支持并发运行同一脚本,为了尽可能减少线程间的通信开销,线程间分享的状态应该存储在像Redis这样的外部数据库中.
六、异步的脚本加载
<script src="resource.js"></script>
在文档<head>
上述加载js为同步阻塞加载(脚本下载完毕并运行之后,浏览器才会加载后续资源),为了避免一些不必要的问题,我们一般把必须立即加载的放到中,可以稍后加载的放到<body>
中。
1. 脚本的延迟运行
<script defer src="resource.js"></script>
其相当于告知浏览器:“请马上开始加载这个脚本,但是,请等到文档就绪且所有此前具有defer属性的脚本都结束运行之后再运行它”
在文档<head>
标签里放入延迟脚本,既能带来脚本置于<body>
标签时的全部好处,又能让大文档的加载速度大幅提升。
提示:目前存在部分浏览器不支持defer,可以将延迟脚本中的代码封装诸如$(document).ready
的结构中。
2. 脚本的异步运行
<script async src="resource.js"></script>
脚本会以任意次序运行,而且只要JavaScript引擎可用就会立即运行,而不论文档就绪与否。
注意:
(1)在同时支持这两个属性的浏览器中使用,async会覆盖掉defer。
(2)使用异步或延迟加载的脚本中,不能使用document.write
,其会表现出不可预知的行为。
3. 动态加载脚本
var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.src = "resource.js";
head.appendChild(script);
script.onload = function(){
// 可以调用动态加载脚本中的函数了
};
注意:onload兼容性问题
所以这里还是推荐大家使用第三方库,比如:requirejs