jQuery – Async OR Sync 异步与同步深层剖析

以前在用 jQuery 的时候,对于一些 Ajax 的调用,为了容易理解,一般都是使用同步模式:通过一个全局的配置将所有的 ajax 异步关掉(即开启同步模式,默认是异步)

$.ajaxSetup({ async: false });

然后像下面这个案例,我就可以用一种同步编程的模式来理解代码,比较方便:

$.post('/to_do_some/action/', form_data, function(response_data) {
    // deal with the response;
});

// Do something after ajax finished.
do_something();

如果是在同步开启的情况下,do_something 函数会在 $.post 结束,并且调用完 callback 来处理 response_data 完成之后,才被调用。

如果是异步模式的话,do_something 函数会在 $.post 这个 ajax 请求发出之后,立即执行。但是至于上面这个 ajax 执行情况如何,是生是死,之后结束的 callback 什么时候被执行,在这种情况下是完全没有保证的。

简而言之,异步模式下,不能在 do_something 函数中假设前面的 ajax 已经完成。但同步模式可以。


所以我一直都是直接全局开同步模式的。

这样做的话,好处是代码直观,容易理解。

但缺点也很明显:

首先是效率不高,没有办法利用到 javascript 语言本身的异步特性,即使是可以一起发送的 ajax 请求,也非得等前一个请求执行完才去发送,这样的话根据统筹方法的理论,连网络延时都要被重复等待很多次(假设有连续几个 ajax 发送);

再进一步,如果这个 ajax 请求被阻塞,整个页面就会被卡死一个相当长的时间,用户体验是很差的。

刚刚做项目的时候将页面上的很多请求载入都放在页面读取之后通过 ajax 异步去获取(当然这个做法增加了请求次数,未必是一个好的实践),但是,在这种情况下,却为本文的主题提供了一个极好的案例:

当我开始用同步模式去载入这个页面的时候,感觉非常缓慢(实际上 ajax 卡了 9秒才把页面载入完),我在 chrome 调试器中看了一下请求:

sync

结果是这个奇葩样子了,一个阶梯状的 ajax 请求,即使后台是多线程处理的,也得等着一个请求完了,数据来回跑一次,再发一次请求,客户端和本身可以重叠的网络延时拖了很多时间(还没算网络延时,因为这个案例是 localhost 已经酱紫了,如果是远程访问,可想而知)。


正因这个案例,使我有很大的动力试图去将一直以来习惯的 async:false 去掉,让自己习惯使用异步模式,当然,这里面有需要解决的问题,我们等下再说,先来看看我最后优化之后的图来说明问题:

async

可以看到,效率是明显地提高了。

但是这样做的情况下,有需要处理的问题,就是现在我们不能在 do_something 函数里面假设所有的 ajax 都处理完了,但事实上我们需要在所有 ajax 处理完之后再来调用这个 do_something,怎么办?

为了这个问题,我在 stackoverflow 找了一下(最初的目的只是找一下 $.post 当返回失败的时候怎么 callback,因为默认用法只有 $.ajax 方法有,而 $.get$.post 都没有),发现了一些基础的 jquery 技巧很有用却一直没有用到的。

就是关于 jquery 里面有一个重要的类,叫做 jqXHR,对于每次的 ajax 请求方法(包括 $.ajax, $.get, $.post, $.load 以及一些第三发的库)都会返回一个 jqXHR 对象,然后它支持 一系列的方法来对这个 ajax 事件注册 callback。

当然,这个句柄只有在调用了 ajax 方法之后才可以获得,然后在后面的代码再来在这个句柄之上注册 callbacks,如果是同步 ajax 的话,其实因为 ajax 已经早就跑完了,这个时候 jqXHR 上面的定义的方法就会在绑定 callback 的时候 直接执行之,整体的效果逻辑是一样的。

这个 jqXHR 上面可以注册的事件包括 done/fail/always/then 等,大可按需制定。

下面举一个例子来说明一下 jqXHR 对象绑定事件的方法:

$(function() {
    //$.ajaxSetup({ async: false });
    var xhr = $.get('.');
    xhr.done(function() { alert(2); });
    alert(1);
});

这样,如果开启同步模式,显然先会弹出 2 再弹出 1,反之,异步模式下应该是先出 1 再出 2;

好,问题再加深一层,回到我们现在的问题,我们现在有一大拉子的 ajax 调用,然后返回了一堆的 jqXHR 对象,我们完全不知道哪个先跑完哪个最后跑完,我们只想在确保所有这堆 jqXHR 跑完之后触发一个 callback,这样来保证前面的东西已经跑好。

这种情况,jquery 还提供了一个全局的方法 $.when,传入一系列的 jqXHR,然后返回一个具有 .done/.fail/.always/.then 等方法的 Promise 对象。

举个栗子:

$(function() {
    //$.ajaxSetup({ async: false });
    xhr1 = $.get('.');
    xhr2 = $.get('.');
    $.when(xhr1, xhr2).done(function() { alert(2); });
    alert(1);
});

酱紫,就可以在多个 ajax 确保完成之后去触发一个事件。

But wait..

这样传进去的话,我其实不知道一共有多少个 jqXHR 对象啊,如果这些不定个 jqXHR 放在一个数组里面,怎么调用?

我当时不会,在 stackoverflow 问了一下,原来是基础问题,用一个基础 javascirpt 方法 apply 即可解决:

$(function() {
    //$.ajaxSetup({ async: false });
    var xhrs = [];
    for(var i = 0; i < 10; ++i) {
        xhrs.push($.get('.'));
    }
    $.when.apply($, xhrs).done(function() { alert(2); });
    alert(1);
});

总结一下:

  1. 为了更好提高效率以及提高响应度,jquery 中尽可能使用异步模式,不要用同步模式;
  2. 异步模式的情况下,主循环在 ajax 一掉用完不等其执行完毕就直接向后跑了,在后面的代码不可假设 ajax 的 callback 已经被执行;
  3. ajax 方法会返回一个 jqXHR 对象;
  4. 为了在 ajax 执行之后来注册一个方法,可以在返回的 jqXHR 对象上面利用 .done/.fail/.always/.then 来注册 callbacks,这里可以大胆假设 ajax 的 callbacks 已经跑完;
  5. 如果有多个 jqXHR 并行,需要在整个 jqXHR 集合都跑完之后再触发 callback,可以用 $.when 方法来串联这些 jqXHR 对象,得到一个 Promise 对象然后再来注册 .done/.fail/.always/.then 这些事件。

【转载请附】愿以此功德,回向 >>

原文链接:https://www.huangwenchao.com.cn/2014/06/jquery-async-or-sync.html【jQuery – Async OR Sync 异步与同步深层剖析】

《jQuery – Async OR Sync 异步与同步深层剖析》有1个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注