第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. Fling——K

    K. Fling Fling is a kind of puzzle games available on phone.This game is played on a board with 7 ro ...

  2. jquery输入框按下回车提交表单

    jQuery on()方法是官方推荐的绑定事件的一个方法 $('#password').on('keydown', function(e) { // 短路语法,当e.keyCode == 13成立的时 ...

  3. 关于padding与margin的区别

    代码一:全为padding. <!doctype html><html><head>    <meta charset="UTF-8"&g ...

  4. SQL 语句调用这个存储过程,生成顺序编码

    一直很讨厌存储过程,没想到今天帮了我大忙啊,或许会因为今天让我慢慢喜欢上存储过程吧,不多说了,切入正题 在使用数据库的时候,难免要在使用过程中进行删除的操作,如果是使用int类型的字段,令其自增长,这 ...

  5. 使用spring提供的LocalSessionFactoryBean来得到SessionFactory

    一直不明白,spring为什么可以通过注入LocalSessionFactoryBean的bean名称来得到SessionFactory,起初以为LocalSessionFactoryBean必然是S ...

  6. android之简单图形绘制

    首先编写MyView类 代码如下: package com.example.myhello; import android.content.Context; import android.graphi ...

  7. Gruntfile.js

    module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), clea ...

  8. 数位dp/记忆化搜索

    一.引例 #1033 : 交错和 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 给定一个数 x,设它十进制展从高位到低位上的数位依次是 a0, a1, ..., an  ...

  9. Unity3D 开发 之 JDK安装与环境变量配置

     安装JDK 选择安装目录 安装过程中会出现两次 安装提示 .第一次是安装 jdk ,第二次是安装 jre .建议两个都安装在同一个java文件夹中的不同文件夹中.(不能都安装在java文件夹的根目录 ...

  10. ThinkPHP 3.2.3 Widget 扩展的使用

    ThinkPHP3.2.3 手册中 Widget 扩展的地址是: http://www.kancloud.cn/manual/thinkphp/1862 Widget 扩展一般用于页面组件的扩展,和自 ...