最近在系统学习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 = {}; }
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); for(var i=0, len=this.handlers[eventType].length; i<len; i++) { 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) {
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例子练习,执行结果点击这里:
- 显示一个加载指示图标
- 加载一篇小说的 JSON,包含小说名和每一章内容的 URL。
- 在页面中填上小说名
- 加载所有章节正文
- 在页面中添加章节正文
- 停止加载指示
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); });
|