当前位置: 首页>编程语言>正文

JavaScript同步执行阻塞UI线程渲染的解决方案

每到周末就喜欢自己折腾demo,早就注册了简书,但一直没时间在上面写东西,今天正好遇到点问题,同步在这里记录下。

在之前的项目VueNode中有一个优惠券列表页,当时使用点击按钮加载更多的方式实现的,但是这里有个问题:如果用户网络环境不好,或者正好当时并发太大,即使是每次只加载10条数据,依然会出现延迟,这个时候在页面中加一个loading效果,提示用户数据正在加载,体验会更好些,于是就先着手写一些demo,本以为只是一个loading图片定位布局外加蒙层的效果,没想到却是一波三折。。。

目使用Vue开发,所有的数据请求都是基于VueResource的(其实就是类似jQuery的ajax异步),本来是想提高代码重用性,封装一个getData的函数,这样就省下每次写一些结构性的代码,只需要传入参数,然后return返回结果即可,测试代码如下:

function getData () {
    var res;
    $.ajax({
        type: 'get',
        async: false, // 因为要return res,所以这里使用同步
        url: './test.php?a=1&b=2',
        dataType: 'json',
        success: function (data) {
            res = data;
        }
    });
    return res;
}

为了模拟数据加载慢的场景,PHP输出延迟了2s:

sleep(2);
echo json_encode($_GET);

最后执行点击事件:

$('#btn').click(function () {
    $('#loading').show();
    console.log(getData());
    $('#loading').hide();
});

本以为这样就可以了,但是在浏览器里执行的时候,loading图根本没有出现,只是过了2s在控制台打印出了输出结果,郁闷。。。

然后仔细看了下代码,初步判定是JS线程阻塞了UI线程,所以loading图无法被渲染到浏览器中显示,这也是我们平时将JS脚本放到页面底部,并且在页面加载过程中尽量不操作DOM的原因。

我的初始目的是封装一个公用函数去加载数据,但是鉴于同步加载可能导致的上述问题,只能放弃改用异步。但是异步加载又可能会导致另外的问题:回调地狱,解决当前的需求,只需一次回调,问题不大,但是后期需要通过异步加载很多数据,并且每一次异步加载都需要上一次加载的结果作为条件,就不好办了,如下代码:

setTimeout(function () {
    // do something
    setTimeout(function () {
        // do something
        setTimeout(function () {
            // do something
            // ... 无限回调
        });
    });
});

网上找了下资料,看看jQuery解决多次异步回调的方法,还真有:jQuery在1.5版本之后,引入了Deferred对象,提供的很方便的异步机制,所以,整理以上代码如下:

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对象的resolve方法传入一个任意类型的参数,并且这个参数可以在done方法中拿到,最后我们异步请求来的数据就可以正常返回了,而且不会阻塞UI线程。

本来问题到此为止算是解决了,但是忽然想到:现在的异步代码是运行在浏览器,主要是操作DOM,所以有jQuery帮忙解决多层异步回调,如果是运行在服务端的Node,肿么办,毕竟这种场景很常见。

ES6提供了异步对象Promise,其中Promise.all()就是来解决多层回调问题的,之前我在VueNode项目中获取首页数据就用到了这种方法,代码片段如下:

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,都是这样一个过程:先让某一段代码执行着,等有结果了再来通知我,即使永远没有结果,也不要紧,不会阻塞其他代码的执行。这个不是很像发布订阅模式吗?Vue中的$emit、$on就是发布订阅,主要解决多个组件之间数据交互的。所以来测试下,还是实现以上的loading效果:

// 发布订阅类
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);
        }
    });
});

试着运行,果然可以。

总结:
(1)Ajax默认是异步的,所以最好不要强制使用同步,即使涉及到多层嵌套,也有很多方案可以解决;
(2)使用JS操作DOM的时候要时刻注意JS会阻塞UI渲染,即使要操作DOM,也要确保其他UI渲染操作没有同时执行;
(3)如果习惯了异步,真的是很好用,Node很流行应该也有这方面的原因吧。


https://www.xamrdz.com/lan/5vz1848937.html

相关文章: