javascript异步编程
什么是异步编程?为什么会需要异步编程?
所谓”异步”,简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
前端的异步任务比较常见的包括:
- IO操作,下载,上传等
- 用户操作,点击,滑动等
- 网络请求,ajax请求,图片加载等
前端为什么需要异步操作?主要的是取决于javascript的单线程特性,单线程在执行任务只能是队列的形式,很容易导致任务阻塞的现象。
很常见的就是,由于任务阻塞导致的页面假死的现象。于是,采用异步的方式来处理一些与CPU处理不相关的任务和需要消耗大量计算的任务。那为什么不向后端语言一样采用多线程的处理方式?多线程带来效率的提高,同时带来的状态同步的问题,而对于DOM操作,当一个线程进行DOM的插入操作,而另外一个线程进行删除操作,很容易造成浏览器执行的问题。尽管html5已经提供了开启多线程的方法,但也仅限于处理计算任务,而不能进行DOM操作。
异步编程带来的问题
- 回调问题
- 流程控制问题
- 错误处理
回调问题
- 串行依赖问题
比如说采购通采购页面,商品数据依赖于分类数据,获取商品数据就需要嵌套在获取分类数据的回调函数中,还可能有更深更多的依赖嵌套,写出的代码可读性和可维护性大大降低,这就是传说中的“回调地狱”。
$.ajax({
url: '',
success: function (data) {
$.ajax({
url: '',
success: function (data) {
$.ajax({
url: '',
success: function (data) {
...
}
})
}
})
}
})
- 并行依赖问题
还是采购页面,商品数据的初始化,依赖于分类数据,同时还依赖于购物车数据和经常购买数据,而分类数据、购物车数据、经常购买数据之间又不存在依赖关系。如果通过层层嵌套强行通过串行的方式解决这样的依赖问题,且不论代码的问题,这样完全丢掉了异步的优势,大大降低了执行的效率。
流程控制问题
流程控制问题,有一个典型的例子:
var i = 0;
while(i < 10) {
setTimeout(function() {
console.log(i);
},0);
i++;
}
由于异步的问题,这里的流程控制失去了作用。
错误处理
同流程控制类似,这里的异常捕获也无法工作。
try {
setTimeout(function () {
throw new Error('hello world');
},0);
} catch (e) {
console.log(e);
}
异步处理常见模式
- 事件发布/订阅
- Promise/Deferred
- Generator + co
- async + await
事件发布/订阅
事件的发布/订阅常见的是DOM的事件,例如jquery中的on()和trigger(),剥离浏览器事件的冒泡,默认事件,简单实现一个事件发布/订阅模式:
function Event() {
this.events = new Object();
}
Event.prototype.on = function(eventName, callback) {
if(!this.events[eventName]) {
this.events[eventName] = new Array();
}
this.events[eventName].push(callback);
}
Event.prototype.emit = function(eventName) {
var i = 0,
args = null;
if (!this.events[eventName]) {
return;
}
args = [].slice(arguments, 1);
while (i < this.events[eventName].length) {
this.events[eventName][i++].apply(null, args);
}
}
对于之前的,例如商品依赖于分类的嵌套问题,就可以利用事件/发布进行一定程度的解耦,避免回调地狱这种不便的书写方式。
var event = new Event();
event.on('product', function(data) {
//...
});
event.on('category', function(data){
//...
event.emit('product',data);
});
$.ajax({
url: '',
success: function(data) {
event.emit('category',data);
}
})
而对于并行依赖问题,事件/订阅也有相应的处理方式:
function Event() {
this.events = new Object();
this.allDatas = null;
this.all = null;
}
Event.prototype.trigger = function (eventName, data) {
this.all(eventName, data);
};
Event.prototype.proxyAll = function () {
var args, callback;
args = [].slice.call(arguments, 0, -1);
callback = [].slice.call(arguments, -1)[0];
this.all = function (eventName, data) {
var index = null;
if (!this.allDatas) {
this.allDatas = [];
}
index = args.indexOf(eventName);
if (index === -1) {
return;
}
this.allDatas[index] = data;
if (this.allDatas && this.allDatas.length === args.length && this.allDatas[0]) {
callback.apply(this, this.allDatas);
}
};
};
var events = new Event();
events.proxyAll('A', 'B', 'C', function (a, b, c) {
console.log('A:' + a);
console.log('B:' + b);
console.log('C:' + c);
});
setTimeout(function () {
events.trigger('C', '1');
}, 100);
setTimeout(function () {
events.trigger('B', '2');
}, 200);
setTimeout(function () {
events.trigger('A', '3');
}, 3000);
对于错误处理,比如nodejs开发中,默认将错误作为第一个参数,进行传递, 将上面的例子修改一下,
Promise/Deferred
- 什么是Promise
promise是把类似的异步处理对象和处理规则进行规范化,并按照统一的接口来实现,而采取规定之外的写法都是错误的。这和nodejs的规范第一个参数是错误参数类似。
- 异步处理对象
Promise规定异步对象只有三种状态Fullfilled(完成)、Rejecte(失败)d、Pending(初始化)。promise对象的状态,从Pending转换为Fulfilled或Rejected之后, 这个promise对象的状态就不会再发生任何变化。