录屏在《JS深入浅出》里,需要花钱购买。
原版视频:Async JavaScript at Netflix by Jafar Husain
原视频语速极快,我反复听了好几遍。本节课就是我对该演讲的翻译和理解。
我觉得这个视频对 Rx.js 新手实在是太友好了,基本一听就懂,所以花了一天时间汉化以及重新演绎。
本节课主要关注如何通过「换一种思路」来解决「异步」问题。
我们的所有网页应用都是异步的:
function play(movieId, cancelButton, callback){ let movieTicket let playError let tryFinish = () =>{ if(playError){ callback(null, 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() }) }
我们可以看到,异步编程中的状态(state)是很难跟踪的
当项目变复杂时,你很难理解某个状态是如何变化的。
另一方面,使用回调时,try...catch 语法基本是没用的
另外,如果你监听了一个事件却忘了销毁它,很容易造成内存泄露。这在异步编程很常见。
为了解决这些问题,让我们回到 1994 年。1994 年有一本书叫做《设计模式》
这本书讲了很多编程套路(编程套路就是设计模式)
今天我们只关注其中的两个设计模式
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)
function* idMaker() { var index = 0; while(true) yield index++; } var gen = idMaker(); console.log(gen.next().value); // 0 console.log(gen.next().value); // 1 console.log(gen.next().value); // 2
所谓迭代器模式就是你可以用 .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(); } }); });
假设 A 是一个迭代器,那么 B 可以主动使用 A.next() 来要求 A 产生变化。(B主动要求A变化)
假设 B 是一个观察者,在观察着 A,那么 A 一旦变化,A 就会主动通知 B。(A变化之后B被动接收通知)
或者这么说:在观察者模式里,被观察的人在迭代观察者(调用观察者的一个函数)。
再说清楚一点:观察者就是一个迭代器,被观察的人一旦有变化,就会调用观察者的一个函数。
user .on change observer.next()
只不过,观察者永远可以 .next(),不会结束。而迭代器是会结束的,即返回 {done: true}
。
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 个操作:
用这几个 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 = Collections + Time
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 还可以接收两个额外的参数:
看起来有点像 Promise 对吧。
为了跟清楚地阐述如何使用 forEach/map/filter/takeUntil/concatAll 等 API 来操作 Observable 对象,我现在发明一种新的语法:
这个语法的规则是
> {1......2............3}.forEach(console.log) 1 一段时间后 一段时间后 2 一段时间后 一段时间后 一段时间后 一段时间后 3
> {1......2............3}.map(x=>x+1) 2 一段时间后 一段时间后 3 一段时间后 一段时间后 一段时间后 一段时间后 4
> {1......2............3}.filter(x=>x>1) 一段时间后 一段时间后 2 一段时间后 一段时间后 一段时间后 一段时间后 3
考虑 race conditions(竞态问题)
{ ...{1} ......{2..................3} ............{} ..................{4} }.concatAll() { ...1...2..................3...4 }
> {...1...2............3}.takeUntil( {............4} ) {...1...2...}
这个 API 给我们一个启发:其实我们根本不需要去取消订阅,只需要告诉系统订阅到什么情况为止就行了;就是在另一个事件出现时,系统自动取消上一个事件的订阅即可。
实际上很多人都不会去取消订阅,以至于出现内存泄露。比如我们经常监听 window 的 onload 事件,却基本没人去取消监听 window 的 onload 事件。
我已经有五年没有做过「取消监听」这件事了。但是我的代码却没有内存泄露,因为:
再回头看我们的 drag 代码:
let getElemenetDrags = el => el.mouseDowns .map( mouseDown => document.mouseMoves .takeUntil(document.mouseUps) ) .concatAll() getElementDrags(div) .forEach(position => img.position = position )
这个 demo 的难点有两个:
使用 Observable 来思考这个问题
let search = keyPresses .debounce(250) // 原文是 throttle,但我个人认为原文写错了,我已经在 Twitter 上询问了演讲者,尚未得到回复 .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(null, 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 的思想,如果你需要更实际的练习,你可以点击以下链接:
饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 xiedaimala03,官网:https://jirengu.com
本文作者:饥人谷方应杭老师