第63条建议使用工具函数downloadAllAsync接收一个URL数组并下载所有文件,结果返回一个存储了文件内容的数组,每个URL对应一个字符串。downloadAllAsync并不只有清理嵌套回调函数的好处,其主要好处是并行下载文件。我们可以在同一个事件循环中一次启动所有文件的下载,而不用等待每个文件完成下载。
并行逻辑是微妙的,很容易出错。下面有实现有一个隐藏的缺陷。

function downloadAllAsync(urls,onsuccess,onerror){
var result=[],length=urls.length;
if(length === 0){
setTimeout(onsuccess.bind(null,result),0);
}
urls.forEach(function(url){
downloadAsync(url,function(text){
if(result){
reslut.push(text);
if(result.length===urls.length){
onsuccess(result);
}
}
},function(error){
if(result){
result=null;
onerror(error);
}
});
});
}

这个函数有严重的错误,但首先让我们看看它是如何工作的。先确保如果数组是空的,则会使用空结果数组调用回调函数。如果不这样做,这两个回调函数将不会被调用,因为forEach循环是空的。接下来,遍历整个URL数组,为每个URL请求一个异步下载。每次下载成功,就将文件内容加入到result数组中。如果所有URL都被成功下载,使用result数组调用onsuccess回调函数。如果有任何失败的下载,使用错误值调用onerror回调函数。如果有多个下载失败,设置result数组为null,从而保证onerror只被调用一次,即在第一次错误发生时。
错误示例

var filenames=[
'huge.txt',
'tiny.txt',
'medium.txt'
];
downloadAllAsync(filenames,function(files){
console.log('Huge file:'+files[0].length);//tiny
console.log('Tiny file:'+files[1].length);//medium
console.log('Medium file:'+files[2].length);//huge
},function(error){
console.log('Error: '+error);
});

由于这些文件是并行下载的,事件可以以任意的顺序发生(因些被添加到应用程序事件序列)。例如,如果tiny.txt先下载完成,接下来是medium.txt文件,最后是buge.txt文件,则注册到downloadAllAsync的回调函数并不会按照它们被创建的顺序进行调用。但downloadAllAsync的实现是一旦下载完成就立即将中间结果保存在result数组的末尾。所以downloadAllAsync函数提供的保存下载文件内容的数组的顺序是未知的。这个API几乎不可用,因为无法确认哪个结果对应哪个文件。
程序的执行顺序不能保证与事件发生的顺序一致。
当一个应用程序依赖于特定的事件顺序才能正常工作时,这个程序会遭受数据竞争。数据竞争是指多个并发操作可以修改共享的数据结构,这取决于它们发生的顺序。数据竞争是真正棘手的错误。它们可能不会出现于特定的测试中,因为运行相同的程序两次,每次可能会得不到不同的结果。例如downloadAllAsync的使用者可能会对文件重新排序,基于的顺序是哪个文件可能会最先完成下载。

downloadAllAsync(filenames,function(files){
console.log('Huge file:'+files[2].length);
console.log('Tiny file:'+files[0].length);
console.log('Medium file:'+files[1].length);
},function(error){
console.log('Error: '+error);
});

在这种情况下大多数时候结果是相同的顺序,但偶尔由于改变了服务器负载均衡或网络缓存,文件可能不是期望的顺序。我们可以顺序下载文件,但也失去了并发的性能优势。
下面实现downloadAllAsync不依赖不可预期的事件执行顺序而总能提供预期结果。我们不将每个结果放置到数组末尾,而是存储在其原始的索引位置。

function downloadAllAsync(urls,onsuccess,onerror){
var result=[],length=urls.length;
if(length === 0){
setTimeout(onsuccess.bind(null,result),0);
return;
}
urls.forEach(function(url){
downloadAsync(url,function(text){
if(result){
reslut[i]=text;
if(result.length===urls.length){
onsuccess(result);
}
}
},function(error){
if(result){
result=null;
onerror(error);
}
});
});
}

该实现利用了forEach回调函数的第二个参数。第二个参数为当前迭代提供了数组索引。这也不正确。第51条描述数组更新的契约,即设置一个索引属性,总是确保数组的length属性值大于索引。假设有如下的一个请求。

downloadAllAsync(['huge.txt','medium.txt','tiny.txt']);

如果tiny.txt文件最先被下载,结果数组将获取索引为2的属性,这将导致result.length被更新为3。用户的success回调函数将被过早地调用,其参数为一个不完整的结果数组。
正确的实现应该是使用一个计数器来追踪正在进行的操作数量。

function downloadAllAsync(urls,onsuccess,onerror){
var pending=urls.length;
var result=[];
if(pending === 0){
setTimeout(onsuccess.bind(null,result),0);
return;
}
urls.forEach(function(url){
downloadAsync(url,function(text){
if(result){
reslut[i]=text;
pending--;
if(pending===0){
onsuccess(result);
}
}
},function(error){
if(result){
result=null;
onerror(error);
}
});
});
}

