遇到了同步Ajax引起的UI線程阻塞問題,在此記錄一下。
事情起因是這樣的,因為頁面上有多個相似的異步請求動作,本着提高代碼可重用性的原則,我封裝了一個名為getData的函數,它接收不同參數,只負責獲取數據,然后把數據return。基本的邏輯剝離出來是這樣的:
var result;
$.ajax({
url: "p.php",
async: false,
success: function(data) {
result = data;
}
});
return result;
}
var data = getData1();
alert(data);
});
接下來,要加另外一個功能,由於ajax請求有一定的耗時,所以我需要在發出請求前頁面有個loading效果,即顯示一張“正在加載”的gif圖片,想必大家也都見過。所以我的處理函數就變成了這樣:
$(".loadingicon").show();
var data = getData1();
$(".loadingicon").hide();
alert(data);
});
sleep(3);
echo ("aaaaaa");
?>
瀏覽器的渲染(UI)線程和js線程是互斥的,在執行js耗時操作時,頁面渲染會被阻塞掉。當我們執行異步ajax的時候沒有問題,但當設置為同步請求時,其他的動作(ajax函數后面的代碼,還有渲染線程)都會停止下來。即使我的DOM操作語句是在發起請求的前一句,這個同步請求也會“迅速”將UI線程阻塞,不給它執行的時間。這就是代碼失效的原因。
setTimeout解決阻塞問題
既然明白了問題在哪里,我們就來針對性想辦法。為了不讓同步ajax請求阻塞線程,我想到了setTimeout,把請求的代碼放到sestTimeout中,讓瀏覽器重啟一個線程來操作,不就解決問題了嗎?於是乎,我的代碼就變成了這樣:
setTimeout(function() {
$.ajax({
url: "p.php",
async: false,
success: function(data) {
$(".loadingicon").hide();
alert(data);
}
});
}, 0);
});
setTimeout的第二個參數設為0,瀏覽器會在一個已設的最小時間后執行。不管三七二十一先運行起來看看。
結果loading圖片顯示出來了,但是!!!圖片怎么不動呢,我明明是一張動態gif圖。這個時候我很快就想到了,雖然同步請求延遲執行了,但是它執行期間還是會把UI線程給阻塞。這個阻塞相當牛逼,連gif圖片都不動了,看起來像一張靜態圖片一樣。
結論很明顯,setTimeout治標不治本,相當於把同步請求“稍稍”異步了一下,接下來還是會進入同步的噩夢,阻塞線程。方案失敗。
是時候用Deferred了
jQuery在1.5版本之后,引入了Deferred對象,提供的很方便的廣義異步機制。詳情可參看阮一峰老師的這篇文章http://www.ruanyifeng.com/blog/2011/08/a_detailed_explanation_of_jquery_deferred_object.html。
於是我用Deferred對象改寫了代碼,如下:
function getData3() {
$.ajax({
url: "p.php",
//async : false,
success: function(data) {
defer.resolve(data)
}
});
return defer.promise();
}
$(".btn3").click(function() {
$(".loadingicon").show();
$.when(getData3()).done(function(data) {
$(".loadingicon").hide();
alert(data);
});
});
可以看到我在ajax請求中去掉了async:false,也就是說,這個請求又是異步的了。另外請注意success函數中的這一 句:defer.resolve(data),Deferred對象的resolve方法可傳入一個參數,任意類型。這個參數可以在done方法中拿到, 所以我們異步請求來的數據就可以以這樣的方式來返回了。
至此,問題得到了解決。Deferred對象如此強大且方便,我們可以好好利用它。
最后說明:本篇文章摘自其他站點,僅用來作為一個記錄(自己的整理能力略弱:))