注意: 我们在这里使用带引号的“进程”,因为它们不是计算机科学意义上的真正的操作系统级别的进程。它们是虚拟进程,或者说任务,表示一组逻辑上关联,串行顺序的操作。我们将简单地使用“进程”而非“任务”,因为在术语层面它与我们讨论的概念的定义相匹配。

    第一个“进程”将响应当用户向下滚动页面时触发的事件(发起取得新内容的Ajax请求)。第二个“进程”将接收返回的Ajax应答(将内容绘制在页面上)。

    显然,如果用户向下滚动的足够快,你也许会看到在第一个应答返回并处理期间,有两个或更多的onscroll事件被触发,因此你将使onscroll事件和Ajax应答事件迅速触发,互相穿插在一起。

    并发是当两个或多个“进程”在同一时间段内同时执行,无论构成它们的各个操作是否 并行地(在同一时刻不同的处理器或内核)发生。你可以认为并发是“进程”级别的(或任务级别)的并行机制,而不是操作级别的并行机制(分割进程的线程)。

    注意: 并发还引入了这些“进程”间彼此互动的概念。我们稍后会讨论它。

    在一个给定的时间跨度内(用户可以滚动的那几秒),让我们将每个独立的“进程”作为一系列事件/操作描绘出来:

    “线程”1 (onscroll事件):

    “线程”2 (Ajax应答事件):

    1. response 1
    2. response 2
    3. response 3
    4. response 4
    5. response 5
    6. response 6
    7. response 7

    一个onscroll事件与一个Ajax应答事件很有可能在同一个 时刻 都准备好被处理了。比如我们在一个时间线上描绘一下这些事件的话:

    1. onscroll, request 1
    2. onscroll, request 2 response 1
    3. onscroll, request 3 response 2
    4. response 3
    5. onscroll, request 4
    6. onscroll, request 5
    7. onscroll, request 6 response 4
    8. onscroll, request 7
    9. response 6
    10. response 5
    11. response 7

    但是,回到本章前面的事件轮询概念,JS一次只能处理一个事件,所以不是onscroll, request 2首先发生就是response 1首先发生,但是他们不可能完全在同一时刻发生。就像学校食堂的孩子们一样,不管他们在门口挤成什么样,他们最后都不得不排成一个队来打饭!

    让我们来描绘一下所有这些事件在事件轮询队列上穿插的情况:

    事件轮询队列:

    1. onscroll, request 1 <--- 进程1开始
    2. onscroll, request 2
    3. response 1 <--- 进程2开始
    4. onscroll, request 3
    5. response 2
    6. response 3
    7. onscroll, request 4
    8. onscroll, request 5
    9. onscroll, request 6
    10. response 4
    11. onscroll, request 7 <--- 进程1结束
    12. response 6
    13. response 5
    14. response 7 <--- 进程2结束

    “进程1”和“进程2”并发地运行(任务级别的并行),但是它们的个别事件在事件轮询队列上顺序地运行。

    顺便说一句,注意到response 6response 5没有按照预想的顺序应答吗?

    单线程事件轮询是并发的一种表达(当然还有其他的表达,我们稍后讨论)。

    举个例子:

    foo()bar()是两个并发的“进程”,而且它们被触发的顺序是不确定的。但对我们的程序的结构来讲它们的触发顺序无关紧要,因为它们的行为相互独立所以不需要互动。

    这不是一个“竞合状态”Bug,因为这段代码总能够正确工作,与顺序无关。

    更常见的是,通过作用域和/或DOM,并发的“进程”将有必要间接地互动。当这样的互动将要发生时,你需要协调这些互动行为来防止前面讲述的“竞合状态”。

    这里是两个由于隐含的顺序而互动的并发“进程”的例子,它 有时会出错

    1. var res = [];
    2. function response(data) {
    3. res.push( data );
    4. }
    5. // ajax(..) 是某个包中任意的Ajax函数
    6. ajax( "http://some.url.1", response );
    7. ajax( "http://some.url.2", response );

    并发的“进程”是那两个将要处理Ajax应答的response()调用。它们谁都有可能先发生。

    假定我们期望的行为是res[0]拥有"http://some.url.1"调用的结果,而res[1]拥有"http://some.url.2"调用的结果。有时候结果确实是这样,而有时候则相反,要看哪一个调用首先完成。很有可能,这种不确定性是一个“竞合状态”Bug。

    注意: 在这些情况下要极其警惕你可能做出的主观臆测。比如这样的情况就没什么不寻常:一个开发者观察到"http://some.url.2"的应答“总是”比"http://some.url.1"要慢得多,也许有赖于它们所做的任务(比如,一个执行数据库任务而另一个只是取得静态文件),所以观察到的顺序看起来总是所期望的。就算两个请求都发到同一个服务器,而且它故意以确定的顺序应答,也不能 真正 保证应答回到浏览器的顺序。

    所以,为了解决这样的竞合状态,你可以协调互动的顺序:

    1. function response(data) {
    2. res[0] = data;
    3. }
    4. else if (data.url == "http://some.url.2") {
    5. res[1] = data;
    6. }
    7. }
    8. // ajax(..) 是某个包中任意的Ajax函数
    9. ajax( "http://some.url.1", response );
    10. ajax( "http://some.url.2", response );

    无论哪个Ajax应答首先返回,我们都考察它的data.url(当然,假设这样的数据会从服务器返回)来找到应答数据应当在res数组中占有的位置。res[0]将总是持有"http://some.url.1"的结果,而res[1]将总是持有"http://some.url.2"的结果。通过简单的协调,我们消除了“竞合状态”的不确定性。

    这个场景的同样道理可以适用于这样的情况:多个并发的函数调用通过共享的DOM互动,比如一个在更新<div>的内容而另一个在更新<div>的样式或属性(比如一旦DOM元素拥有内容就使它变得可见)。你可能不想在DOM元素拥有内容之前显示它,所以协调工作就必须保证正确顺序的互动。

    没有协调的互动,有些并发的场景 总是出错(不仅仅是 有时)。考虑下面的代码:

    1. var a, b;
    2. function foo(x) {
    3. a = x * 2;
    4. baz();
    5. }
    6. function bar(y) {
    7. b = y * 2;
    8. baz();
    9. }
    10. function baz() {
    11. console.log(a + b);
    12. }
    13. // ajax(..) 是某个包中任意的Ajax函数
    14. ajax( "http://some.url.1", foo );
    15. ajax( "http://some.url.2", bar );

    在这个例子中,不管foo()bar()谁先触发,总是会使baz()运行的太早了(ab之一还是空的时候),但是第二个baz()调用将可以工作,因为ab将都是可用的。

    有许多不同的方法可以解决这个状态。这是简单的一种:

    baz()调用周围的if (a && b)条件通常称为“大门”,因为我们不能确定ab到来的顺序,但在打开大门(调用baz())之前我们等待它们全部到达。

    考虑这段有问题的代码:

    1. var a;
    2. function foo(x) {
    3. a = x * 2;
    4. baz();
    5. }
    6. function bar(x) {
    7. a = x / 2;
    8. baz();
    9. }
    10. console.log( a );
    11. }
    12. // ajax(..) 是某个包中任意的Ajax函数
    13. ajax( "http://some.url.1", foo );
    14. ajax( "http://some.url.2", bar );

    不管哪一个函数最后触发(foo()bar()),它不仅会覆盖前一个函数对a的赋值,还会重复调用baz()(不太可能是期望的)。

    所以,我们可以用一个简单的门闩来协调互动,仅让第一个过去:

    1. var a;
    2. function foo(x) {
    3. if (a == undefined) {
    4. a = x * 2;
    5. baz();
    6. }
    7. }
    8. function bar(x) {
    9. if (a == undefined) {
    10. a = x / 2;
    11. baz();
    12. }
    13. }
    14. function baz() {
    15. console.log( a );
    16. }
    17. // ajax(..) 是某个包中任意的Ajax函数
    18. ajax( "http://some.url.1", foo );
    19. ajax( "http://some.url.2", bar );

    if (a == undefined)条件仅会让foo()bar()中的第一个通过,而第二个(以及后续所有的)调用将会被忽略。第二名什么也得不到!

    注意: 在所有这些场景中,为了简化说明的目的我们都用了全局变量,这里我们没有任何理由需要这么做。只要我们讨论中的函数可以访问变量(通过作用域),它们就可以正常工作。依赖于词法作用域变量(参见本丛书的 作用域与闭包 ),和这些例子中实质上的全局变量,是这种并发协调形式的一个明显的缺点。在以后的几章中,我们会看到其他的在这方面干净得多的协调方法。

    另一种并发协调的表达称为“协作并发”,它并不那么看重在作用域中通过共享值互动(虽然这依然是允许的!)。它的目标是将一个长时间运行的“进程”打断为许多步骤或批处理,以至于其他的并发“进程”有机会将它们的操作穿插进事件轮询队列。

    举个例子,考虑一个Ajax应答处理器,它需要遍历一个很长的结果列表来将值变形。我们将使用Array#map(..)来让代码短一些:

    1. var res = [];
    2. // `response(..)`从Ajax调用收到一个结果数组
    3. function response(data) {
    4. // 连接到既存的`res`数组上
    5. res = res.concat(
    6. // 制造一个新的变形过的数组,所有的`data`值都翻倍
    7. data.map( function(val){
    8. return val * 2;
    9. } )
    10. );
    11. }
    12. // ajax(..) 是某个包中任意的Ajax函数
    13. ajax( "http://some.url.1", response );

    如果"http://some.url.1"首先返回它的结果,整个结果列表将会一次性映射进res。如果只有几千或更少的结果记录,一般来说不是什么大事。但假如有1千万个记录,那么就可能会花一段时间运行(在强大的笔记本电脑上花几秒钟,在移动设备上花的时间长得多,等等)。

    当这样的“处理”运行时,页面上没有任何事情可以发生,包括不能有另一个response(..)调用,不能有UI更新,甚至不能有用户事件比如滚动,打字,按钮点击等。非常痛苦。

    所以,为了制造协作性更强、更友好而且不独占事件轮询队列的并发系统,你可以在一个异步批处理中处理这些结果,在批处理的每一步都“让出”事件轮询来让其他等待的事件发生。

    这是一个非常简单的方法:

    我们以每次最大1000件作为一个块儿处理数据。这样,我们保证每个“进程”都是短时间运行的,即便这意味着会有许多后续的“进程”,在事件轮询队列上的穿插将会给我们一个响应性(性能)强得多的网站/应用程序。

    当然,我们没有对任何这些“进程”的顺序进行互动协调,所以在res中的结果的顺序是不可预知的。如果要求顺序,你需要使用我们之前讨论的互动技术,或者在本书后续章节中介绍的其他技术。

    我们使用setTimeout(..0)(黑科技)来异步排程,基本上它的意思是“将这个函数贴在事件轮询队列的末尾”。

    注意: 从技术上讲,setTimeout(..0)没有直接将一条记录插入事件轮询队列。计时器将会在下一个运行机会将事件插入。比如,两个连续的setTimeout(..0)调用不会严格保证以调用的顺序被处理,所以我们可能看到各种时间偏移的情况,使这样的事件的顺序是不可预知的。在Node.js中,一个相似的方式是。不管那将会有多方便(而且通常性能更好),(还)没有一个直接的方法可以横跨所有环境来保证异步事件顺序。我们会在下一节详细讨论这个话题。