JavaScript同步執(zhí)行阻塞UI線程渲染的解決方案

每到周末就喜歡自己折騰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)該也有這方面的原因吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容