现在不论事件以什么样的顺序发生,pending计数器都能准确地指出何时所有的事件会被完成,并以适当的顺序返回完整的结果。

提示

  • js应用程序中的事件发生是不确定的,即顺序是不可预测的

  • 使用计数器避免并行操作中的数据竞争

[Effective JavaScript 笔记]第66条:使用计数器来执行并行操作的更多相关文章

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

    js有5种原始值类型:布尔值.数字.字符串.null和undefined. 用typeof检测一下: typeof true; //"boolean" typeof 2; //&q ...

  2. [Effective JavaScript 笔记] 第5条:避免对混合类型使用==运算符

    “1.0e0”=={valueOf:function(){return true;}} 是值是多少? 这两个完全不同的值使用==运算符是相等的.为什么呢?请看<[Effective JavaSc ...

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

    函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行.这使得富有表现力的高阶函数抽象如map和forEach成为可能.它也是js异步I/O方法的核心.与此同时,也可以将代码表示为字符串的形式 ...

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

    js函数有一个非凡的特性,即将其源代码重现为字符串的能力. (function(x){ return x+1 }).toString();//"function (x){ return x+ ...

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

    构建异步API的一种流行的替代方式是使用promise(有时也被称为deferred或future)模式.已经在本章讨论过的异步API使用回调函数作为参数. downloadAsync('file.t ...

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

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

  7. [Effective JavaScript 笔记]第65条:不要在计算时阻塞事件队列

    第61条解释了异步API怎样帮助我们防止一段程序阻塞应用程序的事件队列.使用下面代码,可以很容易使一个应用程序陷入泥潭. while(true){} 而且它并不需要一个无限循环来写一个缓慢的程序.代码 ...

  8. [Effective JavaScript 笔记]第46条:使用数组而不要使用字典来存储有序集合

    对象属性无序性 js对象是一个无序属性集合. var obj={}; obj.a=10; obj.b=30; 属性a和属性b并没有谁前谁后之说.for...in循环,先输出哪个属性都有可能.获取和设置 ...

  9. [Effective JavaScript 笔记]第45条:使用hasOwnProperty方法以避免原型污染

    之前的43条,44条讨论了属性的枚举,但都没有彻底地解决属性查找中原型污染的问题.看下面关于字典的一些操作 'zhangsan' in dict; dict.zhangsan; dict.zhangs ...

随机推荐

  1. nodejs怎么同步从一个数据库查询函数中返回一个值

    var sql=require('msnodesql'); var conn_str="Driver={SQL Server Native Client 11.0};Server={127. ...

  2. odoo XMLRPC 新库 OdooRPC 尝鲜

    无意中发现了python居然有了OdoRPC的库,惊喜之下赶紧尝试一番,比XMLRPC简洁了不少,机制看样子是利用的JsonRPC. #原文出自KevinKong的博客http://www.cnblo ...

  3. MySQL数据库基本数据类型

    1.整型 2. 浮点数类型和定点数类型 3.日期与时间类型 4.字符串类型 5. 二进制类型

  4. MySQL数据库基本指令

    对MySQL的指令不太熟悉,在此特别整理了一下一些常用的指令: 约定:大写字母写SQL关键字和函数名,小写字母写数据库.数据表和数据列的名字.(下述代码更新不同步,部分代码未依据此约定) 1 数据库的 ...

  5. c++用双向链表实现模板栈

      可直接编译运行,其中方法status为形象的显示出栈的结构: // visual stack , need define "cout<<" #include < ...

  6. Asp文件锁定脚本

    锁定指定文件 <% on error resume next server.ScriptTimeout= response.write "<form method=post> ...

  7. json转换数据后面参数要带ture,代码

    强大的PHP已经提供了内置函数:json_encode() 和 json_decode().很容易理解,json_encode()就是将PHP数组转换成Json.相反,json_decode()就是将 ...

  8. 打开网页自动弹出QQ对话框的实现办法

    Ian今天偶然进入一个公司的企业网站,然后QQ聊天窗口这里马上就弹出与那个公司客服聊天的窗口.怀着好奇的心态,Ian分析了该公司的网站源码,发现了实现网页弹出qq对话框的原理与实现方法,相信此时此刻你 ...

  9. Linux内核总结

    1.文件系统就是数据的存储结构,不要以为你的硬盘存储东西理所当然,没有文件系统,你存的只是0010101101100 2.内存管理是计算机运行时内存的分配和使用. 3.进程管理就是说每次执行一个程序都 ...

  10. Enum的使用

    在项目开发中经常会使用到枚举,下面将举个例子,展示枚举的使用,不说废话,直接上代码. package com.tom.enumTest; public enum StatusType { A(&quo ...