前言
rxjs是一个响应式编程的库,它使异步编程和回调代码变得更为简单。 mpvue是一个小程序的框架。它使用vue的语法,使小程序的开发更加快捷简单,让前端开发人员的一套代码同时用于web端和小程序端变成了现实。
先来聊聊rxjs
响应式编程是rxjs的核心概念之一。在主流的三大框架之中都得到了应用:vue的底层就采用了reactive programming.angular2+ 也全面引用了rxjs,,不管是在 http 还是 animation 都用了 RxJS 的 Observable.Redux 从3.5版本开始,也加入了对Observable 操作的支持.甚至于主流的编程语言都有rx的Library,,比如RxRuby, RxPy, RxJava...等 RxJS 提供了一套完整的非同步解决方案,让我们在面对各种非同步行为,不管是 Event, AJAX, 还是 Animation 等,我们都可以使用相同的 API 做开发。
1.网页的世界是异步的
在前端开发过程中,我们会用到各种的js异步,如callback 或是 Promise 物件甚至是async/await,单随着应用越来越复杂,编写异步代码越来越困难和繁琐。异步常见的问题有: * 竞态条件 (Race Condition) * 内存泄漏 (Memory Leak) * 复杂的状态 (Complex State) * 异常处理 (Exception Handling)
- 竞态条件: 发送了第一个请求之后又发送了第二条请求,这两条请求的顺序会影响到最终接收的不同结果.
- 内存泄露: 在单页面应用中,如果有对dom注册监听事件,而没有在适当的时机点把监听的事件移除,就会造成Memory Leak.
- 复杂的状态: 比如说我们有一支付费用户才能播放的影片,首先可能要先抓取这部影片的资讯,接着我们要在播放时去验证使用者是否有权限播放,而使用者也有可能再按下播放后又立即按了取消,而这些都是非同步执行,这时就会各种复杂的状态需要处理.
- 异常处理: JavaScript 的try/catch可以捕捉同步的例外,但非同步的程式就没这么容易,尤其当我们的非同步行为很复杂时,这个问题就愈加明显。
如果我们用rxjs来处理,似乎就变得方便了许多:
var handler = (e) => {
console.log(e);
document.body.removeEventListener('click', handler);
}
document.body.addEventListener('click', handler);
`
可以写成:
`
Rx.Observable
.fromEvent(document.body, 'click') // 注册监听
.take(1) // 只取一次
.subscribe(console.log);
复制代码
总之,RxJS 是一套藉由 Observable sequences 来组合非同步行为和事件基础程序的 Library,可以把 RxJS 想成处理 非同步行为 的 Lodash.是Functional Programming 及 Reactive Programming 两个编程思想的结合.
2.聊聊Observable
RxJS 的基础就是 Observable,只要弄懂 Observable 就等于学会一半的 RxJS.Observable 就是观察者模式(Observer) 和 迭代器模式(Iterator) 两种思想的结合。
-
Observer Pattern
观察者模式在api设计上得到广泛应用,常见的一个例子是
function clickHandler(event) { console.log('user click!'); } document.body.addEventListener('click', clickHandler) 复制代码
让我们自己来实现一个:
//定义 class Producer { constructor() { this.listeners = []; } addListener(listener) { if(typeof listener === 'function') { this.listeners.push(listener) } else { throw new Error('listener 必須是 function') } } removeListener(listener) { this.listeners.splice(this.listeners.indexOf(listener), 1) } notify(message) { this.listeners.forEach(listener => { listener(message); }) } } //应用 var egghead = new Producer(); function listener1(message) { console.log(message + 'from listener1'); } function listener2(message) { console.log(message + 'from listener2'); } egghead.addListener(listener1); // 注册监听 egghead.addListener(listener2); egghead.notify('A new course!!') // 执行 //输出结果: a new course!! from listener1 a new course!! from listener2 复制代码
这个例子很好的说明了 Observer Pattern 如何在event 跟 listeners的应用中做到解耦
-
Iterator Pattern
下面是一个使用Iterator的例子
var arr = [1, 2, 3]; var iterator = arr[Symbol.iterator](); iterator.next(); // { value: 1, done: false } iterator.next(); // { value: 2, done: false } iterator.next(); // { value: 3, done: false } iterator.next(); // { value: undefined, done: true } 复制代码
让我们来动手制作一个:
//定义 class IteratorFromArray { constructor(arr) { this._array = arr; this._cursor = 0; } next() { return this._cursor < this._array.length ? { value: this._array[this._cursor++], done: false } : { done: true }; } map(callback) { const iterator = new IteratorFromArray(this._array); return { next: () => { const { done, value } = iterator.next(); return { done: done, value: done ? undefined : callback(value) } } } } } //使用 var iterator = new IteratorFromArray([1,2,3]); var newIterator = iterator.map(value => value + 3); newIterator.next(); // { value: 4, done: false } newIterator.next(); // { value: 5, done: false } newIterator.next(); // { value: 6, done: false } 复制代码
类似的还有generator的例子:
function* getNumbers(words) { for (let word of words) { if (/^[0-9]+$/.test(word)) { yield parseInt(word, 10); } } } const iterator = getNumbers('12我们3学习4'); iterator.next(); // { value: 1, done: false } iterator.next(); // { value: 2, done: false } iterator.next(); // { value: 3, done: false } iterator.next(); // { value: 4, done: false } iterator.next(); // { value: undefined, done: true } 复制代码
总之,Observer 与 Iterator 都有共同的特性,就是渐进式的获取数据信息,差别在于Observer 是生产者push数据,Iterator 是消费者pull数据。而Observable 具备生产者推送数据的特性,同时能像序列,拥有序列处理数据的方法
3.创建Observable
RxJS 有一个核心和三个重点。核心就是Observable(map, filter...),三个重点分别是:Observer,Subject,Schedulers, 先来讲讲Observable的用法。
建立 Observable: create
var observable = Rx.Observable
.create(function(observer) {
observer.next('Jerry');
observer.next('Anna');
})
复制代码
让我们订阅observable,来接收数据
observable.subscribe(function(value) {
console.log(value);
})
复制代码
需要注意的是,Observable不仅可以处理异步情况,同步行为也是可以的。此外,观察者(Observer) 有三个方法:
- next:每当 Observable 发送出新的值,next 方法就会被调用。
- complete:在 Observable 没有其他的数据可以取得时,complete 方法就会被调用,在 complete 被调用之后,next 方法就不会再起作用。
- error:每当 Observable 内发生错误时,error 方法就会被调用。
var observable = Rx.Observable
.create(function(observer) {
observer.next('Jerry');
observer.next('Anna');
observer.complete();
observer.next('not work');
})
var observer = {
next: function(value) {
console.log(value);
},
error: function(error) {
console.log(error)
},
complete: function() {
console.log('complete')
}
}
observable.subscribe(observer)
//输出
Jerry
Anna
complete
复制代码
4.Observable的常用方法
Observable 的常用方法包括create,of,from,fromEvent,fromPromise,never,empty,throw,interval,timer等等
of的用法,可以和上面的create方法做一个对比
var observable = Rx.Observable.of('Jerry', 'Anna');
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
});
// Jerry
// Anna
// complete!
复制代码
from的用法,参数是数组,对比of传入的是一个个参数
var arr = ['Jerry', 'Anna', 2016, 2017, '30 days']
var observable = Rx.Observable.from(arr);
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
});
// Jerry
// Anna
// 2016
// 2017
// 30 days
// complete!
复制代码
此外,from的参数还可以是字符串或者promise
var observable = Rx.Observable
.from(new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello RxJS!');
},3000)
}))
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
});
// Hello RxJS!
// complete!
复制代码
fromEvent的用法,第一个参数是dom元素,第二个参数是要监听的事件名,例如对body做点击事件监听:
var observable = Rx.Observable.fromEvent(document.body, 'click');
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
});
复制代码
fromEventPattern,这个方法是给类事件使用。所谓的类事件就是指其行为跟事件相像,同时具有注册监听及移除监听两种行为,就像 DOM Event 有 addEventListener 及 removeEventListener
class Producer {
constructor() {
this.listeners = [];
}
addListener(listener) {
if(typeof listener === 'function') {
this.listeners.push(listener)
} else {
throw new Error('listener 必須是 function')
}
}
removeListener(listener) {
this.listeners.splice(this.listeners.indexOf(listener), 1)
}
notify(message) {
this.listeners.forEach(listener => {
listener(message);
})
}
}
var egghead = new Producer();
// egghead 同時有 addEventListener 及 removeEventListener方法
var observable = Rx.Observable
.fromEventPattern(
(handler) => egghead.addListener(handler),
(handler) => egghead.removeListener(handler)
);
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
})
egghead.notify('Hello! Can you hear me?');
// Hello! Can you hear me?
复制代码
empty方法,会返回一个空的observable,立即执行complete
var observable = Rx.Observable.empty();
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
});
// complete!
复制代码
never方法,会返回一个无穷的observable,就是一个一直存在,但什么都不做的observable
var observable = Rx.Observable.never();
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log(error)
}
});
复制代码
throw方法的作用就是抛出错误
var observable = Rx.Observable.throw('Oop!');
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log('Throw Error: ' + error)
}
});
// Throw Error: Oop!
复制代码
interval方法,会发送一个从零开始依次递增的整数,它的参数是间隔时间,单位是毫秒
var observable = Rx.Observable.interval(1000);
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log('Throw Error: ' + error)
}
});
// 0
// 1
// 2
// ...
复制代码
timer方法与 interval有所不同,它有两个参数。第一个参数代表发出第一个值的等待时间,第二个参数代表每次发出值的间隔时间
var observable = Rx.Observable.timer(1000, 5000);
observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log('Throw Error: ' + error)
}
});
//先等一秒
// 0
// 1
// 2 ...
复制代码
unsubscribe方法: 在订阅observable后,会返回一个subscription,它有一个可以释放资源的unsubscribe方法
var observable = Rx.Observable.timer(1000, 1000);
// 取得 subscription
var subscription = observable.subscribe({
next: function(value) {
console.log(value)
},
complete: function() {
console.log('complete!');
},
error: function(error) {
console.log('Throw Error: ' + error)
}
});
setTimeout(() => {
subscription.unsubscribe() // 停止订阅
}, 5000);
// 0
// 1
// 2
// 3
// 4
复制代码
map方法:参数是一个回调函数,对数据进行操作之后,再返回新的observable
var observable = Rx.Observable.interval(1000);
var newest = observable.map(x => x + 2);
newest.subscribe(console.log);
// 2
// 3
// 4
// 5..
复制代码
让我们自己动手来实现一下:
function map(callback) {
return Rx.Observable.create((observer) => {
return this.subscribe(
(value) => {
try{
observer.next(callback(value));
} catch(e) {
observer.error(e);
}
},
(err) => { observer.error(err); },
() => { observer.complete() }
)
})
}
Rx.Observable.prototype.map = map;
var people = Rx.Observable.of('Jerry', 'Anna');
var helloPeople = people.map((item) => item + ' Hello~');
helloPeople.subscribe(console.log);
// Jerry Hello~
// Anna Hello~
复制代码
mapTo方法:把原来的值都改成一个固定值
var observable = Rx.Observable.interval(1000);
var newest = observable.mapTo(2);
newest.subscribe(console.log);
// 2
// 2
// 2
// 2..
复制代码
filter方法:类型Array的filter方法,过滤出一些值.
var observable = Rx.Observable.interval(1000);
var newest = observable.filter(x => x % 2 === 0);
newest.subscribe(console.log);
// 0
// 2
// 4
// 6..
复制代码
take方法:取前多少个元素就结束.
var observable = Rx.Observable.interval(1000);
var example = observable.take(3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// complete
复制代码
first方法:相当于take(1),取出第一个元素后就结束
var observable = Rx.Observable.interval(1000);
var example = observable.first();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// complete
复制代码
takeUntil方法:在某个事件发生时,结束.
var observable = Rx.Observable.interval(1000);
var click = Rx.Observable.fromEvent(document.body, 'click');
var example = observable.takeUntil(click);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// complete //点击body元素时
复制代码
concatAll方法:当Observable传递的元素还是observable时,类似于二维数组,我们通过这个方法把它扁平化为一维数组(concatAll 会一个一个处理,一定是等前一个 observable 完成(complete)才会处理下一个 observable)
var observable = Rx.Observable.fromEvent(document.body, 'click');
var source = observable.map(e => Rx.Observable.of(1,2,3));
var example = source.concatAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
switch方法:同concatAll类似,扁平化observable。(它会在新的 observable 送出后直接处理新的 observable 不管前一个 observable 是否完成,每当有新的 observable 送出就会直接把旧的 observable 退订(unsubscribe),永远只处理最新的 observable!)
var click = Rx.Observable.fromEvent(document.body, 'click');
var source = click.map(e => Rx.Observable.interval(1000));
var example = source.switch();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
mergeAll:同concatAll,switch类似,扁平化observable。mergeAll 可以传入一个数值,这个数值代表他可以同时处理的 observable 数量(不会像 switch 一样退订(unsubscribe)原先的 observable 而是并行处理多个 observable)
var click = Rx.Observable.fromEvent(document.body, 'click');
var source = click.map(e => Rx.Observable.interval(1000));
var example = source.mergeAll();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
//----------------00---11---22---33---(04)4--...
复制代码
下面来实现一个拖拉功能:页面上有个元素(#drag),在该元素上按下左键(mousedown)时,开始监听鼠标滑动的位置。当鼠标释放时(mouseup),结束监听。当鼠标移动时(mousemove),改变元素的位置
const dragDOM = document.getElementById('drag');
const body = document.body;
const mouseDown = Rx.Observable.fromEvent(dragDOM, 'mousedown');
const mouseUp = Rx.Observable.fromEvent(body, 'mouseup');
const mouseMove = Rx.Observable.fromEvent(body, 'mousemove');
mouseDown
.map(event => mouseMove.takeUntil(mouseUp))
.concatAll()
.map(event => ({ x: event.clientX, y: event.clientY }))
.subscribe(pos => {
dragDOM.style.left = pos.x + 'px';
dragDOM.style.top = pos.y + 'px';
})
复制代码
skip方法:跳过前几个元素,从后面继续取值
var observable = Rx.Observable.interval(1000);
var example = observable.skip(3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 3
// 4
// 5...
复制代码
takeLast方法:从后面取值
var observable = Rx.Observable.interval(1000).take(6);
var example = observable.takeLast(2);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 4
// 5
// complete
复制代码
last方法:用来取到最后的元素
var observable = Rx.Observable.interval(1000).take(6);
var example = observable.last();
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 5
// complete
复制代码
concat方法:把多个observable合并成一个
var source = Rx.Observable.interval(1000).take(3);
var source2 = Rx.Observable.of(3)
var source3 = Rx.Observable.of(4,5,6)
var example = source.concat(source2, source3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// complete
复制代码
startWith方法:在observable最开始插入元素
var observable = Rx.Observable.interval(1000);
var example = observable.startWith(0);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 0
// 1
// 2
// 3...
复制代码
merge方法:合并observable,和concat不同的是:merge把多个observable同时处理,而concat处理完一个之后才会处理接下来的observable
var source = Rx.Observable.interval(500).take(3);
var source2 = Rx.Observable.interval(300).take(6);
var example = source.merge(source2);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 0
// 1
// 2
// 1
// 3
// 2
// 4
// 5
// complete
复制代码
combineLatest方法:它会取到各个observable的值,经过处理后再输出
var observale = Rx.Observable.interval(500).take(3);
var newest = Rx.Observable.interval(300).take(6);
var example = observale.combineLatest(newest, (x, y) => x + y);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// complete
复制代码
分析:combineLatest会等两个observable都有传送值的时候才会执行callback
var observale = Rx.Observable.interval(500).take(3);
var newest = Rx.Observable.interval(300).take(6);
var example = observale.zip(newest, (x, y) => x + y);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 2
// 4
// complete
复制代码
分析:zip 会等到 observale 跟 newest 都送出了第一个元素,再传入 callback,下次则等到 observale 跟 newest 都送出了第二个元素再一起传入 callback
var main = Rx.Observable.from('hello').zip(Rx.Observable.interval(500), (x, y) => x);
var some = Rx.Observable.from([0,1,0,0,0,1]).zip(Rx.Observable.interval(300), (x, y) => x);
var example = main.withLatestFrom(some, (x, y) => {
return y === 1 ? x.toUpperCase() : x;
});
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// h
// e
// l
// L
// O
// complete
复制代码
分析:withLatestFrom 会在 main 送出值的时候执行 callback,但请注意如果 main 送出值时 some 之前没有送出过任何值 callback 仍然不会执行
const video = document.getElementById('video');
const anchor = document.getElementById('anchor');
const scroll = Rx.Observable.fromEvent(document, 'scroll');
scroll.map(e => anchor.getBoundingClientRect().bottom < 0)
.subscribe(bool => {
if(bool) {
video.classList.add('video-fixed');
} else {
video.classList.remove('video-fixed');
}
})
const mouseDown = Rx.Observable.fromEvent(video, 'mousedown')
const mouseUp = Rx.Observable.fromEvent(document, 'mouseup')
const mouseMove = Rx.Observable.fromEvent(document, 'mousemove')
const validValue = (value, max, min) => {
return Math.min(Math.max(value, min), max)
}
mouseDown
.filter(e => video.classList.contains('video-fixed'))
.map(e => mouseMove.takeUntil(mouseUp))
.concatAll()
.withLatestFrom(mouseDown, (move, down) => {
return {
x: validValue(move.clientX - down.offsetX, window.innerWidth - 320, 0),
y: validValue(move.clientY - down.offsetY, window.innerHeight - 180, 0)
}
})
.subscribe(pos => {
video.style.top = pos.y + 'px';
video.style.left = pos.x + 'px';
})
复制代码
scan方法:类似Array的reduce方法,第一个参数传入callback,第二个参数传入初始值(可以没有)。返回一个observable 实例
var source = Rx.Observable.from('hello')
.zip(Rx.Observable.interval(600), (x, y) => x);
var observable = source.scan((origin, next) => origin + next, '');
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// h
// he
// hel
// hell
// hello
// complete
复制代码
buffer方法:它会把原本的 observable (source)送出的元素缓存在数组中,等到传入的 observable(source2) 送出元素时,就会触发把缓存的元素送出。
var source = Rx.Observable.interval(300);
var source2 = Rx.Observable.interval(1000);
var observable = source.buffer(source2);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// [0,1,2]
// [3,4,5]
// [6,7,8]...
复制代码
bufferCount方法:逢n个数缓存在数组中,并输出
var source = Rx.Observable.interval(300);
var observable = source.bufferCount(3);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// [0,1,2]
// [3,4,5]
// [6,7,8]...
复制代码
bufferTime方法:如下例子,鼠标在500ms内连续点两下才输出
const button = document.getElementById('demo');
const click = Rx.Observable.fromEvent(button, 'click')
const observable = click
.bufferTime(500)
.filter(arr => arr.length >= 2);
observable.subscribe({
next: (value) => { console.log('success'); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
delay方法:延迟多久之后再输出元素,参数可以是数字(ms),也可以是日期格式
var source = Rx.Observable.interval(300).take(5);
var observable = source.delay(500);
// observable = source.delay(new Date(new Date().getTime() + 1000));
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
//延迟500ms之后
// 0
// 1
// 2
// 3
// 4
复制代码
delayWhen方法:delayWhen 可以影响每个元素,而且需要传一个 callback 并回传一个 observable
var source = Rx.Observable.interval(300).take(5);
var observable = source
.delayWhen(
x => Rx.Observable.empty().delay(100 * x * x)
);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
用学到的方法实现一个小功能:许多图片跟着鼠标跑,但是不能跑的一样快
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<style>
* {
margin: 0;
padding: 0;
cursor: pointer;
}
img {
width: 50px;
position: absolute;
border-radius: 50%;
border: 3px white solid;
transform: translate3d(0,0,0);
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.1/Rx.js"></script>
</head>
<body>
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover6.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover5.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover4.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover3.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover2.jpg" alt="">
<img src="https://res.cloudinary.com/dohtkyi84/image/upload/c_scale,w_50/v1483019072/head-cover1.jpg" alt="">
<script>
var imgList = document.getElementsByTagName('img');
var movePos = Rx.Observable.fromEvent(document, 'mousemove')
.map(e => ({ x: e.clientX, y: e.clientY }))
function followMouse(DOMArr) {
const delayTime = 600;
DOMArr.forEach((item, index) => {
movePos
.delay(delayTime * (Math.pow(0.65, index) + Math.cos(index / 4)) / 2) //时间规则可以换成其他的
.subscribe(function (pos){
item.style.transform = 'translate3d(' + (pos.x-25) + 'px, ' + (pos.y-25) + 'px, 0)';
});
});
}
followMouse(Array.from(imgList))
</script>
</body>
</html>
复制代码
debounce 方法:和防抖函数功能一致,debounce 跟 debounceTime 一个是传入 observable 另一个则是传入毫秒,比较常用到的是 debounceTime:会先把元素cache 住并等待一段时间,如果这段时间内已经没有收到任何元素,则把元素送出;如果这段时间内又收到新的元素,则会把原本cache 住的元素释放 掉并重新计时,不断反复
const searchInput = document.getElementById('searchInput');
const theRequestValue = document.getElementById('theRequestValue');
Rx.Observable.fromEvent(searchInput, 'input')
.debounceTime(300)
.map(e => e.target.value)
.subscribe((value) => {
theRequestValue.textContent = value;
// 在这请求接口
})
复制代码
throttle 方法:和节流函数功能一致,throttle 跟 throttleTime 一个是传入 observable 另一个则是传入毫秒,比较常用到的是throttleTime:会先开放送出元素,等到有元素被送出就会沉默一段时间,等到时间过了又会开放发送元素,throttle 是在控制行为的最高频率,更适合用在连续性行为
var source = Rx.Observable.interval(300).take(5);
var observable = source.throttleTime(1000);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// 0
// 4
// complete
复制代码
distinct方法:相同的值只留一个
var source = Rx.Observable.from([{ value: 'a'}, { value: 'b' }, { value: 'c' }, { value: 'a' }, { value: 'c' }])
.zip(Rx.Observable.interval(300), (x, y) => x);
var observable = source.distinct((x) => {
return x.value
});
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// {value: "a"}
// {value: "b"}
// {value: "c"}
// complete
复制代码
distinctUntilChanged方法:跟 distinct 一样会把相同的元素过滤掉,但 distinctUntilChanged 只会跟最后一次送出的元素比较,不会每个都比
var source = Rx.Observable.from(['a', 'b', 'c', 'c', 'b'])
.zip(Rx.Observable.interval(300), (x, y) => x);
var observable = source.distinctUntilChanged()
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// a
// b
// c
// b
// complete
复制代码
catch方法:用来处理错误
var source = Rx.Observable.from(['a','b','c',2])
.zip(Rx.Observable.interval(500), (x,y) => x);
var observable = source
.map(x => x.toUpperCase())
.catch(error => Rx.Observable.of('h'));
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// A
// B
// C
// h
// complete
复制代码
retry方法:当某个Observable发生错误时,从头尝试循环Observable.参数为循环的次数
var source = Rx.Observable.from(['a','b',2])
.zip(Rx.Observable.interval(500), (x,y) => x);
var observable = source
.map(x => x.toUpperCase())
.retry(1);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// A
// B
// A
// B
// Error: TypeError: x.toUpperCase is not a function
复制代码
retryWhen方法:当某个Observable发生错误时,去做某些处理之后再去循环尝试
var source = Rx.Observable.from(['a','b','c','d',2])
.zip(Rx.Observable.interval(500), (x,y) => x);
var observable = source
.map(x => x.toUpperCase())
.retryWhen(
errorObs => errorObs.map(err => fetch('...')));
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
repeat方法:和retry类似,是一直重复订阅的效果,但没有错误发生。参数是循环次数,没有参数就默认无限循环
var source = Rx.Observable.from(['a','b','c'])
.zip(Rx.Observable.interval(500), (x,y) => x);
var observable = source.repeat(1);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
concatMap方法:等同于 map 加上 concatAll,在前一个observable执行完成后再执行下一个observable
var source = Rx.Observable.fromEvent(document.body, 'click');
var observable = source
.concatMap(
e => Rx.Observable.interval(100).take(3)
);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
此外,concatMap 还有第二个参数是一个 selector callback,这个 callback 会传入四个参数,分别是 外部 observable 送出的元素、内部 observable 送出的元素、外部 observable 送出元素的 index、 内部 observable 送出元素的 index
function getPostData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var observable = source.concatMap(
e => Rx.Observable.from(getPostData()),
(e, res, eIndex, resIndex) => res.title); //res就是Rx.Observable.from(getPostData())的observable
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
switchMap方法:等同于 map 加上 switch,会在下一个 observable 被送出后直接退订前一个未处理完的 observable
function getPostData() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(res => res.json())
}
var source = Rx.Observable.fromEvent(document.body, 'click');
var observable = source.switchMap(
e => Rx.Observable.from(getPostData()));
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
mergeMap方法:等同于 map 加上 mergeAll ,可以并行处理多个 observable(也能传入第二个参数 selector callback,这个 selector callback 跟 concatMap 第二个参数也是完全一样的,但 mergeMap 的重点是我们可以传入第三个参数,来限制并行处理的数量)
var source = Rx.Observable.fromEvent(document.body, 'click');
var observable = source
.mergeMap(
e => Rx.Observable.interval(100).take(3)
);
observable.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
复制代码
总之
* concatMap 用在可以确定内部的 observable 结束时间比外部 observable 发送时间来快的情境,并且不希望有任何并行处理行为,适合少数要一次一次完成到底的的 UI 动画或特别的 HTTP request 行为。
* switchMap 用在只要最后一次行为的结果,适合绝大多数的使用情境。
* mergeMap 用在并行处理多个 observable,适合需要并行处理的行为,像是多个 I/O 的并行处理。
复制代码
小范例(制作一个简易的autocomplete)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.1/Rx.js"></script>
<style>
html, body {
height: 100%;
background-color: white;
padding: 0;
margin: 0;
}
.autocomplete {
position: relative;
display: inline-block;
margin: 20px;
}
.input {
width: 200px;
border: none;
border-bottom: 1px solid black;
padding: 0;
line-height: 24px;
font-size: 16px;
}
.input:focus {
outline: none;
border-bottom-color: blue;
}
.suggest {
width: 200px;
list-style: none;
padding: 0;
margin: 0;
-webkit-box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.suggest li {
cursor: pointer;
padding: 5px;
}
.suggest li:hover {
background-color: lightblue;
}
</style>
</head>
<body>
<div class="autocomplete">
<input class="input" type="search" id="search" autocomplete="off">
<ul id="suggest-list" class="suggest">
</ul>
</div>
<script>
const url = 'https://zh.wikipedia.org/w/api.php?action=opensearch&format=json&limit=5&origin=*';
const getSuggestList = (keyword) => fetch(url + '&search=' + keyword, { method: 'GET', mode: 'cors' })
.then(res => res.json())
const searchInput = document.getElementById('search');
const suggestList = document.getElementById('suggest-list');
const keyword = Rx.Observable.fromEvent(searchInput, 'input');
const selectItem = Rx.Observable.fromEvent(suggestList, 'click');
const render = (suggestArr = []) => suggestList.innerHTML = suggestArr.map(item => '<li>'+ item +'</li>').join('')
keyword
// .filter(e => e.target.value.length > 2) 使用者打了 2 个字以上在发送 request
.debounceTime(100)
.switchMap(
e => getSuggestList(e.target.value),//.retry(3) 在 API 失败的时候重新尝试 3 次
(e, res) => res[1]
)
.subscribe(list => render(list))
selectItem
.filter(e => e.target.matches('li'))
.map(e => e.target.innerText)
.subscribe(text => {
searchInput.value = text;
render();
})
</script>
</body>
</html>
复制代码
window方法:把拆出来的元素放入observable并送出observable,类似buffer拆分出來的元素放到数组并送出数组
var click = Rx.Observable.fromEvent(document, 'click');
var source = Rx.Observable.interval(1000);
var observable = source.window(click);
observable
.switch()
.subscribe(console.log);
// 0
// 1
// 2
// 3
// 4
// 5 ...
复制代码
windowToggle方法,传入两个参数:第一个是开始的observable,第二个是一个回调函数回传一个结束的observable
var source = Rx.Observable.interval(1000);
var mouseDown = Rx.Observable.fromEvent(document, 'mousedown');
var mouseUp = Rx.Observable.fromEvent(document, 'mouseup');
var observable = source
.windowToggle(mouseDown, () => mouseUp)
.switch();
observable.subscribe(console.log);
复制代码
groupBy方法:把相同条件元素拆分成一个observable
var people = [
{name: 'Anna', score: 100, subject: 'English'},
{name: 'Anna', score: 90, subject: 'Math'},
{name: 'Anna', score: 96, subject: 'Chinese' },
{name: 'Jerry', score: 80, subject: 'English'},
{name: 'Jerry', score: 100, subject: 'Math'},
{name: 'Jerry', score: 90, subject: 'Chinese' },
];
var source = Rx.Observable.from(people)
.zip(
Rx.Observable.interval(300),
(x, y) => x);
var observable = source
.groupBy(person => person.name)
.map(group => group.reduce((acc, curr) => ({
name: curr.name,
score: curr.score + acc.score
})))
.mergeAll();
observable.subscribe(console.log);
// { name: "Anna", score: 286 }
// { name: 'Jerry', score: 270 }
复制代码
5.Observable的特性
Observable 与数组相比,有两大不同:1.延迟运算 2.渐进式取值
1.延迟运算:observable会等到订阅后才开始对元素做运算,如果没有订阅就不会有运算的行为
var source = Rx.Observable.from([1,2,3,4,5]);
var example = source.map(x => x + 1);
//上面的代码就不会去做运算
var source = [1,2,3,4,5];
var example = source.map(x => x + 1);
//数组执行完之后,已经做了运算
复制代码
2.渐进式取值:observable的每次运算会一算到底,并不是数组的运算完全部的元素之后再返回
var source = Rx.Observable.from([1,2,3]);
var example = source
.filter(x => x % 2 === 0)
.map(x => x + 1)
example.subscribe(console.log);
//1到filter被过滤掉;2到filter再到mapb变成3返回并打印;3到filter被过滤掉
var source = [1,2,3];
var example = source
.filter(x => x % 2 === 0)
.map(x => x + 1)
//数组执行到filter会返回完整的数组[2],再到map返回完整的数组[3]
复制代码
6.Subject是什么
Subject 同时是 Observable 又是 Observer,Subject 会对内部的 observers 清单进行组播(multicast)。是 Observer Pattern 的实例并且继承自 Observable。
动手实现一个Subject
var source = Rx.Observable.interval(1000).take(3);
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
var subject = {
observers: [],
subscribe: function(observer) {
this.observers.push(observer)
},
next: function(value) {
this.observers.forEach(o => o.next(value))
},
error: function(error){
this.observers.forEach(o => o.error(error))
},
complete: function() {
this.observers.forEach(o => o.complete())
}
}
subject.subscribe(observerA)
source.subscribe(subject);
setTimeout(() => {
subject.subscribe(observerB);
}, 1000);
// "A next: 0"
// "A next: 1"
// "B next: 1"
// "A next: 2"
// "B next: 2"
// "A complete!"
// "B complete!"
复制代码
对比一下真正的subject:可以看出,Subject 可以拿去订阅 Observable(source) 代表他是一个 Observer,同时 Subject 又可以被 Observer(observerA, observerB) 订阅,代表他是一个 Observable。
var source = Rx.Observable.interval(1000).take(3);
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
var subject = new Rx.Subject()
subject.subscribe(observerA)
source.subscribe(subject);
setTimeout(() => {
subject.subscribe(observerB);
}, 1000);
// "A next: 0"
// "A next: 1"
// "B next: 1"
// "A next: 2"
// "B next: 2"
// "A complete!"
// "B complete!"
复制代码
在某些无法直接使用Observable的前端框架中,我们可以用subject
class MyButton extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.subject = new Rx.Subject();
this.subject
.mapTo(1)
.scan((origin, next) => origin + next)
.subscribe(x => {
this.setState({ count: x })
})
}
render() {
return <button onClick={event => this.subject.next(event)}>{this.state.count}</button>
}
}
复制代码
BehaviorSubject: 希望 Subject 能代表当下的状态,而不是简单的事件发送
var subject = new Rx.BehaviorSubject(0); // 0 为起始值
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
subject.subscribe(observerA);
// "A next: 0" 如果是普通的subject则不会输出此行
subject.next(1);
// "A next: 1"
subject.next(2);
// "A next: 2"
subject.next(3);
// "A next: 3"
setTimeout(() => {
subject.subscribe(observerB);
// "B next: 3" 如果是普通的subject则不会输出此行
},3000)
复制代码
ReplaySubject:在某些时候我们会希望 Subject 代表事件,但又能在新订阅时重新发送最后的几个元素
var subject = new Rx.ReplaySubject(2); // 重复发送最后两个元素
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
subject.subscribe(observerA);
subject.next(1);
// "A next: 1"
subject.next(2);
// "A next: 2"
subject.next(3);
// "A next: 3"
setTimeout(() => {
subject.subscribe(observerB);
// "B next: 2"
// "B next: 3"
},3000) //ReplaySubject 只是事件的重放而已。
复制代码
AsyncSubject: 会在subject结束后送出最后一个值,行为和promise很像
var subject = new Rx.AsyncSubject();
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
subject.subscribe(observerA);
subject.next(1);
subject.next(2);
subject.next(3);
subject.complete();
// "A next: 3"
// "A complete!"
setTimeout(() => {
subject.subscribe(observerB);
// "B next: 3"
// "B complete!"
},3000)
复制代码
multicast:可以用来挂载 subject 并回传一个可连结(connectable)的 observable
var source = Rx.Observable.interval(1000)
.take(3)
.multicast(new Rx.Subject());
var observerA = {
next: value => console.log('A next: ' + value),
error: error => console.log('A error: ' + error),
complete: () => console.log('A complete!')
}
var observerB = {
next: value => console.log('B next: ' + value),
error: error => console.log('B error: ' + error),
complete: () => console.log('B complete!')
}
source.subscribe(observerA); // subject.subscribe(observerA)
source.connect(); // source.subscribe(subject)
setTimeout(() => {
source.subscribe(observerB); // subject.subscribe(observerB)
}, 1000);
复制代码