欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于JavaScript中异步的一些理解

程序员文章站 2024-02-09 18:10:04
...

这是由一个问题引发的阅读和思考。肯定还会有后续的。

问题的开端是这样,有人来问我,我这个代码为什么一直undefined,我说你让我看看。他的代码大概长这样:

var arr = [1, 2, 3];
for (var i = 0; i < arr.length; ++i) {
    $.ajax({
        type: 'GET',
        async: true,
        url: 'http://localhost:8080/test?' + 'test=' + arr[i],
        success: function (res) {
            console.log(arr[i] + res);
        },
        error: function (err) {
            console.log(err.toString());
        }
    });
}

我一开始怀疑是对象调用带来的隐式绑定的问题,为此还专门去翻了一下jQuery的源码,不过发现好像没啥关系。不过,这个简化版的代码也能解释一下为什么回调函数里的this指向jQuery内部:

// jquery-3.4.1.js
jQuery.extend( {
    // ...
    ajax: function( url, options ) {
        // ...
        s = jQuery.ajaxSetup( {}, options ),
        jqXHR = {
            //...
        };
        jqXHR.done( s.success );
    }
}

其实这只是一个简单的作用域问题。因为作用域问题,回调函数里每次绑定的都是最后一次的i(这里是4),所以输出结果不符合预期。至于解决方案,要么把var改成let,要么就用一个IIFE把ajax包在里面。

但是我回忆起自己的写法,突然意识到一件事,Array.prototype.forEach()竟然是同步的。这实在是出乎我的意料。

arr.forEach((item, index) => {
    $.ajax(//...);
});

国内没太多解释,去Stack Overflow和MDN转了一圈,发现了老外的说法。于是阅读了一下ECMA语言规范和MDN上给出的polyfill,发现forEach同步性的核心其实在这里:

Array.prototype.forEach = function(callback, thisArg) {
    var T, k;
    if (this == null) {
      throw new TypeError(' this is null or not defined');
    }
    var O = Object(this);
    var len = O.length >>> 0;
    if (typeof callback !== "function") {
      throw new TypeError(callback + ' is not a function');
    }
    if (arguments.length > 1) {
      T = thisArg;
    }
    k = 0;
    while (k < len) {
      var kValue;
      if (k in O) {
        kValue = O[k];
        callback.call(T, kValue, k, O); // 这是重点
      }
      k++;
    }
  };
}

这里每次都使用了一次call,相当于把当前的context传给了callback;而且,call就是直接的函数调用,以此确保了同步性。不过话说回来了,call的同步性的来源,其实还是因为JavaScript中的函数具有原子性,在函数的内部是同步的。

一提到JavaScript,我们就会想起单线程、非阻塞、事件驱动,以及各种各样的回调,以致于让人觉得异步是JavaScript的内部机制。事实上,在ES6的Promise(以及任务队列)之前,JavaScript从语言机制上并没有什么异步,异步机制(事件循环,或者说事件队列)是由宿主环境提供的(比如浏览器、Node)。所以,我觉得这语言从本质上来说应当是同步的。不过,JavaScript的一些语言机制,比如全局变量,DOM共享状态,单线程非阻塞的事件模型,天生就让它具有对异步的亲和性。

接着说异步的事情。异步是什么?同步和异步的区别在于对将来发生的事件的处理方式。异步是对未来的“封装”,用专业一点的说法,叫continuation。其中最著名的实现大概要算回调。我们在调用一个函数的时候,都会假定参数是现在值(now value),表示的是调用时的状态;而回调则将未来值(future value)封装成了类似于现在值的形式,让我们可以直接调用。传递回调函数时, 我们是把“未来”交给了“现在”。

如果要打个比方的话(我并不是很喜欢打比方),异步有点像锦囊;出发前把锦囊交给将领,让他在遇到某某情况时打开锦囊,依计行事。异步也是如此,遇到交互、计时器、IO时,把回调推入事件队列,在下一个tick的时候依次出队处理。至于同步么,就是不管发生什么,一切按照计划行事。

从这个层面来说,异步提高了语言的维度,增加了时间这一维度,提高了语言的抽象能力,似乎是比同步更高级(也更灵活)的表现形式。

无论是同步的多线程,还是异步的多事件,都是为了解决并发问题,让任务能够像底层的指令一样并行。并发问题大致有三种表现形式:

  1. 非交互。这种情况下其实不存在异步带来的执行顺序问题,反正互不影响,做完就行。

  2. 交互。这种情况会共享全局变量,或者通过DOM进行状态交换。这种情况下,会出现竞态(race)问题。为了应对这种情况,一般有两种解决方案:

    1. 等待两者全部完成才继续进行的门(gate):

      var a, b;
      
      function foo(x) {
      	a = x * 2;
      	if (a && b) {
      		baz();
      	}
      }
      
      function bar(y) {
      	b = y * 2;
      	if (a && b) {
      		baz();
      	}
      }
      
      function baz() {
      	console.log( a + b );
      }
      
      // ajax(..) is some arbitrary Ajax function given by a library
      ajax( "http://some.url.1", foo );
      ajax( "http://some.url.2", bar );
      
    2. 先到先得,送完即止的闩(latch):

      var a;
      
      function foo(x) {
      	a = x * 2;
      	baz();
      }
      
      function bar(x) {
      	a = x / 2;
      	baz();
      }
      
      function baz() {
      	console.log( a );
      }
      
      // ajax(..) is some arbitrary Ajax function given by a library
      ajax( "http://some.url.1", foo );
      ajax( "http://some.url.2", bar );
      
  3. 协作。这种时候,考虑得更多的是如何分割一个很大的任务,以免长时间占用线程,造成假死。不要忘了,JavaScript是单线程的,一个复杂的事件对性能的影响是很大的。这种时候,可以开辟缓冲区等等来承接这些数据,类似C++、Java在进行IO时会创建的buffer数组。

但是,回调是有很大问题的。所谓的回调地狱,或者是金字塔灾难,其实完全取决于你认为回调是在空间里向下延伸还是向上延伸的(笑)。由于回调是将对程序的控制权交给另一个函数,其实也算是IoC了。不过,同步语言,比如Java的IoC,因为语言机制上的同步,让契约是可以确保正确、可控地履行的。而回调带来的不确定性,完全取决于调用回调的函数的实现,是对契约的破坏。这也算是回调的原罪吧。因此,就有了Promise的出现。有时候我也会恶趣味地想,是不是因为回调破坏了契约,而我们应当拥有契约精神,所以才给它起了’Promise’这个名字呢?

参考资料(不分先后)

  1. Async JavaScript
  2. ECMAScript® Language Specification
  3. Is JavaScript synchronous or asynchronous? What the hell is a promise?
  4. JavaScript, Node.js: is Array.forEach asynchronous?
  5. Numbers in JavaScript
  6. You Don’t Know JS: Async & Performance
相关标签: JavaScript 异步