通过「换一种思路」来解决「异步」问题
Rx.js比async还要好
我们的所有网页应用都是异步的:
脚本加载
播放器
数据访问
动画
DOM事件绑定、数据事件绑定
异步编程
image.png
我们可以看到,异步编程中的状态(state)是很难跟踪的
image.png
三处用到了movieTicket变量
当项目变复杂时,你很难理解某个状态是如何变化的。
另一方面,使用回调时,try...catch 语法基本是没用的
image.png
这是异步的,try catch捕获不了
另外,如果你监听了一个事件却忘了销毁它,很容易造成内存泄露。这在异步编程很常见。
image.png
只要按钮不消失,匿名函数就不会消失
为了解决这些问题,让我们回到 1994 年。1994 年有一本书叫做《设计模式》
image.png
这本书讲了很多编程套路(编程套路就是设计模式)
image.png
书中介绍的所有设计模式之间的关系
这里只关注其中的两个设计模式
Iterator 迭代器
Observer 观察者
迭代器
function makeIterator(array){
var nextIndex = 0;
return {
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
}
var it = makeIterator(['a', 'b']);
console.log(it.next().value); // 'a'
console.log(it.next().value); // 'b'
console.log(it.next().done); // true
ES 6 提供了一个语法糖来达成迭代器模式,这个语法糖叫做生成器(Generator)
image.png
image.png
所谓迭代器模式就是你可以用 .next() API 来「依次」访问下一项。(next只是一个函数名而已,可以随意约定)
如果有下一项,你就会得到 {value: 下一项的值, done: false}
如果没有下一项,你就会得到 {value: null, done: true}
观察者模式
这个模式则是监听一个对象的变化,一旦对象发生变化,就调用你提供的函数。(JS 已废弃 Object.observe(),请使用 Proxy API 代替)
var user = {
id: 0,
name: 'Brendan Eich',
title: 'Mr.'
};
// 创建用户的greeting
function updateGreeting() {
user.greeting = 'Hello, ' + user.title + ' ' + user.name + '!';
}
updateGreeting();
Object.observe(user, function(changes) {
changes.forEach(function(change) {
// 当name或title属性改变时, 更新greeting
if (change.name === 'name' || change.name === 'title') {
updateGreeting();
}
});
});
image.png
两种模式的区别
假设 A 是一个迭代器,那么 B 可以主动使用 A.next() 来要求 A 产生变化。(B主动要求A变化)
假设 B 是一个观察者,在观察着 A,那么 A 一旦变化,A 就会主动通知 B。(A变化之后B被动接收通知)
或者这么说:在观察者模式里,被观察的人在迭代观察者(调用观察者的一个函数)。
再说清楚一点:观察者就是一个迭代器,被观察的人一旦有变化,就会调用观察者的一个函数。
user .on change
observer.next()
只不过,观察者永远可以 .next(),不会结束。而迭代器是会结束的,即返回 {done: true}
数组VS事件
Array: [ {x:1,y:1}, {x:2, y:2}, {x:10,y:10} ]
Event: {x:1,y:1} ... {x:2, y:2} ... {x:10, y:10}
数组和事件,有啥区别?
他们都是 collection(数据集、集合)。
为了阐述它俩之间的相同点,我们来举两个例子。
首先我们介绍 Array 的 4 个操作:
forEach
[1,2,3].forEach(x=> console.log(x))
1
2
3
map
[1,2,3].map(x=> x+1)
[2,3,4]
filter
[1,2,3].filter(x => x>1)
[2,3]
concatAll(这不是标准 API,不过很容易实现这个 API)
[ [1] , [2,3], [], [4] ].concatAll()
[1,2,3,4]
用这几个 API 我们可以做一些 amazing 的事情,在 Netflix 我们主要向用户展示一些好看的剧集:
我们需要展示评分最高的剧集给用户。能不能用上面的操作做到呢?
let getTopRatedFilms = user =>
user.videoLists
.map( videoList =>
videoList.videos
.filter( video => video.rating === 5.0)
).concatAll()
getTopRatedFilms(currentUser)
.forEach(film => console.log(film) )
好,如果我现在告诉你,一个拖曳操作能用类似的代码实现,你相信吗?
let getElemenetDrags = el =>
el.mouseDowns
.map( mouseDown =>
document.mouseMoves
.takeUntil(document.mouseUps)
)
.concatAll()
getElementDrags(div)
.forEach(position => img.position = position )
能做到这一切,都是因为 Observable(大意:可被观察的对象)
Observable
Observable = Collections + Time
image.png
用途
Observable 可以表示
事件
数据请求
动画
而且可以方便的把这三种东西组合起来,因此,异步操作变得很简单。
将事件转化为 Observable 的 API 很简单
var mouseDowns = Observable.fromEvent(element, 'mouseDown')
之前我们是如何操作事件的?——监听(或者叫做订阅)
// 订阅或监听
let handler = e => console.log(e)
document.addEventListener('mousemove', handler)
// 取消订阅或去掉监听
document.removeEventListener('mousemove', handler)
现在我们怎么对事件进行操作呢?——forEach
// 订阅
let subscription = mouseMoves.forEach(e => console.log(e) )
// 取消订阅
subscription.dispose()
将事件包装成 Observable 对象,可以方便地使用 forEach/map/filter/takeUntil/concatAll 等 API 来操作,比之前的方式容易很多。
为了处理失败情况,forEach 还可以接收两个额外的参数:
image.png
看起来有点像 Promise 对吧。
为了跟清楚地阐述如何使用 forEach/map/filter/takeUntil/concatAll 等 API 来操作 Observable 对象,我现在发明一种新的语法:
image.png
这个语法的规则是
{1...2} 表示这个对象会一开始发射一个1,一段时间后发射一个2
{1...2......3}表示发射1,一段时间后发射2,两段时间后发射3(也就是说 ... 表示一段时间,...... 表示两段时间)
forEach
{1......2............3}.forEach(console.log)
1
一段时间后
一段时间后
2
一段时间后
一段时间后
一段时间后
一段时间后
3
map
{1......2............3}.map(x=>x+1)
2
一段时间后
一段时间后
3
一段时间后
一段时间后
一段时间后
一段时间后
4
filter
{1......2............3}.filter(x=>x>1)
一段时间后
一段时间后
2
一段时间后
一段时间后
一段时间后
一段时间后
3
image.png
image.png
image.png
image.png
image.png
自动搜索建议
image.png
这个 demo 的难点有两个:
如果用户依次输入 abcdef,请问你应该发送几个请求?答案是用函数防抖,发一次请求。
如果用户输入 a,然后 300 毫秒后输入 b,那么你会发两个请求,一个请求查询 a 相关的热词,一个请求查询 ab 相关的热词,你能保证这两个请求响应的顺序吗?答案是不能。(竞态问题)
使用 Observable 来思考这个问题
let search =
keyPresses
.debounce(250) //
.map(key =>
getJSON('/search?q=' + input.value)
.retry(3)
.takeUntil(keyPresses)
)
.concatAll()
search.forEach(
results => updateUI(results),
error => showMessage(error)
)
最开始的回调地狱
最后我们本文回到最开始的代码
function play(movieId, cancelButton, callback){
let movieTicket
let playError
let tryFinish = () =>{
if(playError){
callback(playError)
}else if(movieTicket && player.initialized){
callback(null, movieTicket)
}
}
cancelButton.addEventListener('click', ()=>{ playError = 'cancel' })
if(!player.initialized){
player.init((error)=>{
playError = error
tryFinish()
})
}
authorizeMovie(movieId, (error, ticket)=>{
playError = error
movieTicket = ticket
tryFinish()
})
}
通过改变思维方式,你可以写出这样的代码
let authorizations =
player.init()
.map(()=>
playAttempts
.map(movieId=>
player.authorize(movieId)
.retry(3)
.takeUntil(cancels)
)
.concatAll()
)
.concatAll()
authorizations.forEach(
license => player.play(license),
error => showError(error)
)
Rx.js的教程
- 英文教程:http://reactivex.io/learnrx/ (演讲者自己写的教程)
- 中文教程:https://www.google.com/search?q=site%3Azhihu.com+太狼+rxjs (我推荐太狼在知乎上写的 Rx.js 教程)
网友评论