关于JavaScript中异步的一些理解

2019.5.28 修改了一些自己觉得不准确的说法

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

问题的开端是这样,有人来问我,我这个代码为什么一直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
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值