[Effective JavaScript 笔记]第67条:绝不要同步地调用异步的回调函数

设想有downloadAsync函数的一种变种,它持有一个缓存(实现为一个Dict)来避免多次下载同一个文件。在文件已经被缓存的情况下,立即调用回调函数是最优选择。

var cache=new Dict();
function downloadCachingAsync(url,onsuccess,onerror){
  if(cache.has(url)){
    onsuccess(cache.get(url));
    return;
  }
  return downloadAsync(url,function(file){
    cache.set(url,file);
    onsuccess(file);
  },onerror);
}

通常情况下,它会立即提供数据,但这种方式是违反了异步API客户端的期望。首先,它改变了操作的预期顺序。第62条显示了下面的例子,对于循规蹈矩的异步API应该总是以一种可预测的顺序来记录日志消息。

downloadAsync(‘file.txt‘,function(file){
  console.log(‘finished‘);
});
console.log(‘starting‘);

使用上面的downloadCachingAsync实现,这样的客户端代码可能最终会以任意的顺序记录事件,这取决于文件是否已被缓存起来。

downloadCachingAsync(‘file.txt‘,function(file){
  console.log(‘finished‘);
});
console.log(‘starting‘);

日志消息的顺序是一回事。更一般的是,异步API的目的是维持事件循环中每轮的严格分离。正如第61条解释的,这简化了并发,通过减轻每轮事件循环的代码量而不必担心其他代码并发地修改共享的数据结构。同步地调用异步的回调函数违反了这一分离,导致在当前轮完成之前,代码用于执行一轮隔离的事件循环。
例如,应用程序可能会持有一个剩余的文件队列给用户下载和显示消息。

downloadCachingAsync(remaining[0],function(file){
  remaining.shift();
});
status.display(‘Downloading ‘+remaining[0]+‘...‘);

如果同步地调用该回调函数,那么将显示错误的文件名的消息(或者更糟糕的是,如果队列为空会显示“undefined”)。
同步的调用异步的回调函数甚至可能会导致一些微妙的问题。第64条解释了异步的回调函数本质上是以空的调用栈来调用,因此将异步的循环实现为递归函数是安全的,完全没有累积超越调用栈空间的危险。同步的调用不能保障这一点,因而使得一个表面上的异步循环很可能会耗尽调用栈空间。另一种问题是异常。对于上面的downloadCachingAsync实现,如果回调函数抛出一个异常,它将会在每轮的事件循环中,也就是开始下载时而不是期望的一个分离的回合中抛出该异常。
为了确保总是异步地调用回调函数,我们可以使用已存在的异步API。就像我们在第65条和第66条中所做的一样,我们使用通用的库函数setTimeout在每隔一个最小的超时时间后给事件队列增加一个回调函数。可能有比setTimeout函数更完美的替代方案来调度即时事件,这取决于特定平台。

var cache=new Dict();
function downloadCachingAsync(url,onsuccess,onerror){
  if(cache.has(url)){
    var cache=cache.get(url);
    setTimeout(onsuccess.bind(null,cached),0);
    return;
  }
  return downloadAsync(url,function(file){
    cache.set(url,file);
    onsuccess(file);
  },onerror);
}

这里使用bind函数将结果保存为onsuccess回调函数的第一个参数。

提示

  • 即使可以立即得到数据,也绝不要同步地调用异步回调函数

  • 同步地调用异步的回调函数扰乱了预期的操作序列,并可能导致意想不到的交错代码
  • 同步地调用异步的回调函数可能导致栈溢出或错误地处理异常
  • 使用异步的API,比如setTimeout函数来调度异步回调函数,使其运行于另一回合
时间: 2024-10-07 01:47:42

[Effective JavaScript 笔记]第67条:绝不要同步地调用异步的回调函数的相关文章

[Effective JavaScript 笔记]第28条:不要信赖函数对象的toString方法

js函数有一个非凡的特性,即将其源代码重现为字符串的能力. (function(x){ return x+1 }).toString();//"function (x){ return x+1}" 反射获取函数源代码的功能很强大,使用函数对象的toString方法有严重的局限性.toString方法的局限性ECMAScript标准对函数对象的toString方法的返回结果(即该字符串)并没有任何要求.这意味着不同的js引擎将产生不同的字符串,甚至产生的字符串与该函数并不相关. 如果函数

[Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码

函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行.这使得富有表现力的高阶函数抽象如map和forEach成为可能.它也是js异步I/O方法的核心.与此同时,也可以将代码表示为字符串的形式传递给eval函数以达到同样的功能.程序员面临一个选择:应该将代码表示为函数还是字符串?毫无疑问,应该将代码表示为函数.字符串表示代码不够灵活的一个重要原因是:它们不是闭包. 闭包回顾 看下面这个图 js的函数值包含了比调用它们时执行所需要的代码还要多的信息.而且js函数值还在内部存储它们可能会引用

[Effective JavaScript 笔记] 第4条:原始类型优于封闭对象

js有5种原始值类型:布尔值.数字.字符串.null和undefined. 用typeof检测一下: typeof true; //"boolean" typeof 2; //"number" typeof "s";//"string" typeof null;//"object":ECMAScript把null描述为独特的类型,但返回值却是对象类型,有点困惑. 可以使用Object.prototype.t

[Effective JavaScript 笔记]第68条:使用promise模式清洁异步逻辑

构建异步API的一种流行的替代方式是使用promise(有时也被称为deferred或future)模式.已经在本章讨论过的异步API使用回调函数作为参数. downloadAsync('file.txt',function(file){ console.log('file:'+file); }); 基于promise的API不接收回调函数作为参数.相反,它返回一个promise对象,该对象通过其自身的then方法接收回调函数. var p=downloadP('file.txt'); p.th

[Effective JavaScript 笔记]第61条:不要阻塞I/O事件队列

js程序是构建在事件之上的.输入可能来自不同的外部源.在一些语言中,我们习惯地编写代码来等待某个特定的输入. var text=downloadSync('http://example.com/file.txt'); console.log(text); 像这样的形式downloadSync称为同步函数(或阻塞函数).程序会停止做任何工作,而等待它的输入.在这个例子中,也就是等待从网络上下载文件的结果.由于在等待下载完成的期间,计算机可以做其他有用的工作,因此这样的语言通常为程序员提供一种方法来

[Effective JavaScript 笔记]第63条:当心丢弃错误

管理异步编程的一个是错误处理.同步代码中只要使用try语句块包装一段代码很容易一下子处理所有的错误. try{ f(); g(); h(); } catch(e){ //这里用来下得出现的错误 } try语句块 但对于异步的代码,多步的处理通常会被分隔到事件队列的单独轮次中,因此,不可能将它们包装在一个try语句块中.事实上异步的API甚至根本不可能抛出异常,因为,当一个异步的错误发生时,没有一个明显的执行上下文来抛出异常!相反,异步的API倾向于将错误表示为回调函数的特定参数,或使用一个附加的

[Effective JavaScript 笔记]第50条:迭代方法优于循环

"懒"程序员才是好程序员.复制和粘贴样板代码,一但代码有错误,或代码功能修改,那么程序在修改的时候,程序员需要找到所有相同功能的代码一处处进行修改.这会使人重复发明轮子,而且在别人看代码的时候无法在更高层次都看待问题的解决方案.太容易陷入细节. for循环 js中的for循环在进行一些细微变化时,可以引入不同的行为.编程的时候对于边界条件的判断往往会导致一些简单的错误.下面的一些for循环的细微变化导致边界条件的变化. for(var i=0;i<=n;i++){...} //包

[Effective JavaScript 笔记]第62条:在异步序列中使用嵌套或命名的回调函数

异步程序的操作顺序 61条讲述了异步API如何执行潜在的代价高昂的I/O操作,而不阻塞应用程序继续处理其他输入.理解异步程序的操作顺序刚开始有点混乱.例如,下面的代码会在打印"finished"之前打印"starting",即使这两个动作的程序源文件中以相反的顺序呈现. downloadAsync('file.txt',function(file){ console.log('finished'); }); console.log('starting'); down

[Effective JavaScript 笔记] 第6条:了解分号插入的局限

分号可以省略 js可以在语句结束不强制加分号.(建议还是添加,不添加分号往往会出现不易发现的BUG) function Point(x,y){ this.x=x||0; this.y=y||0; } Point.prototype.isOrigin=function(){ return this.x===0 && this.y===0 } 上面代码可以运行,是由于js可以自动插入分号,它是一种程序解析技术.能推断出某些上下文中省略的分号,然后有效地自动地将分号"插入"到