10分钟理解JS异步编程(从概念到原理)

5,510 阅读7分钟

一、异步编程简介

  众所周知JavaScript语言执行环境是“单线程”(单线程,就是指一次只能完成一件任务,如果有多个任务就必须排队等候,前面一个任务完成,再执行后面一个任务)。这种“单线程”模式执行效率较低,任务耗时长。
  为了解决这个问题,提出了“异步模式”(异步模式,是指后一个任务不等前一个任务执行完就执行,每个任务有一个或多个回调函数)。
  异步模式使得JavaScript在处理事务时非常高效,但也带来很多问题,如异常处理困难、嵌套过深。

二、异步编程的发展

JavaScript异步编程从出现不断发展和精进,主要经历了以下4个阶段:

阶段1 传统callback回调函数

  回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字 callback,直译过来就是"重新调用"。虽然回调函数多用于异步编程,但带有回调函数的方法不一定是异步的。

// demo1(简单callback封装)
function successCallback() {
    console.log('callback');
}
function fn(successCallback) {
    console.log('这里表示执行了一大堆各种代码');

    // 其他代码执行完毕,最后执行回调函数
   successCallback instanceof Function && successCallback();
}
fn(successCallback);
// demo2(回调地域)

//callback hell
doSomethingAsync1(function(){
    doSomethingAsync2(function(){
        doSomethingAsync3(function(){
            doSomethingAsync4(function(){
                doSomethingAsync5(function(){
                    // code...
                });
            });
        });
    });
});

  可以发现一个问题,在回调函数嵌套层数不深的情况下,代码还算容易理解和维护,一旦嵌套层数加深,就会出现“回调金字塔”的问题,就像demo2那样,如果这里面的每个回调函数中又包含了很多业务逻辑的话,整个代码块就会变得非常复杂。从逻辑正确性的角度来说,上面这几种回调函数的写法没有任何问题,但是随着业务逻辑的增加和趋于复杂,这种写法的缺点马上就会暴露出来,想要维护它们实在是太痛苦了,这就是“回调地狱(callback hell)”。

阶段2 事件发布/订阅模式

  发布订阅模式,它定义了一种一对多的关系,可以使多个观察者对象对一个主题对象进行事件监听,当这个主题对象发生改变时,依赖的所有对象都会被通知到。

var PubSub = function(){
    this.handlers = {}; 
};
PubSub.prototype.subscribe = function(eventType, handler) {
    if (!(eventType in this.handlers)) {
        this.handlers[eventType] = [];
    }
    this.handlers[eventType].push(handler); //添加事件监听器
    return this;//返回上下文环境以实现链式调用
};
PubSub.prototype.publish = function(eventType) {
    var _args = Array.prototype.slice.call(arguments, 1);
    for (var i = 0, _handlers = this.handlers[eventType]; i < _handlers.length; i++) {
        _handlers[i].apply(this, _args);// 遍历事件监听器
    }
    return this;
};
var event = new PubSub;// 构造PubSub实例
event.subscribe('list', function(msg) {
    console.log(msg);
});
event.publish('list', {data: ['one,', 'two']});// Object {data: Array[2]}

阶段3 Deferred

  jQuery $.Deferred() 是一个构造函数,用来返回一个链式实用对象方法来注册多个回调,并且调用回调队列,传递任何同步或异步功能成功或失败的状态。成功回调done()中使用deferred.resolve,失败回调fail()中使用deferred.reject。

var deferred = $.Deferred();
$.ajax(url, {
    type: "post",
    dataType: "json",
    data: data
}).done(function(json) [
    if (json.code !== 0) {
        showError(json.message || "操作发生错误");
        deferred.reject();
    } else {
        deferred.resolve(json);
    }
}).fail(function() {
    showError("服务器错误,请稍后再试");
    deferred.reject();
}).always(function() {
    if (button) {
        button.prop("disabled", false);
    }
});
return deferred.promise();

阶段4 Promise

  Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
  所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
  使用Promise对象可以用同步操作的流程写法来表达异步操作,避免了层层嵌套的异步回调,代码也更加清晰易懂,方便维护,也可以捕捉异常。

function fn(num) {
  return new Promise(function(resolve, reject) {
    if (typeof num == 'number') {
      resolve();
    } else {
      reject();
    }
  })
  .then(function() {
    console.log('第1个then:参数是一个number值');
  })
  .then(null, function() {
    console.log('第2个then');
  })
}
fn('haha');
fn(1234);// 第1个then:参数是一个number值、第2个then

异步编程解决方案的优缺点:

callback回调 事件发布/订阅 Deferred Promise
优点 简单、容易理解 容易理解,可以绑定多个事件,每个事件可以指定多个回调函数 避免了层层嵌套的回调函数,有统一的API,使得控制异步操作更加容易 ES6将promise写进了语言标准,统一了使用的语法,使用简洁方便,比传统异步解决方案更合理强大
缺点 不利于代码的阅读和维护,会出现“回调地域”,而且每个任务只能指定一个回调函数 由事件驱动,运行流程变得很不清晰 状态不可逆,确定状态后再次调用resolve/reject对原状态不起任何作用 状态不可逆

三、异步编程的实现机制

我们从以下3个步骤来深入理解异步编程的实现机制:

1、创建一个容器,存放多个callback

/**
 * 1.创建容器list
 * 2.依次执行
 */
