JavaScript异步编程设计快速响应的网络应用
2016-07-10 17:29
357 查看
JavaScript已然成为了多媒体、多任务、多内核网络世界中的一种单线程语言。其利用事件模型处理异步触发任务的行为成就了JavaScript作为开发语言的利器。如何深入理解和掌握JavaScript异步编程变得尤为重要!!!《JavaScript异步编程设计快速响应的网络应用》提供了一些方法和灵感。
因为setTimeout回调在while循环结束运行之前不可能被触发!
调用setTimeout时,会有一个延时事件排入队列。然后继续执行下一行代码,直到再没有任何代码(处理器空闲时),才执行setTimeout回调函数(前提已到达其延迟时间)。
JavaScript代码永远不会被中断,这是因为代码在运行期间内只需要安排队事件即可,而这些事件在代码运行结束之前不会被触发!
请参考:JavaScript事件驱动机制&定时器机制
WebKit的console.log由于表现出异步行为而让很多开发者惊诧不已。在Chrome或Safari中,以下这段代码会在控制台记录
WebKit的console.log并没有立即拍摄对象快照,相反,它只存储了一个指向对象的引用,然后在代码返回事件队列时才去拍摄快照。
Node的console.log是另一回事,它是严格同步的,因此同样的代码输出的却为{}
注意:在控制台记录{foo:bar},是在先执行后打开控制台!我们通过console调试代码时,要格外注意。
JavaScript并没有提供一种机制以阻止函数在其异步操作结束之前返回。
有些函数既返回有用的值,又要取用回调。这种情况下,切记回调有可能被同步调用(返值之前),也有可能被异步调用(返值之后)。
永远不要定义一个潜在同步而返值却有可能用于回调的函数(回调依赖返回值)。
如果一个函数既返回值又运行回调,则需确保回调在返值之后才运行!!
try/catch语句只能捕获setTimeout函数自身内部发生的错误!
所以,只能在回调内部处理源于回调的异步错误。
对于未捕获异常的处理:
(1)浏览器环境中
(2)Node环境中
请避免两层以上的函数嵌套。关键是找到一种在激活异步调用之函数的外部存储异步结果的方式,这样回调本身就没有必要再嵌套了。
这里描述的方式为发布/订阅模式,即观察者模式。曾在我的博客中介绍过:JavaScript设计模式–观察者模式
这证明了click事件的处理函数因为trigger方法而立即被激活。事实上,只要触发了jQuery事件,就会不被中断地按顺序执行其所有事件处理函数。
需要明确一点,如果用户点击submit按钮时,这确实是一个异步事件!!!
补充一下:冒泡
只要某个DOM元素触发了某个事件,其父元素就会接着触发这个事件,接着是父元素的父元素,以此类推,一直追溯到根元素document;除非在这条冒泡之路的某个地方调用了事件的stopPropagation方法(如果事件处理函数返回false,则jQuery会替我们自动调用stopPropagation方法)。需要注意的是,blur、focus、mouseenter、mouseleave不支持冒泡。
示例:jQuery自定义事件同样支持冒泡
有时我们不想让其冒泡,幸运的是jQuery提供了对应的方法
这个特别的方法将会触发指定的事件类型上所有绑定的处理函数。但不会执行浏览器默认动作,也不会产生事件冒泡。
这个方法的行为表现与trigger类似,但有以下三个主要区别:
* 第一,他不会触发浏览器默认事件。
* 第二,只触发jQuery对象集合中第一个元素的事件处理函数。
* 第三,这个方法的返回的是事件处理函数的返回值,而不是据有可链性的jQuery对象。此外,如果最开始的jQuery对象集合为空,则这个方法返回 undefined
jQuery的deferred对象详解
示例:进度通知
在JavaScript中我们可以利用worker单开一个单独的线程,其交互方式类似于I/O操作。
注意:同一个进程内的多个线程之间可以分享状态,而彼此独立的进程之间则不能。
注意:cluster支持并发运行同一脚本,为了尽可能减少线程间的通信开销,线程间分享的状态应该存储在像Redis这样的外部数据库中.
在文档
上述加载js为同步阻塞加载(脚本下载完毕并运行之后,浏览器才会加载后续资源),为了避免一些不必要的问题,我们一般把必须立即加载的放到中,可以稍后加载的放到
其相当于告知浏览器:“请马上开始加载这个脚本,但是,请等到文档就绪且所有此前具有defer属性的脚本都结束运行之后再运行它”
在文档
提示:目前存在部分浏览器不支持defer,可以将延迟脚本中的代码封装诸如
脚本会以任意次序运行,而且只要JavaScript引擎可用就会立即运行,而不论文档就绪与否。
注意:
(1)在同时支持这两个属性的浏览器中使用,async会覆盖掉defer。
(2)使用异步或延迟加载的脚本中,不能使用
注意:onload兼容性问题
所以这里还是推荐大家使用第三方库,比如:requirejs
一、深入理解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代码永远不会被中断,这是因为代码在运行期间内只需要安排队事件即可,而这些事件在代码运行结束之前不会被触发!
请参考: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对象
PromisejQuery的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
相关文章推荐
- JQuery1——基础($对象,选择器,对象转换)
- Android学习笔记(二九):嵌入浏览器
- Android java 与 javascript互访(相互调用)的方法例子
- JavaScript演示排序算法
- javascript实现10进制转为N进制数
- apache mpm
- 最后一次说说闭包
- Ajax
- 2019年开发人员应该学习的8个JavaScript框架
- HTML中的script标签研究
- 对一个分号引发的错误研究
- 异步流程控制:7 行代码学会 co 模块
- ES6 走马观花(ECMAScript2015 新特性)
- JavaScript拆分字符串时产生空字符的原因
- Canvas 在高清屏下绘制图片变模糊的解决方法
- Redux系列02:一个炒鸡简单的react+redux例子