每到周末就喜歡自己折騰demo,早就注冊(cè)了簡(jiǎn)書,但一直沒時(shí)間在上面寫東西,今天正好遇到點(diǎn)問題,同步在這里記錄下。
在之前的項(xiàng)目VueNode中有一個(gè)優(yōu)惠券列表頁,當(dāng)時(shí)使用點(diǎn)擊按鈕加載更多的方式實(shí)現(xiàn)的,但是這里有個(gè)問題:如果用戶網(wǎng)絡(luò)環(huán)境不好,或者正好當(dāng)時(shí)并發(fā)太大,即使是每次只加載10條數(shù)據(jù),依然會(huì)出現(xiàn)延遲,這個(gè)時(shí)候在頁面中加一個(gè)loading效果,提示用戶數(shù)據(jù)正在加載,體驗(yàn)會(huì)更好些,于是就先著手寫一些demo,本以為只是一個(gè)loading圖片定位布局外加蒙層的效果,沒想到卻是一波三折。。。
目使用Vue開發(fā),所有的數(shù)據(jù)請(qǐng)求都是基于VueResource的(其實(shí)就是類似jQuery的ajax異步),本來是想提高代碼重用性,封裝一個(gè)getData的函數(shù),這樣就省下每次寫一些結(jié)構(gòu)性的代碼,只需要傳入?yún)?shù),然后return返回結(jié)果即可,測(cè)試代碼如下:
function getData () {
var res;
$.ajax({
type: 'get',
async: false, // 因?yàn)橐猺eturn res,所以這里使用同步
url: './test.php?a=1&b=2',
dataType: 'json',
success: function (data) {
res = data;
}
});
return res;
}
為了模擬數(shù)據(jù)加載慢的場(chǎng)景,PHP輸出延遲了2s:
sleep(2);
echo json_encode($_GET);
最后執(zhí)行點(diǎn)擊事件:
$('#btn').click(function () {
$('#loading').show();
console.log(getData());
$('#loading').hide();
});
本以為這樣就可以了,但是在瀏覽器里執(zhí)行的時(shí)候,loading圖根本沒有出現(xiàn),只是過了2s在控制臺(tái)打印出了輸出結(jié)果,郁悶。。。
然后仔細(xì)看了下代碼,初步判定是JS線程阻塞了UI線程,所以loading圖無法被渲染到瀏覽器中顯示,這也是我們平時(shí)將JS腳本放到頁面底部,并且在頁面加載過程中盡量不操作DOM的原因。
我的初始目的是封裝一個(gè)公用函數(shù)去加載數(shù)據(jù),但是鑒于同步加載可能導(dǎo)致的上述問題,只能放棄改用異步。但是異步加載又可能會(huì)導(dǎo)致另外的問題:回調(diào)地獄,解決當(dāng)前的需求,只需一次回調(diào),問題不大,但是后期需要通過異步加載很多數(shù)據(jù),并且每一次異步加載都需要上一次加載的結(jié)果作為條件,就不好辦了,如下代碼:
setTimeout(function () {
// do something
setTimeout(function () {
// do something
setTimeout(function () {
// do something
// ... 無限回調(diào)
});
});
});
網(wǎng)上找了下資料,看看jQuery解決多次異步回調(diào)的方法,還真有:jQuery在1.5版本之后,引入了Deferred對(duì)象,提供的很方便的異步機(jī)制,所以,整理以上代碼如下:
function getData () {
var defer = $.Deferred();
$.ajax({
type: 'get',
async: true,
url: './test.php?a=1&b=2',
dataType: 'json',
success: function (data) {
defer.resolve(data);
}
});
return defer.promise();
}
$('#btn').click(function () {
$('#loading').show();
$.when(getData()).done(function (data) {
console.log(data);
$('#loading').hide();
});
});
defer.resolve(data),Deferred對(duì)象的resolve方法傳入一個(gè)任意類型的參數(shù),并且這個(gè)參數(shù)可以在done方法中拿到,最后我們異步請(qǐng)求來的數(shù)據(jù)就可以正常返回了,而且不會(huì)阻塞UI線程。
本來問題到此為止算是解決了,但是忽然想到:現(xiàn)在的異步代碼是運(yùn)行在瀏覽器,主要是操作DOM,所以有jQuery幫忙解決多層異步回調(diào),如果是運(yùn)行在服務(wù)端的Node,腫么辦,畢竟這種場(chǎng)景很常見。
ES6提供了異步對(duì)象Promise,其中Promise.all()就是來解決多層回調(diào)問題的,之前我在VueNode項(xiàng)目中獲取首頁數(shù)據(jù)就用到了這種方法,代碼片段如下:
let bannerData = new Promise((resolve, reject) => {
// do something
resolve(data);
});
let hotCoupon = new Promise((resolve, reject) => {
// do something
resolve(data);
});
Promise.all([bannerData, hotCoupon]).then((res) => {
// do something
});
甚至我們可以用ES7新推出的async/await這種方法,代碼更直觀。
不管是jQuery的Deferred還是ES6的Promise,都是這樣一個(gè)過程:先讓某一段代碼執(zhí)行著,等有結(jié)果了再來通知我,即使永遠(yuǎn)沒有結(jié)果,也不要緊,不會(huì)阻塞其他代碼的執(zhí)行。這個(gè)不是很像發(fā)布訂閱模式嗎?Vue中的$emit、$on就是發(fā)布訂閱,主要解決多個(gè)組件之間數(shù)據(jù)交互的。所以來測(cè)試下,還是實(shí)現(xiàn)以上的loading效果:
// 發(fā)布訂閱類
class PubSub {
constructor () {
this.eventList = {};
}
on (eventName, callback) {
if (this.eventList[eventName] === undefined) {
this.eventList[eventName] = [];
}
this.eventList[eventName].push(callback);
}
emit (eventName, ...args) {
if (this.eventList[eventName] === undefined) {
return false;
}
this.eventList[eventName].forEach((item) => {
item.apply(null, args);
});
}
}
const pubsub = new PubSub();
pubsub.on('loading', (...args) => {
console.log(args);
$('#loading').hide();
});
$('#btn').click(function () {
$('#loading').show();
$.ajax({
type: 'get',
async: true,
url: './test.php?a=1&b=2',
dataType: 'json',
success: function (data) {
pubsub.emit('loading', data);
}
});
});
試著運(yùn)行,果然可以。
總結(jié):
(1)Ajax默認(rèn)是異步的,所以最好不要強(qiáng)制使用同步,即使涉及到多層嵌套,也有很多方案可以解決;
(2)使用JS操作DOM的時(shí)候要時(shí)刻注意JS會(huì)阻塞UI渲染,即使要操作DOM,也要確保其他UI渲染操作沒有同時(shí)執(zhí)行;
(3)如果習(xí)慣了異步,真的是很好用,Node很流行應(yīng)該也有這方面的原因吧。