JavaScript异步编程[试读]
第1章:深入理解JavaScript事件
事件!事件到底是怎么工作的?JavaScript出现了多久,对JavaScript异步事件模型就迷惘了多久。迷惘导致bug,bug导致愤怒,然后尤达大师就会教我们如何如何…… 不过本质上,从概念上看,JavaScript事件模型既优雅又实用。一旦大家接受了这种语言的单线程设计,就会觉得JavaScript事件模型更像是一种功能,而不是一种局限。它意味着我们的代码是不可中断的,也意味着调度的事件会整整齐齐排好队,有条不紊地运行。 本章将介绍JavaScript的异步机制,并破除一些常见的误解。我们会看到setTimeout真正做了些什么。接着会讨论回调中抛出错误的处理。最后会奠定本书的主旨... 查看全部[ 第1章:深入理解JavaScript事件 ]
1.1.1 现在还是将来运行
在探究setTimeout之前,先来看一个简单的例子。该情形常常会迷惑JavaScript新手,特别是那些刚刚从Java和Ruby等多线程语言迁移过来的新手。 EventModel/loopWithTimeout.js for (var i = 1; i <= 3; i++) { setTimeout(function(){ console.log(i); }, 0); }; 4 4 4 大多数刚接触JavaScript语言的人都会认为以上循环会输出1,2,3,或者重复输出这3个数字,因为这里的3次延时都抢着要第一个触发(每次暂停都调度为0毫秒后到时)。 ... 查看全部[ 1.1.1 现在还是将来运行 ]
1.1.2 线程的阻塞
下面这段代码打破了我对JavaScript事件的成见。 EventModel/loopBlockingTimeout.js var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {}; 按照多线程的思维定势,我会预计500毫秒后计时函数就会运行。不过这要求中断欲持续整整一秒钟的循环。如果运行代码,会得到类似这样的结果... 查看全部[ 1.1.2 线程的阻塞 ]
1.1.3 队列
调用setTimeout的时候,会有一个延时事件排入队列。然后setTimeout调用之后的那行代码运行,接着是再下一行代码,直到再也没有任何代码。这时JavaScript虚拟机才会问:“队列里都有谁啊?” 如果队列中至少有一个事件适合于“触发”(就像1000毫秒之前设定好的那个为期500毫秒的延时事件),则虚拟机会挑选一个事件,并调用此事件的处理器(譬如传给setTimeout的那个函数)。事件处理器返回后,我们又回到队列处。 输入事件的工作方式完全一样:用户单击一个已附加有单击事件处理器的DOM(Document Object Model,文档对象模型)元素时,会有一个单击事件排入... 查看全部[ 1.1.3 队列 ]
1.2 异步函数的类型
每一种JavaScript环境都有自己的异步函数集。有些函数,如setTimeout和setInterval,是各种JavaScript环境普遍都有的。另一些函数则专属于某些浏览器或某几种服务器端框架。JavaScript环境提供的异步函数通常可以分为两大类:I/O函数和计时函数。如果想在应用中定义复杂的异步行为,就要使用这两类异步函数作为基本的构造块。... 查看全部[ 1.2 异步函数的类型 ]
1.2.1 异步的I/O函数
创造Node.js,并不是为了人们能在服务器上运行JavaScript,仅仅是因为Ryan Dahl想要一个建立在某高级语言之上的事件驱动型服务器框架。JavaScript碰巧就是适合干这个的语言。为什么?因为JavaScript语言可以完美地实现非阻塞式I/O。 在其他语言中,一不小心就会“阻塞”应用(通常是运行循环)直到完成I/O请求为止。而在JavaScript中,这种阻塞方式几乎沦为无稽之谈。类似如下的循环将永远运行下去,不可能停下来。 var ajaxRequest = new XMLHttpRequest; ajaxRequest.open('GET', url); a... 查看全部[ 1.2.1 异步的I/O函数 ]
1.2.2 异步的计时函数
我们已经看到,异步函数非常适合用于I/O操作,但有些时候,我们仅仅是因为需要异步而想要异步性。换句话说,我们想让一个函数在将来某个时刻再运行——这样的函数可能是为了作动画或模拟。基于时间的事件涉及两个著名的函数,即setTimeout与setInterval。 遗憾的是,这两个著名的计时器函数都有自己的一些缺陷。正如我们在1.1.2节中看到的,其中有个缺陷是无法弥补的:当同一个JavaScript进程正运行着代码时,任何JavaScript计时函数都无法使其他代码运行起来。但是,即便容忍了这一局限性,setTimeout及setInterval的不确定性也会令人犯怵。下面是一个示例。 E... 查看全部[ 1.2.2 异步的计时函数 ]
1.3 异步函数的编写
JavaScript中的每个异步函数都构建在其他某个或某些异步函数之上。凡是异步函数,从上到下(一直到原生代码)都是异步的! 反之亦然:任何函数只要使用了异步的函数,就必须以异步的方式给出其操作结果。正如我们在1.1.2节学到的,JavaScript并没有提供一种机制以阻止函数在其异步操作结束之前返回。事实上,除非函数返回,否则不会触发任何异步事件。 本节将考察异步函数设计的一些常见模式。我们将看到有些函数如反复无常的小人,非得等到特定时候才下决心成为异步的。不过,我们先来精确地定义异步函数。... 查看全部[ 1.3 异步函数的编写 ]
1.3.1 何时称函数为异步的
异步函数这个术语有点名不副实:调用一个函数时,程序只在该函数返回之后才能继续。JavaScript写手如果称一个函数为“异步的”,其意思是这个函数会导致将来再运行另一个函数,后者取自于事件队列(若后面这个函数是作为参数传递给前者的,则称其为回调函数,简称为回调)。于是,一个取用回调的异步函数永远都能通过以下测试。 var functionHasReturned = false; asyncFunction(function() { console.assert(functionHasReturned); }); functionHasReturned = true; 异步函数... 查看全部[ 1.3.1 何时称函数为异步的 ]
1.3.2 间或异步的函数
有些函数某些时候是异步的,但其他时候却不然。举个例子,jQuery的同名函数(通常记作$)可用于延迟函数直至DOM已经结束加载。但是,若DOM早已结束了加载,则不存在任何延迟,$的回调将会立即触发。 不注意的话,这种行为的不可预知性会带来很多麻烦。我曾经看到也犯过这样一个错误,即假定$会在已加载本页面其他脚本之后再运行一个函数。 // application.js $(function() { utils.log('Ready'); }); // utils.js window.utils = { log: function() { if (windo... 查看全部[ 1.3.2 间或异步的函数 ]
1.3.3 缓存型异步函数
间或异步的函数有一个常见变种是可缓存结果的异步请求类函数。举例来说,假设正在编写一个基于浏览器的计算器,它使用了网页Worker对象以单独开一个线程来进行计算。(第5章将介绍网页Worker对象的API。)主脚本看起来像这样: var calculationCache = {}, calculationCallbacks = {}, mathWorker = new Worker('calculator.js'); mathWorker.addEventListener('message', function(e) { var message = e.dat... 查看全部[ 1.3.3 缓存型异步函数 ]
1.3.4 异步递归与回调存储
在runCalculation函数中,为了等待Worker对象完成自己的工作,或者通过延时而重复相同的函数调用(即异步递归),或者简单地存储回调结果。 哪种方式更好呢?乍一看,只使用异步递归是最简单的,因为这里不再需要calculationCallbacks对象。出于这个目的,JavaScript新手常常会使用setTimeout,因为它很像线程型语言的风格。此程序的Java版本可能会有这样一个循环: while (!calculationCache.get(formula)) { Thread.sleep(0); }; 但是,延时并不是免费的午餐。大量延时的话,会造成巨大的计算... 查看全部[ 1.3.4 异步递归与回调存储 ]
1.3.5 返值与回调的混搭
在以上两种runCalculation实现中,有时会用到返值技术。这是出于简洁的目的而随意作出的选择。下面这行代码 return callback(calculationCache[formula]); 很容易即可改写成 callback(calculationCache[formula]); return; 这是因为并没有打算使用这个返值。这是JavaScript的一种普遍做法,而且通常无害。 不过,有些函数既返回有用的值,又要取用回调。这类情况下,切记回调有可能被同步调用(返值之前),也有可能被异步调用(返值之后)。 永远不要定义一个潜在同步而返值却有可能用于回调的... 查看全部[ 1.3.5 返值与回调的混搭 ]
1.4 异步错误的处理
像很多时髦的语言一样,JavaScript也允许抛出异常,随后再用一个try/catch语句块捕获。如果抛出的异常未被捕获,大多数JavaScript环境都会提供一个有用的堆栈轨迹。举个例子,下面这段代码由于'{'为无效JSON对象而抛出异常。 EventModel/stackTrace.js function JSONToObject(jsonStr) { return JSON.parse(jsonStr); } var obj = JSONToObject('{'); SyntaxError: Unexpected end of input at Obje... 查看全部[ 1.4 异步错误的处理 ]
1.4.1 回调内抛出的错误
如果从异步回调中抛出错误,会发生什么事?让我们先来做个测试。 EventModel/nestedErrors.js setTimeout(function A() { setTimeout(function B() { setTimeout(function C() { throw new Error('Something terrible has happened!'); }, 0); }, 0); }, 0); 上述应用的结果是一条极其简短的堆栈轨迹。 Error: Something terrible has happened... 查看全部[ 1.4.1 回调内抛出的错误 ]
1.4.2 未捕获异常的处理
如果是从回调中抛出异常的,则由那个调用了回调的人负责捕获该异常。但如果异常从未被捕获,又会怎么样?这时,不同的JavaScript环境有着不同的游戏规则…… 1. 在浏览器环境中 现代浏览器会在开发人员控制台显示那些未捕获的异常,接着返回事件队列。要想修改这种行为,可以给window.onerror附加一个处理器。如果windows.onerror处理器返回true,则能阻止浏览器的默认错误处理行为。 window.onerror = function(err) { return true; //彻底忽略所有错误 }; 在成品应用中,会考虑某种JavaScript错误处理... 查看全部[ 1.4.2 未捕获异常的处理 ]
1.4.3 抛出还是不抛出
遇到错误时,最简单的解决方法就是抛出这个错误。在Node代码中,大家会经常看到类似这样的回调: function(err) { if (err) throw err; // ... } 在第4章中,我们会经常沿用这一做法。但是,在成品应用中,允许例行的异常及致命的错误像踢皮球一样踢给全局处理器,这是不可接受的。回调中的throw相当于JavaScript写手在说“现在我还不想考虑这个”。 如果抛出那些自己知道肯定会被捕获的异常呢?这种做法同样凶险万分。2011年,Isaac Schlueter(npm的开发者,在任的Node开发负责人)就主张try/catch是一种“反... 查看全部[ 1.4.3 抛出还是不抛出 ]
1.5 嵌套式回调的解嵌套
JavaScript中最常见的反模式做法是,回调内部再嵌套回调。还记得前言里提到的金字塔厄运吗?我们先来看一个具体的例子,你也可能在Node服务器上看到过类似的代码。 function checkPassword(username, passwordGuess, callback) { var queryStr = 'SELECT * FROM user WHERE username = ?'; db.query(queryStr, username, function (err, result) { if (err) throw err; hash(pas... 查看全部[ 1.5 嵌套式回调的解嵌套 ]
1.6 小结
本章阐释了JavaScript的单线程性为什么既是福利又是祸害。使用得当的话,它会使代码优美且没有那些多线程应用中泛滥成灾的可怕竞态条件。不过,这需要你形成正确的思维定势并掌握恰当的技术。 本书其余章节将介绍JavaScript中处理事件时用到的一些库和设计模式。我们考查的所有示例都可以运行于主流的浏览器或未经改动的Node.js环境。不过,编写JavaScript并不是产生JavaScript代码的唯一途径。关于其他一些有趣编辑器的概况,请参阅附录A。 这里值得提一下,JavaScript中存在一种多线程性:可以孵化出Worker进程。每个孵化出的进程都可以与其他进程交换数据,其限制等... 查看全部[ 1.6 小结 ]
书名: JavaScript异步编程
作者: [英] Trevor Burnham
出版社: 人民邮电出版社
原作名: Async JavaScript: Build More Responsive Apps with Less Code
副标题: 设计快速响应的网络应用
译者: 许青松
出版年: 2013-6
页数: 118
定价: 32.00元
装帧: 平装
丛书: 图灵程序设计丛书
ISBN: 9787115316578