JS异步编程学习

最近在系统学习JS异步编程的知识,本篇文章会持续更新,每次都会有新增内容

函数式编程

在异步编程之前需要回顾一下函数式编程,此为异步编程的基础。

把函数作为参数,或将函数作为返回值的函数,如:

1
2
3
4
5
function foo(x) {
return function() {
return x;
};
}

此外函数式编程还能形成一种后续传递风格,比如:

1
2
3
function foo(x, bar) {
return bar(x);
}

把函数bar作为参数传入,那么不同的bar函数就会得到不同的结果,这就使得函数的业务重点变为传入哪种回调函数了,不同的回调函数处理不同的业务逻辑。数组的sort()forEach()map()reduce()reduceRight()filter()every()some()就是典型的例子。这种函数叫高阶函数。

还有一类函数式编程的写法,由于有些时候会遇到重复定义相似函数的情况,通过引入一个新函数,让这个函数像工厂一样批量创建这些相似函数,那么就会在一定程度上简化了代码,如下面的例子:

1
2
3
4
5
6
7
8
var isType = function(type) {
return function(obj) {
return toString.call(obj) == '[object' + type + ']';
};
}

var isString = isType('String');
var isFunction = isType('Function');

上例实现了类型判断功能,通过上面的这种写法,我们可以创建具体的类型判断函数isString()isFunction()。这种函数也叫偏函数,通过指定部分参数来产生一个新的定值函数。

上面的函数式编程方式在异步编程中非常常见。

PubSub模式

事件监听器模式是广泛应用于异步编程的模式,是回调函数的事件化,也称作PubSub模式(发布/订阅模式)。node的EventEmitter对象,Backbone的事件化模型和jQuery的自定义事件都是这种模式的表现。

下面先介绍一下PubSub模式的JS实现

很典型的click事件

1
dom.onclick = clickHandler;

上面的过程完成了两件事,一、注册了事件click,二、添加了事件处理器,也就是当这个事件触发时要执行的回调函数

如果我们要自己实现一个PubSub模式,思路便是首先给出注册事件的函数,里面包含了事件名及对应事件名要执行的事件处理器,然后给出触发函数,使注册的事件能够对应执行相应的事件处理器

我们自己实现一个PubSub模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function PubSub() {
this.handlers = {};
}

//注册事件以及handler
PubSub.prototype.on = function(eventType,handler) {
//如果注册的事件之前没有存在过,那么新添进去
if (!(eventType in this.handlers)) {
this.handlers[eventType] = [];
}

this.handlers[eventType].push(handler); //将事件处理器作为数组存储,那么就会按照添加处理器的顺序来调用
return this;
}

//触发事件,遍历所有事件处理器
PubSub.prototype.emit = function(eventType) {

var handlerArgs = Array.prototype.slice.call(arguments, 1); //取到除了eventType之外的所有参数,并转换为数组
for(var i=0, len=this.handlers[eventType].length; i<len; i++) {
//对这个eventType执行注册进去的handler
this.handlers[eventType][i].apply(this, handlerArgs);
}
//返回这个对象
return this;
}

var pubsub = new PubSub();
pubsub.on('myEvent', function() {
console.log('success!');
})

pubsub.emit('myEvent');

运行代码,console里打印出来success,表明调用成功。其中,由于this.handlers[eventType][i].apply(this, handlerArgs);handlerArgs是数组,那么就可以实现一个事件执行多个事件处理器。

是不是发现PubSub很简单啊?

其实PubSub模式本身没有同步或异步调用的问题,node中的emit()调用大部分是伴随事件循环而异步触发,因此,我们说PubSub广泛应用于异步编程。上面的思想就是node中EventEmitter对象的核心部分,未实现的部分只剩下移除事件处理器和附加一次性事件处理器。

Promise

想象这样的场景,需要有两次异步请求,第二次的请求需要第一次请求的数据:

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
url: url1,
success: function(data) {
ajax({
url: url2,
data: data,
success: function() {
}
});
}
});

如果接下来在回调函数里做进一步操作,那么嵌套的层数会越来多。promise就用来解决这种问题。

  • promise的操作只会处于3种状态:未完成态,完成态和失败态
  • 状态只会出现从未完成态向完成态或失败态转化,不可逆反,完成态和失败态之间不能互相转化。
  • 一个promise只能成功或失败一次,且状态无法改变
  • 若promise执行成功或失败后,为其添加的成功或失败的回调会立即执行

创建Promise:

1
2
3
4
5
6
7
8
9
10
var promise = new Promise(function(resolve, reject) {
//doSomething

if(/*正常*/) {
resolve('success');
}
else {
reject(Error('error'));
}
});

Promise的构造器以函数作为参数,此回调函数有两个变量resolve和reject,由构造器传入,回调函数中进行的异步操作若成功,则调用resolve,失败则调用reject。

Promise对象还有一个很重要的方法then()

  • 接受两个参数,分别是完成态和错误态的回调方法,操作成功和失败时分别调用相应的回调方法。
  • 只接受function对象
  • 继续返回promise对象,以实现链式调用

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
promise.then(function(result) {
console.log(result); //成功
}, function(err) {
console.log(err); //失败
});


//链式调用
promise()
.then(obj.api1)
.then(obj.api2)
.then(obj.api3)
.then(obj.api4)
.then(function (value4) {
//do something with value4
}, function(error) {
//handle any error from step1 through step4
})
.done();

promise例子练习,执行结果点击这里

  1. 显示一个加载指示图标
  2. 加载一篇小说的 JSON,包含小说名和每一章内容的 URL。
  3. 在页面中填上小说名
  4. 加载所有章节正文
  5. 在页面中添加章节正文
  6. 停止加载指示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var storyDiv = document.querySelector('.story');

function addHtmlToPage(content) {
var div = document.createElement('div');
div.innerHTML = content;
storyDiv.appendChild(div);
}

function addTextToPage(content) {
var p = document.createElement('p');
p.textContent = content;
storyDiv.appendChild(p);
}

function get(url) {
return new Promise(function(resolve, reject) {

var req = new XMLHttpRequest();
req.open('GET', url);

req.onload = function() {
if(req.status == 200) {
resolve(req.response);
}
else {
reject(Error(req.statusText));
}
};

req.onerror = function() {
reject(Error('network error!'));
};

req.send();
});
}

function getJSON(url) {
return get(url).then(JSON.parse);
}

var storyPromise;

function getChapter(i) {
var storyPromise = storyPromise || getJSON(story.json);

return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
});
}

getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);

return story.chapterUrls.reduce(function(sequence, chapterUrl) {
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage('All done!')
}).catch(function(err) {
addTextToPage('argh broken:' + err.message);
});