每到周末就喜欢自己折腾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很流行应该也有这方面的原因吧。