关于异步编程

javascript异步编程

什么是异步编程?为什么会需要异步编程?

所谓”异步”,简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

前端的异步任务比较常见的包括:

  • IO操作,下载,上传等
  • 用户操作,点击,滑动等
  • 网络请求,ajax请求,图片加载等

前端为什么需要异步操作?主要的是取决于javascript的单线程特性,单线程在执行任务只能是队列的形式,很容易导致任务阻塞的现象。
很常见的就是,由于任务阻塞导致的页面假死的现象。于是,采用异步的方式来处理一些与CPU处理不相关的任务和需要消耗大量计算的任务。那为什么不向后端语言一样采用多线程的处理方式?多线程带来效率的提高,同时带来的状态同步的问题,而对于DOM操作,当一个线程进行DOM的插入操作,而另外一个线程进行删除操作,很容易造成浏览器执行的问题。尽管html5已经提供了开启多线程的方法,但也仅限于处理计算任务,而不能进行DOM操作。

异步编程带来的问题

  • 回调问题
  • 流程控制问题
  • 错误处理

回调问题

  1. 串行依赖问题

比如说采购通采购页面,商品数据依赖于分类数据,获取商品数据就需要嵌套在获取分类数据的回调函数中,还可能有更深更多的依赖嵌套,写出的代码可读性和可维护性大大降低,这就是传说中的“回调地狱”。

$.ajax({
    url: '',
    success: function (data) {
        $.ajax({
            url: '',
            success: function (data) {
                $.ajax({
                    url: '',
                    success: function (data) {
                        ...
                    }
                })
            }
        })
    }
})
  1. 并行依赖问题

还是采购页面,商品数据的初始化,依赖于分类数据,同时还依赖于购物车数据和经常购买数据,而分类数据、购物车数据、经常购买数据之间又不存在依赖关系。如果通过层层嵌套强行通过串行的方式解决这样的依赖问题,且不论代码的问题,这样完全丢掉了异步的优势,大大降低了执行的效率。

流程控制问题

流程控制问题,有一个典型的例子:

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

  1. 什么是Promise

promise是把类似的异步处理对象和处理规则进行规范化,并按照统一的接口来实现,而采取规定之外的写法都是错误的。这和nodejs的规范第一个参数是错误参数类似。

  • 异步处理对象
    Promise规定异步对象只有三种状态Fullfilled(完成)、Rejecte(失败)d、Pending(初始化)。promise对象的状态,从Pending转换为Fulfilled或Rejected之后, 这个promise对象的状态就不会再发生任何变化。