(function(root){
    var _ = {
    	callbacks: function(options) {
            var list = [];
            var index, length;
            // 依次执行
            var fire = function(data) {
            	index = 0;
            	length = list.length;
            	for (; index < length; index++) {
                    list[index].apply(data[0], data[1]);
            	}
            }
            var self = {
                add: function() {
                    var args = [].slice.call(arguments);
                    args.forEach(function(fn){
                    	if(toString.call(fn) == '[object Function]') {
                    		list.push(fn);
                    	}
                    })
                },
                fireWith: function(context, arguments) {
                    var args = [context, arguments];
                    fire(args)
                },
                // 传参
                fire: function() {
                    self.fireWith(this, arguments)
                }
            }
            return self;
    	}
    }
    root._ = _;
})(this);

// 调用
var callList = _.callbacks();
callList.add(function(){
    console.log('1111')
})
callList.add(function(){
    console.log('2222')
})
callList.fire();

2、容器添加状态控制(stopFalse、once、memory)

1)stopFalse某个回调函数执行完,后面停止执行的状态

var fire = function(data) {
    index = 0;
    length = list.length;
    for (; index < length; index++) {
    	if(list[index].apply(data[0], data[1]) === false && options.stopFalse) {// 配置了stopFalse状态,且有回调函数返回false,终止后面执行
    	    break;
    	}
    }
}

// 调用
var callList = _.callbacks('stopFalse');
callList.add(function(){
    console.log('1111');
    return false;
});
callList.add(function(){
    console.log('2222')
});
callList.fire();// 111

2)once仅执行一次的状态

fireWith: function(context, arguments) {
    var args = [context, arguments];
    // 第一次调fire:false || true
    // 调用fire后就将testing设为true,!testing就为false
    if(!options.once || !testing) {
    	fire(args)
    }
}

// 调用
var callList = _.callbacks('once');
callList.add(function(){
    console.log('1111');
});
callList.add(function(){
    console.log('2222')
});
callList.fire();// 111、222,只执行一次fire
callList.fire();

3)memory记住上一次执行的状态

(function(root){
    var _ = {
        callbacks: function(options) {
            options = typeof options === 'string' ? createOptions(options) : {};
            
            var list = [];
            var index, length, testing, memory, start, starts;
            
            // 依次执行
            var fire = function(data) {
            	console.log(data)
            	memory = options.memory && data;
            	testing = true;
            	index = starts || 0;
            	starts = 0;
            	length = list.length;
            	for (; index < length; index++) {
            		if(list[index].apply(data[0], data[1]) === false && options.stopFalse) {
            			break;
            		}
            	}
            }
            var self = {
            	add: function() {
                    var args = [].slice.call(arguments);
                    start = list.length;
                    args.forEach(function(fn){
                    	if(toString.call(fn) == '[object Function]') {
                            list.push(fn);
                    	}
                    })
                    // 重新调用fire,执行callback
                    if(memory) {
                    	starts = start
                    	fire(memory)
                    }
            	},
            	fireWith: function(context, arguments) {
                    var args = [context, arguments];
                    // 第一次调fire:false || true
                    if(!options.once || !testing) {
                    	fire(args)
                    }
            	},
            	// 传参
            	fire: function() {
                    self.fireWith(this, arguments)
            	}
            }
            return self;
        }
    }
    function createOptions(options) {
    	var obj = {};
    	options.split(/\s+/).forEach(function(value){
    		obj[value] = true;
    	})
    	return obj;
    }
    root._ = _;
})(this);

// 调用
var callList = _.callbacks('memory');
callList.add(function(){
    console.log('1111');
});
callList.add(function(){
    console.log('2222')
});
callList.fire(); // 1111、2222
callList.add(function(){
    console.log('memory')
}); // memory

3、resolve、reject、notify等语法糖封装

  如果理解了异步编程的容器创建和状态控制,resolve、reject、notify等异步回调函数就更容易理解了,这仅仅是语法糖的封装。封装语法糖实现机制图如下:

实现代码:

Deferred: function() {
    // 创建语法糖的过程
    var tuples = [
    	[ "resolve", "done", _.callbacks("once memory"), "resolved" ],
    	[ "reject", "fail", _.callbacks("once memory"), "rejected" ],
    	[ "notify", "progress", _.callbacks("memory") ]
    ]
    
    var promise = {}; // 添加数据(回调)
    var deferred = {}; // 状态控制
    var state; // 最终状态
    tuples.forEach(function(tuple, i) {
    	var callList = tuple[2],
    		stateString = tuple[3];
    	// promise[ done | fail | progress] = callList.add
    	promise[tuple[1]] = callList.add;
    
    	// Handle state
    	if(stateString) {
    		// state = [ resolved | rejected ]
    		state = stateString;
    	}
    
    	// deferred[ resolve | reject | notify] = callList.fire
    	deferred[tuple[0]] = function() {
    		deferred[tuple[0] + 'With'](this === deferred ? promise : this, arguments);
    		return this;
    	}
    	deferred[tuple[0] + 'With'] = callList.fireWith;
    	
    	return deferred;
    })
}

小结

  如今异步编程还在不断精进和改善中,又有generator+co、async+await等异步编程方式的出现。我们重要的是理解异步编程的机制,这样才能更快地学习掌握日新月异的异步编程方式~~