关于rxjs:你会用RxJS吗初识-RxJS中的Observable和Observer

概念RxJS是一个库,能够应用可察看队列来编写异步和基于事件的程序的库。RxJS 中治理和解决异步事件的几个关键点: Observable: 示意将来值或事件的可调用汇合的概念。Observer: 是一个回调汇合,它晓得如何监听 Observable 传递的值。Subscription: 示意一个 Observable 的执行,次要用于勾销执行。Operators:** 是纯函数,能够应用函数式编程格调来解决具备map、filter、concat、reduce等操作的汇合。Subject: 相当于一个EventEmitter,也是将一个值或事件多播到多个Observers的惟一形式。Schedulers: 是管制并发的集中调度程序,容许咱们在计算产生在 eg setTimeoutor requestAnimationFrame或者其它上时进行协调。 牛刀小试咱们通过在dom上绑定事件的小案例,感受一下Rxjs的魅力。 在dom绑定事件,咱们通常这样解决 document.addEventListener('click', () => console.log('Clicked!'));复制代码用Rxjs创立一个observable,内容如下import { fromEvent } from 'rxjs'; fromEvent(document, 'click').subscribe(() => console.log('Clicked!'));复制代码 这时候咱们简略降级一下,须要记录一下点击的数量 let count = 0;document.addEventListener('click', () => console.log(Clicked ${++count} times));复制代码用Rxjs能够隔离状态,import { fromEvent, scan } from 'rxjs'; fromEvent(document, 'click') .pipe(scan((count) => count + 1, 0)) .subscribe((count) => console.log(Clicked ${count} times));复制代码能够看到,咱们用到了scan操作符,该操作符的工作形式和数组的reduce相似,回调函数接管一个值, 回调的返回值作为下一次回调运行裸露的一个值。通过下面的案例能够看出,RxJS的弱小之处在于它可能应用纯函数生成值。这意味着您的代码不太容易出错。 通常你会创立一个不纯的函数,你的代码的其余局部可能会弄乱你的状态。 这时候,需要又有变动了,要求咱们一秒内只能有一次点击 let count = 0;let rate = 1000;let lastClick = Date.now() - rate;document.addEventListener('click', () => { if (Date.now() - lastClick >= rate) { ...

August 17, 2022 · 2 min · jiezi

关于rxjs:Rxjs源码解析一Observable

从 new Observable 开始import { Observable } from 'rxjs' const observable = new Observable<number>(subscriber => { subscriber.next(1) subscriber.next(2) subscriber.complete()})observable.subscribe({ next: data => console.log('next data:', data), complete: () => { console.log('complete')}}) 输入如下:// 开始输入next data: 1next data: 2complete// 完结输入 通过 new Observable() 办法创立了一个可察看对象 observable,而后通过 subscribe 办法订阅这个observable,订阅的时候会执行在 new Observable时候传入的函数参数,那么就来看下 new Observable到底做了什么// /src/internal/Observable.tsexport class Observable<T> implements Subscribable<T> { // ... constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic) { if (subscribe) { this._subscribe = subscribe;}} // ...} ...

July 1, 2022 · 5 min · jiezi

关于rxjs:Rxjs-SwitchMap-的一些容易犯的错误和替代方案

上面是一个在 Effect 里应用 SwitchMap 的例子:从购物车里移除某个行我的项目 @Effect()public removeFromCart = this.actions.pipe( ofType(CartActionTypes.RemoveFromCart), switchMap(action => this.backend .removeFromCart(action.payload) .pipe( map(response => new RemoveFromCartFulfilled(response)), catchError(error => of(new RemoveFromCartRejected(error))) ) ));购物车列出了用户打算购买的商品,每个商品都有一个从购物车中删除商品的按钮。 单击该按钮会将 RemoveFromCart 操作分派给与应用程序后端通信的对应 API,并查看从购物车中删除的我的项目。 这段代码看似可能失常运行,但实际上 switchMap 的应用,引入了竞态条件(race condition)。 如果用户单击购物车中多个我的项目的删除按钮,会呈现什么样的行为? 依据客户点击按钮的速度不同,应用程序可能会: 从购物车中删除所有点击的物品,比方客户点击一个行我的项目的删除按钮,等删除操作在后盾胜利执行之后,再点击第二个行我的项目。客户飞快地点击了前两个行我的项目的删除按钮。第一个行我的项目的删除申请正在发送往后台服务器的过程当中,则第二个按钮的点击,会勾销第一个行我的项目的删除申请。最初仅仅第二个行我的项目被删除了。客户顺次点击了前两个行我的项目的删除按钮。第一个删除申请曾经到达后盾,正在执行后盾的删除操作。第二个申请也达到了后盾。此时的行为,取决于后盾 API 从 cart 上删除行我的项目时,是否给以后的 cart 加了锁。咱们考虑一下是否能用如下的 Operator 来代替 SwitchMap. mergeMap/flatMap如果 switchMap 被 mergeMap 替换,则 effect 的代码将同时解决每个调度的动作。 也就是说,pending 的删除不会被停止;后端申请将同时产生。申请实现时,Effect 会 dispatch 对应的 action. 须要留神的是,因为操作的并发解决,响应的程序可能与申请的程序不匹配。 例如,如果用户单击第一个和第二个我的项目的删除按钮,则第二个我的项目的删除可能产生在第一个我的项目的删除之前。 对于购物车里删除行我的项目的场景而言,删除的程序并不重要,因而应用 mergeMap 而不是 switchMap 能够修复该谬误,躲避潜在的竟态条件。 concatMap从购物车中移除商品的程序可能无关紧要,但通常有一些操作对排序很重要。 例如,如果咱们的购物车有一个减少商品数量的按钮,那么以正确的程序解决分派的操作很重要。 否则,前端购物车中的数量最终可能与后端购物车中的数量不同步。 对于排序很重要的操作,应应用 concatMap. ...

April 29, 2022 · 1 min · jiezi

关于rxjs:rxjs-里-Skip-操作符的一个使用场景

skip 操作符容许咱们疏忽源的前 x 个排放。 当咱们有一个始终在 subscription 上收回心愿疏忽的某些值的可察看对象时,就能够应用这个操作符。比方 Observable emit 的前几个值并不是咱们感兴趣的值,另一种状况是咱们订阅了 Replay 或 BehaviorSubject,并且不须要对初始值进行操作,而只关怀初始值之后的数据 emit. 这种状况下,skip 操作符十分有用。 有时候咱们能够通过应用带有索引的 filter 操作符来达到和应用 skip 同样的成果: filter((val, index) => index > 1)来看看一个事实我的项目中的例子。 应用 skip 组合出的 Observable 代码如下: combineLatest([ data$.pipe(startWith(null)), loading$,]).pipe( takeWhile(([data, loading]) => !data || loading, true), map(([data, loading]) => loading ? null : data), skip(1), distinctUntilChanged(),); 下面的代码执行时候三种不同的状况。 加载工夫不到 1 秒。咱们的初始 null 被 skip(1) 跳过,并且 data$ 在 loader 收回 true 之前收回了 true. 这意味着 takeWhile 条件失败,咱们终止让数据通过的流(数据是 not falsy,loading 是 false).加载耗时 1.5 秒。当初咱们有 data$ 收回 null 并且 loading 是 true. 这合乎 takeWhile 条件并被映射为 null。咱们应用这个 null 来显示宏流中的 loading. 下一个 data$ 收回该值,但加载依然为真。所以 takeWhile 容许它,并且该值再次映射到 null ,该 null 由 distinctUntilChanged 过滤。整秒过后,加载会收回 false 并 takeWhile 终止流。最初一次发射被映射到 data$ 上次发射的值,咱们暗藏加载指示器并显示数据。加载工夫超过 2 秒。结尾是一样的,然而咱们当初加载的不是 data$ 收回的值,而是收回 false ,因为不再须要显示加载批示符。然而数据依然为空,因而 takeWhile 放弃流处于活动状态并将其映射为空。然而一旦咱们从 data$ 中取得值——流就实现了,map 返回咱们想要显示的理论数据。

April 15, 2022 · 1 min · jiezi

关于rxjs:rxjs-里-CombineLatest-操作符的一个使用场景

一个具体的例子: combineLatest([ data$.pipe(startWith(null)), loading$,]).pipe( takeWhile(([data, loading]) => !data || loading, true), map(([data, loading]) => loading ? null : data), skip(1), distinctUntilChanged(),);咱们在这里应用奇妙的 takeWhile 函数。 它只会让带有虚伪数据(falsy data)(初始 null )或 truthy loading 的发射值通过,这正是咱们须要显示 spinner 的时候。 当条件不满足时,因为第二个参数,咱们也让最终值通过。 而后咱们应用 map 函数映射后果。 如果 loading 标记位为 true,则 map 返回的值为 null,这很正当,因为 loading 的时候是必定没有数据返回的。当 loading 标记位为 false,阐明数据返回结束,则返回实在的 data. 咱们应用 skip(1) 是因为咱们不心愿咱们的 startWith(null) 数据流通过。 咱们应用 distinctUntilChanged 所以多个空值也不会通过。 这里波及到的知识点: startWith一个例子: // RxJS v6+import { startWith } from 'rxjs/operators';import { of } from 'rxjs';//emit (1,2,3)const source = of(1, 2, 3);//start with 0const example = source.pipe(startWith(0));//output: 0,1,2,3const subscribe = example.subscribe(val => console.log(val));下面的 example 订阅后,会打印通过 startWith 传入的初始值 0,而后是 of 包裹的1,2,3 ...

April 15, 2022 · 2 min · jiezi

关于rxjs:rxjs

对于 rxjs 的好问题我集体认为,这样的解释形式是很好的: 1 间接聊返回值类型 The map operators emits value as observable. The SwitchMap creates a inner observable, subscribes to it and emits its value as observable. 12 聊生产关系 对(按钮事件)按钮数据流的生产关系 1 3 间接聊 order (辨析) mergeMap does not care about the order.1concatMap care about the outer observable's order112其它1 聊 merge stream 基本聊不清其它 这个 marble 图对我来说如同没什么意义 1齐全不如后果图 1 2

March 18, 2022 · 1 min · jiezi

关于rxjs:80-行代码实现简易-RxJS

RxJS 是一个响应式的库,它接管从事件源收回的一个个事件,通过解决管道的层层解决之后,传入最终的接收者,这个解决管道是由操作符组成的,开发者只须要抉择和组合操作符就能实现各种异步逻辑,极大简化了异步编程。除此以外,RxJS 的设计还遵循了函数式、流的理念。 间接讲概念比拟难了解,不如咱们实现一个繁难的 RxJS 再来看这些。 RxJS 的应用RxJS 会对事件源做一层封装,叫做 Observable,由它收回一个个事件。 比方这样: const source = new Observable((observer) => { let i = 0; setInterval(() => { observer.next(++i); }, 1000);});复制代码在回调函数外面设置一个定时器,一直通过 next 传入事件。 这些事件会被接受者监听,叫做 Observer。 const subscription = source.subscribe({ next: (v) => console.log(v), error: (err) => console.error(err), complete: () => console.log('complete'),});复制代码observer 能够接管 next 传过来的事件,传输过程中可能有 error,也能够在这里解决 error,还能够解决传输实现的事件。 这样的一个监听或者说订阅,叫做 Subscription。 能够订阅当然也能够勾销订阅: subscription.unsubscribe();复制代码勾销订阅时的回调函数是在 Observable 里返回的: const source = new Observable((observer) => { let i = 0; const timer = setInterval(() => { observer.next(++i); }, 1000); return function unsubscribe() { clearInterval(timer); };});复制代码发送事件、监听事件只是根底,处理事件的过程才是 RxJS 的精华,它设计了管道的概念,能够用操作符 operator 来组装这个管道: ...

February 28, 2022 · 5 min · jiezi

关于rxjs:RxJs-操作符-withLatestFrom-在-SAP-电商云-Spartacus-UI-中的应用

看上面这段代码: getSupportedDeliveryModes(): Observable<DeliveryMode[]> { return this.checkoutStore.pipe( select(CheckoutSelectors.getSupportedDeliveryModes), withLatestFrom( this.checkoutStore.pipe( select(getProcessStateFactory(SET_SUPPORTED_DELIVERY_MODE_PROCESS_ID)) ) ), tap(([, loadingState]) => { if ( !(loadingState.loading || loadingState.success || loadingState.error) ) { this.loadSupportedDeliveryModes(); } }), pluck(0), shareReplay({ bufferSize: 1, refCount: true }) ); }调用 withLatestFrom 的 Observable 对象,起到主导数据产生给上游观察者的作用。作为参数被调用的 Observable 对象只能奉献新的数据,而不能控制数据的产生机会。 换句话说,上述 Spartacus 的例子,CheckoutSelectors.getSupportedDeliveryModes Observable 对象是向上游产生数据的主导者,而 select(getProcessStateFactory(SET_SUPPORTED_DELIVERY_MODE_PROCESS_ID 只是数据片段的贡献者。 下图第 54 行的语法是元祖,元祖也是数组,但各个元素的数据类型不肯定必须雷同。 第 54 行的 loadingState,代表的就是从 ngrx store 里取出的 setDeliveryModeProcess 的状态。第 55 行的语义是,如果状态是 loading 或者 胜利,或者是 error ,则不做任何事件,否则调用 58 行的 loadSupportedDeliveryModes, 进行 mode 的加载。 ...

February 15, 2022 · 1 min · jiezi

关于rxjs:使用-RxJs-实现一个支持-infinite-scroll-的-Angular-Component

首先看看我这个反对 infinite scroll 的 Angular 利用的运行时成果: https://jerry-infinite-scroll... 滚动鼠标中键,向下滚动,能够触发 list 一直向后盾发动申请,加载新的数据: 上面是具体的开发步骤。 (1) app.component.html 的源代码: <div> <h2>{{ title }}</h2> <ul id="infinite-scroller" appInfiniteScroller scrollPerecnt="70" [immediateCallback]="true" [scrollCallback]="scrollCallback" > <li *ngFor="let item of news">{{ item.title }}</li> </ul></div>这里咱们给列表元素 ul 施加了一个自定义指令 appInfiniteScroller,从而为它赋予了反对 infinite scroll 的性能。 [scrollCallback]="scrollCallback" 这行语句,前者是自定义执行的 input 属性,后者是 app Component 定义的一个函数,用于指定当 list 的 scroll 事件产生时,应该执行什么样的业务逻辑。 app component 里有一个类型为汇合的属性 news,被 structure 指令 ngFor 开展,作为列表行我的项目显示。 (2) app Component 的实现: import { Component } from '@angular/core';import { HackerNewsService } from './hacker-news.service';import { tap } from 'rxjs/operators';@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'],})export class AppComponent { currentPage: number = 1; title = ''; news: Array<any> = []; scrollCallback; constructor(private hackerNewsSerivce: HackerNewsService) { this.scrollCallback = this.getStories.bind(this); } getStories() { return this.hackerNewsSerivce .getLatestStories(this.currentPage) .pipe(tap(this.processData)); // .do(this.processData); } private processData = (news) => { this.currentPage++; this.news = this.news.concat(news); };} ...

February 14, 2022 · 3 min · jiezi

关于rxjs:combineLatest-使用的一个陷阱和基于-debounceTime-的解决方案

首先理解 combineLatest 这个操作符的作用: 组合多个 Observable 以创立一个 Observable,其值是依据其每个输出 Observable 的最新值计算得出的。 其弹珠图如下图所示: 咱们有一个限度值流和一个偏移值流。 咱们应用 combineLatest 组合这些流以创立一个流,该流将在每次源流之一更改时具备一个新值。 而后咱们应用 switchMap 依据这些值从后端获取数据以获取 pokemon$。 因为咱们应用了switchMap,如果一个调用还没有完结,那么当一个新的调用通过扭转limit或者offset来发动一个新的调用时,前一个调用就会被勾销。 代码如下: this.pokemon$ = combineLatest(limit$, offset$) .pipe( map(data => ({limit: data[0], offset: data[1]})), switchMap(data => this.pokemonService.getPokemon(data.limit, data.offset)), map((response: {results: Pokemon[]}) => response.results), );代码地址如下: https://stackblitz.com/edit/a... 当我批改 limit 和 offset 为其余值之后,点击 reset 按钮,此时会察看到先后发动两个申请,并且第一个申请主动被 cancel 的状况: 通过单击重置按钮,咱们通过同时重置限度和偏移值来更新咱们的两个源流。 这个动作的成果是 combineLatest 创立的流触发了两次,因而启动了两个后端申请,另一方面,因为咱们应用了 switchMap,立刻勾销了一个。 咱们来单步拆解,以加深印象: combineLatest 保留所有源流的最初一个值。比方开始场景是,limit = 8,offset = 2)单击重置按钮limit 设置为 5combineLatest 看到一个新值进入 limit 并收回一个新组合,limit = 5,offset = 2switchMap 获取这些值并订阅触发后端调用的流偏移量设置为 0combineLatest 看到一个新的 offset 值,并收回一个新的组合,limit = 5,offset = 0switchMap 获取这些值,勾销订阅(并因而勾销)先前的申请并开始新的申请在此流程中您可能没有预料到的是,无论何时设置 limit ,此更改都会在更改 offset 之前间接流传到 combineLatest. ...

February 14, 2022 · 1 min · jiezi

关于rxjs:使用-RxJs-Observable-来避免-Angular-应用中的-Promise-使用

咱们通过一个具体的例子来论述。 思考您正在构建一个搜寻输出掩码,该掩码应在您键入时立刻显示后果。 如果您已经构建过这样的货色,那么您可能会意识到该工作带来的挑战。 不要在每次击键时都点击搜寻端点将搜寻端点视为您按申请付费。不论它是不是你本人的硬件。咱们不应该比须要的更频繁地敲击搜寻端点。基本上咱们只想在用户进行输出后点击它,而不是每次击键时点击它。 不要在后续申请中应用雷同的查问参数命中搜寻端点假如您键入 foo,进行,键入另一个 o,而后立刻退格并返回到 foo。这应该只是一个带有 foo 一词的申请,而不是两个,即便咱们在搜寻框中有 foo 后从技术上讲进行了两次。 3.解决乱序响应 当咱们同时有多个申请进行中时,咱们必须思考它们以意外程序返回的状况。思考咱们首先键入 computer,进行,申请收回,咱们键入 car,进行,申请收回。当初咱们有两个正在进行的申请。可怜的是,在为 car 携带后果的申请之后,为 computer 携带后果的申请又回来了。这可能是因为它们由不同的服务器提供服务。如果咱们不正确处理此类情况,咱们最终可能会显示 computer 的后果,而搜寻框会显示 car. 咱们将应用收费和凋谢的维基百科 API 来编写一个小演示。 为简略起见,咱们的演示将只蕴含两个文件:app.ts 和 wikipedia-service.ts。 不过,在事实世界中,咱们很可能会将事件进一步拆分。 让咱们从一个基于 Promise 的实现开始,它不解决任何形容的边缘状况。 这就是咱们的 WikipediaService 的样子。 应用了 jsonp 这个 Angular HTTP 服务: 上图将来自 angular/http 库中的 jsonp 返回的对象,应用 toPromise 办法转换成了 promise. 简略地说,咱们正在注入 Jsonp 服务,以应用给定的搜索词针对维基百科 API 收回 GET 申请。 请留神,咱们调用 toPromise 是为了从 Observable\<Response\> 到 Promise\<Response\>。 通过 then-chaining 咱们最终失去一个 Promise\<Array\<string\>\> 作为咱们搜寻办法的返回类型。 到目前为止一切顺利,让咱们看看保留咱们的 App 组件的 app.ts 文件。 ...

December 13, 2021 · 2 min · jiezi

关于rxjs:从一个实际的例子触发理解什么是-Rxjs-的-defer-函数

咱们在开发简单的 Angular 利用时,常常会应用到 Rxjs 的 defer 函数,例如: 创立一个 Observable,在订阅时调用 Observable 工厂为每个新的 Observer 创立一个 Observable 对象。 该函数接管一个输出参数,类型为一个工厂函数。输入为一个 Observable 对象,一旦被订阅时,其绑定的工厂函数会被调用。 defer 的本质是提早创立机制,即只有在返回的 Observable被订阅时,才开始创立 Observable 对象。 defer 容许你只在 Observer 订阅时创立一个 Observable。 它始终等到 Observer 订阅它,调用给定的工厂函数来获取一个 Observable —— 工厂函数通常会生成一个新的 Observable —— 并将 Observer 订阅到这个 Observable。 如果工厂函数返回一个假值,则应用 EMPTY 作为 Observable 代替。 最初但并非最不重要的是,工厂函数调用期间的异样通过调用 error 传递给观察者。 看上面这个具体的例子。 咱们来单步调试下下面这段代码。首先进入 defer 外部执行逻辑: 在 defer 外部,间接结构一个新的 Observable,并且将工厂函数传入。该工厂函数在第8行被调用,用于生成一个蕴含应用程序业务逻辑的 Observable 对象,存储在 input 里。最初,将应用程序的subscriber 订阅到这个工厂函数返回的 Observable 上。 咱们再单步执行,发现程序执行流从上图的第5行,跳转到了 第16行。这体现了 defer 函数提早创立 Observable 对象的行为。所谓提早创立,精确的说,应该是提早了蕴含业务逻辑的 Observable 对象的创立。 ...

November 21, 2021 · 1 min · jiezi

关于rxjs:Rxjs-里-Subject-和-BehaviorSubject-的区别

通过一个理论的例子来了解。 上面的代码,创立了一个新的 subject,而后调用 next 办法,多播给其所有的监听者。 import { Subject } from 'rxjs';const jerry = new Subject();const subscription = jerry.subscribe((data) => console.log(data));console.log('ok');jerry.next(111);jerry.next(222);subscription.unsubscribe();console.log('?');jerry.next(333);上文的例子,会打印 111,222 如果 Subject 在被订阅之前就开始多播(即下图第5行的 111),那么这些多播值,不会被开始多播之后的订阅者收到。如下图所示:订阅者只会打印其订阅 subject 之后收到的多播值 222: 应用 BehaviorSubject,就能够防止这个问题:即便订阅者订阅该 subject 之前,后者就开始调用 next 进行多播,这些多播值同样可能被订阅者接管到: 更多Jerry的原创文章,尽在:"汪子熙":

November 7, 2021 · 1 min · jiezi

关于rxjs:了解rxjs中的defer

上面介绍一个少有人晓得的observable -- defer,如何应用,什么时候应用读这篇文章之前,你须要对rxjs根底用法有肯定的理解 假如咱们须要写一个自定义operator叫做tapOnce。接管一个函数当作参数,只有流的第一次触发时才执行 function tapOnce(fn: Function) { let run = false; return function (source: Observable<any>) { return source.pipe( tap((data) => { if (!run) { fn(data); run = true; } }) ); };}这段代码简略直观,在tap的根底上,用了一个变量来管制执行次数,调用一下 const test$ = interval(1000).pipe(tapOnce((d) => console.log('', d)));test$.subscribe();// 1s之后打印 0运行很失常,在流第一次触发的时候打印狗头。要是再加一个订阅者呢? const test$ = interval(1000).pipe(tapOnce((d) => console.log('', d)));test$.subscribe();test$.subscribe();// 1s之后打印 0后果只打印了一遍,这是因为两个订阅者订阅同一个流,应用同一个run变量。想要打印两遍,咱们就须要一个可能在订阅时才创立流的性能。defer就是用来做这件事的改良一下 function tapOnce(fn: Function) { return function (source: Observable<any>) { return defer(() => { let run = false; return source.pipe( tap((data) => { if (!run) { fn(data); run = true; } }) ); }); };}defer接管一个返回类型为observable的函数。只有当defer被订阅了,函数才会执行。而不是在创立时。而后利用js闭包,让每个订阅者有本人的作用域。 ...

October 11, 2021 · 1 min · jiezi

关于rxjs:RxJs-SwitchMapTo-操作符之移花接木

将每个源值投影到同一个 Observable,该 Observable 在输入 Observable 中应用 switchMap 屡次展平。 输出一个 Observable,输入一个 function Operator. 理论是一个函数,每次在源 Observable 上收回值时,该函数都会返回一个 新的 Observable. 该函数从给定的 innerObservable 收回我的项目,并且仅从最近投影的外部 Observable 中获取值。 看个例子: import { EMPTY, range } from 'rxjs';import { first, take, tap } from 'rxjs/operators';import { fromEvent, interval } from 'rxjs';import { switchMapTo } from 'rxjs/operators';const clicks = fromEvent(document, 'click');const test = event => console.log('Jerry: ', event);const result = clicks.pipe( tap(test), switchMapTo(interval(1000)));result.subscribe(x => console.log(x));输入: 每次点击之后,click$ 抛出的 PointerEvent,被 switchMapTo 返回的 Function Operator 抛弃了。最初用户订阅 result 函数里,打印的值,是 switchMapTo 输出的 interval(1000) Observable 发射的值,而不再是 clicks$ 抛出的 PointerEvent. ...

September 15, 2021 · 1 min · jiezi

关于rxjs:NgRx-里-first-和-take1-操作符的区别

take(1) vs first() first() 运算符采纳可选的 predicate 函数,并在源实现后没有匹配的值时收回谬误告诉。 下列代码会报错: import { EMPTY, range } from 'rxjs';import { first, take } from 'rxjs/operators';EMPTY.pipe(first()).subscribe(console.log, err => console.log('Jerry Error:', err)); 同理,上面代码也会报错: range(1, 5).pipe( first(val => val > 6),).subscribe(console.log, err => console.log('Error', err)); 下列代码输入1: import { EMPTY, range } from 'rxjs';import { first, take } from 'rxjs/operators';range(1, 5) .pipe(first()) .subscribe(console.log, err => console.log('Error', err)); 另一方面, take(1) 只取第一个值并实现。不波及进一步的逻辑。 import { EMPTY, range } from 'rxjs';import { first, take } from 'rxjs/operators';EMPTY.pipe( take(1),).subscribe(console.log, err => console.log('Error', err));下面代码不会有任何输入: ...

September 15, 2021 · 1 min · jiezi

关于rxjs:RxJS-switchMap-mergeMap-concatMapexhaustMap-的比较

原文:Comprehensive Guide to Higher-Order RxJs Mapping Operators: switchMap, mergeMap, concatMap (and exhaustMap) 咱们日常发现的一些最罕用的 RxJs 操作符是 RxJs 高阶映射操作符:switchMap、mergeMap、concatMap 和exhaustMap。 例如,咱们程序中的大部分网络调用都将应用这些运算符之一实现,因而相熟它们对于编写简直所有反应式程序至关重要。 晓得在给定状况下应用哪个运算符(以及为什么)可能有点令人困惑,咱们常常想晓得这些运算符是如何真正工作的,以及为什么它们会这样命名。 这些运算符可能看起来不相干,但咱们真的很想一口气学习它们,因为抉择谬误的运算符可能会意外地导致咱们程序中的奥妙问题。 Why are the mapping operators a bit confusing?这样做是有起因的:为了了解这些操作符,咱们首先须要理解每个外部应用的 Observable 组合策略。 与其试图本人了解switchMap,不如先理解什么是Observable切换; 咱们须要先学习 Observable 连贯等,而不是间接深刻 concatMap。 这就是咱们在这篇文章中要做的事件,咱们将按逻辑程序学习 concat、merge、switch 和exhaust 策略及其对应的映射运算符:concatMap、mergeMap、switchMap 和exhaustMap。 咱们将联合应用 marble 图和一些理论示例(包含运行代码)来解释这些概念。 最初,您将确切地晓得这些映射运算符中的每一个是如何工作的,何时应用,为什么应用,以及它们名称的起因。 The RxJs Map Operator让咱们从头开始,介绍这些映射运算符的个别作用。 正如运算符的名称所暗示的那样,他们正在做某种映射:但到底是什么被映射了? 咱们先来看看 RxJs Map 操作符的弹珠图: How the base Map Operator works应用 map 运算符,咱们能够获取输出流(值为 1、2、3),并从中创立派生的映射输入流(值为 10、20、30)。 底部输入流的值是通过获取输出流的值并将它们利用到一个函数来取得的:这个函数只是将这些值乘以 10。 所以 map 操作符就是映射输出 observable 的值。 以下是咱们如何应用它来解决 HTTP 申请的示例: ...

July 15, 2021 · 5 min · jiezi

关于rxjs:RxJs-SwitchMap-学习笔记

网址:https://www.learnrxjs.io/lear... The main difference between switchMap and other flattening operators is the cancelling effect. On each emission the previous inner observable (the result of the function you supplied) is cancelled and the new observable is subscribed. You can remember this by the phrase switch to a new observable.switchMap 和其余扁平化操作符的次要区别在于勾销成果。 在每次发射时,先前的外部 observable(您提供的函数的后果)被勾销并订阅新的 observable。 您能够通过短语 switch to a new observable 记住这一点。 This works perfectly for scenarios like typeaheads where you are no longer concerned with the response of the previous request when a new input arrives. This also is a safe option in situations where a long lived inner observable could cause memory leaks, for instance if you used mergeMap with an interval and forgot to properly dispose of inner subscriptions. Remember, switchMap maintains only one inner subscription at a time, this can be seen clearly in the first example.这对于像事后输出这样的场景十分无效,当新输出达到时,您不再关怀先前申请的响应。 在长期存在的外部 observable 可能导致内存透露的状况下,这也是一个平安的抉择。 ...

June 22, 2021 · 3 min · jiezi

关于rxjs:RxJs-map-operator-工作原理分析

应用一个例子来钻研 map 操作符的工作原理。 举荐浏览本文之前,先浏览这篇文章RxJs fromEvent 工作原理剖析以理解相干常识。 源代码: import { Component, OnInit, Inject } from '@angular/core';import { fromEvent, combineLatest } from 'rxjs';import { mapTo, startWith, scan, tap, map } from 'rxjs/operators';import { DOCUMENT } from '@angular/common';@Component({ selector: 'app-combine-latest', templateUrl: './combine-latest.component.html'})export class CombineLatestComponent implements OnInit { readonly document: Document; constructor( // https://github.com/angular/angular/issues/20351 @Inject(DOCUMENT) document: any) { this.document = document as Document; } redTotal:HTMLElement; blackTotal: HTMLElement; total:HTMLElement; test:HTMLElement; ngOnInit(): void { this.redTotal = this.document.getElementById('red-total'); this.blackTotal = this.document.getElementById('black-total'); this.total = this.document.getElementById('total'); this.test = this.document.getElementById('test'); combineLatest(this.addOneClick$('red'), this.addOneClick$('black')).subscribe(([red, black]: any) => { this.redTotal.innerHTML = red; this.blackTotal.innerHTML = black; this.total.innerHTML = red + black; }); fromEvent(this.test, 'click').pipe(map( event => event.timeStamp)).subscribe((event) => console.log(event)); } addOneClick$ = id => fromEvent(this.document.getElementById(id), 'click').pipe( // map every click to 1 mapTo(1), // keep a running total scan((acc, curr) => acc + curr, 0), startWith(0) );}关上页面,点击 Test 按钮,能在 Chrome 控制台里看到每次点击产生时的 timestamp 工夫戳: ...

June 5, 2021 · 2 min · jiezi

关于rxjs:RxJs-fromEvent-工作原理分析

fromEvent(this.test, 'click').subscribe((event) => console.log(event));this.test 的赋值逻辑: this.test = this.document.getElementById('test');每当该 id 为 test 的按钮被点击一次,则 fromEvent issue 一个新的值,内容为 MouseClick 事件: 本文介绍这个神奇的 fromEvent 的工作原理。 在 rxjs/_esm2015/index.js 下能看到所有 rxjs 反对的 operators: fromEvent 构造函数反对最多 4 个输出参数,但我的例子里,之传入了两个。因而间接进入 Observable 对象的结构逻辑: Observable 的构造函数,只有一个输出参数,该输出参数为一个函数。这个函数是一个匿名函数,只有函数体而无函数名称。 把该匿名函数保护在 Observable 的公有属性 _subscribe 里。 fromEvent 返回一个可察看对象,调用该对象的 subscribe 办法,给其注册观察者。 上图 observerOrNext 就是咱们应用程序里,传入给 subscribe 办法的匿名函数,即应用 console.log 打印 id 为 test 的 button 被点击之后抛出的 MouseEvent 事件。 因为咱们临时没有给 fromEvent 返回的 Observable 对象指定 operator,因而第 20 行 operator 为 undefined: ...

June 5, 2021 · 1 min · jiezi

关于rxjs:如何使用RxJS间隔发送一定数量的数据

最近在开发上传进度的时候须要一个模仿数据:模仿距离发送1-100之间的值。此须要在RxJS的反对下能够轻松的实现。代码如下: let i = 0;interval(100).pipe( take(100), map(() => ++i)).subscribe(data => console.log(data));控制台如下: 最终的上传进度成果如下: 简略解释下上述代码:interval(100)为RxJS的办法,示意距离100ms发送一次数据,take(100)的作用是取前100个数据,从而达到了100ms发送一次数据,共发送100次的目标。map()操作符用于数据转换,最终将++i的值发送给上游,subscribe订阅到的便是++i的值。~~~~

March 27, 2021 · 1 min · jiezi

关于rxjs:怎么记住rxjs中的60操作符

什么是操作符,在rxjs 中 map, filter 函数都是操作符。操作符:一个操作符是返回一个Observable对象的函数。 rxjs 中有60多个操作符,在理论开发过程中该应用哪个操作符适合,把每个操作符的性能和个性都都记下来有点艰难,如果有适合的分类办法,把操作符分类,晓得每一类操作符的特点,当咱们遇到问题,依据要解决问题和各类操作符的特点,抉择适合的操作符,开发就会更高效。分类如下,当前分享每类的应用, 操作符创立类fromcreateofrangegeneraterepeat/repeatWhenthrowemptyajaxneverdeferfomPromiseintervaltimerfromEvent合并类concat/concatAllmerge/mergeAllzip/zipAllcombineLatest/conbineAll/withLatestFromracestartWithforkJoinswitch/exhaust辅助工具类countmax/minreduceeveryfind/findIndexisEmptydefaultEmpty过滤类filterfirstlasttaketakeLasttakeWhile/takeUntilskipskipWhile/skipUntilthrottleTime/debounceTime/auditTimethrottle/debounce/auditsample/sampleTimedistnctsingleelementAtignoreElementsdistnctUtilChanged/distnctUntilKeyChanged转换类mapmapTopluckwindowTime/scan/mergeScan错误处理类catchretry/retryWhenfinally多播multicastpublishLastpublishReplaypublishBehavior

August 16, 2020 · 1 min · jiezi

关于rxjs:怎么记住rxjs中的60操作符

什么是操作符,在rxjs 中 map, filter 函数都是操作符。操作符:一个操作符是返回一个Observable对象的函数。 rxjs 中有60多个操作符,在理论开发过程中该应用哪个操作符适合,把每个操作符的性能和个性都都记下来有点艰难,如果有适合的分类办法,把操作符分类,晓得每一类操作符的特点,当咱们遇到问题,依据要解决问题和各类操作符的特点,抉择适合的操作符,开发就会更高效。分类如下,当前分享每类的应用, 操作符创立类fromcreateofrangegeneraterepeat/repeatWhenthrowemptyajaxneverdeferfomPromiseintervaltimerfromEvent合并类concat/concatAllmerge/mergeAllzip/zipAllcombineLatest/conbineAll/withLatestFromracestartWithforkJoinswitch/exhaust辅助工具类countmax/minreduceeveryfind/findIndexisEmptydefaultEmpty过滤类filterfirstlasttaketakeLasttakeWhile/takeUntilskipskipWhile/skipUntilthrottleTime/debounceTime/auditTimethrottle/debounce/auditsample/sampleTimedistnctsingleelementAtignoreElementsdistnctUtilChanged/distnctUntilKeyChanged转换类mapmapTopluckwindowTime/scan/mergeScan错误处理类catchretry/retryWhenfinally多播multicastpublishLastpublishReplaypublishBehavior

August 16, 2020 · 1 min · jiezi

基于-React-和-Redux-的-API-集成解决方案

在前端开发的过程中,我们可能会花不少的时间去集成 API、与 API 联调、或者解决 API 变动带来的问题。如果你也希望减轻这部分负担,提高团队的开发效率,那么这篇文章一定会对你有所帮助。 文章中使用到的技术栈主要有: React 全家桶TypeScriptRxJS文章中会讲述集成 API 时遇到的一些复杂场景,并给出对应解决方案。通过自己写的小工具,自动生成 API 集成的代码,极大提升团队开发效率。 本文的所有代码都在这个仓库:request。 自动生成代码的工具在这里:ts-codegen。 1. 统一处理 HTTP 请求1.1 为什么要这样做?我们可以直接通过 fetch 或者 XMLHttpRequest 发起 HTTP 请求。但是,如果在每个调用 API 的地方都采用这种方式,可能会产生大量模板代码,而且很难应对一些业务场景: 如何为所有的请求添加 loading 动画?如何统一显示请求失败之后的错误信息?如何实现 API 去重?如何通过 Google Analytics 追踪请求?因此,为了减少模板代码并应对各种复杂业务场景,我们需要对 HTTP 请求进行统一处理。 1.2 如何设计和实现?通过 redux,我们可以将 API 请求 「action 化」。换句话说,就是将 API 请求转化成 redux 中的 action。通常来说,一个 API 请求会转化为三个不同的 action: request action、request start action、request success/fail action。分别用于发起 API 请求,记录请求开始、请求成功响应和请求失败的状态。然后,针对不同的业务场景,我们可以实现不同的 middleware 去处理这些 action。 1.2.1 Request Actionredux 的 dispatch 是一个同步方法,默认只用于分发 action (普通对象)。但通过 middleware,我们可以 dispatch 任何东西,比如 function (redux-thunk) 和 observable,只要确保它们被拦截即可。 ...

October 9, 2019 · 5 min · jiezi

免费angular8高级实战教程网易云音乐pc端

自制angular8实战教程,先上链接: 网易云课堂:https://study.163.com/course/...B站:https://www.bilibili.com/vide...历时个把月,本想出个单一功能的教程,没想一开始就控制不住了,到最后时长竟高达30多个小时, 为什么是angular?angular是我的第一个框架,所谓先入为主,即使工作中怕是再难用上,也不会把它丢掉,而且angular用户是痛苦的,至少在国内,不论是文档、生态、百度、教程等都全面被vue和react压制,并非angular技不如人,只因google太任性。本教程也算是为推进angular做点贡献吧,这应该是前端框架中,最给力的免费教程了。 做什么?用angular8仿造网易云音乐pc端的部分功能,包括:歌单、歌曲、歌手和会员的登录注册等,并实现网易云核心的播放器功能。 能学到什么?主技术栈:angular8 + ngrx8 + ng-zorro + material/cdk,包括但不限于: ng-template,ng-content,ng-containerng模块化设计proxy,http拦截器依赖注入自定义指令和管道自定义表单控件动态组件各种rxjs操作符material/cdk变更检测策略ngrx8...课程特色?本课程全程都在实战,在开发过程中会尽力覆盖ng的各种api,项目的模块化、目录设计和组件化等都是以真实项目标准来做的,可复用到日常工作的各项目中去,代码极度精简,期间更有徒手造轮子的过程,是一门学习框架和提升基本功的双休课程。源码也分好了章节上传到github:master分支是最终完成的代码https://github.com/lycHub/ng-wyy 需要的基本知识?typescriptrxjsangular基本api的使用(重要)学完后能达到什么水平?由于本课程会尽可能多的使用angular高级api,如果能完全掌握,那在使用层面已经非常优秀了。完全可以独自用angular胜任网易云音乐官网这种难度的项目 教授方式?手写每行ts和html,样式部分复制做好的。 很遗憾无法上传到慕课网,因为实现没有做好功课,推荐大家去网易云课堂或51cto(待上)观看,记的好评哟: 网易云课堂:https://study.163.com/course/...B站:https://www.bilibili.com/vide...

October 8, 2019 · 1 min · jiezi

响应式编程的思维艺术-5Angular中Rxjs的应用示例

本文是【Rxjs 响应式编程-第四章 构建完整的Web应用程序】这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:《大史住在大前端》原创博文目录 华为云社区地址:【你要的前端打怪升级指南】 [TOC] 一. 划重点RxJS-DOM原文示例中使用这个库进行DOM操作,笔者看了一下github仓库,400多星,而且相关的资料很少,所以建议理解思路即可,至于生产环境的使用还是三思吧。开发中Rxjs几乎默认是和Angular技术栈绑定在一起的,笔者最近正在使用ionic3进行开发,本篇将对基本使用方法进行演示。 冷热Observable 冷Observable从被订阅时就发出整个值序列热Observable无论是否被订阅都会发出值,机制类似于javascript事件。涉及的运算符bufferWithTime(time:number)-每隔指定时间将流中的数据以数组形式推送出去。 pluck(prop:string)- 操作符,提取对象属性值,是一个柯里化后的函数,只接受一个参数。 二. Angular应用中的Http请求Angular应用中基本HTTP请求的方式: import { Injectable } from '@angular/core';import { Observable, of } from 'rxjs';import { MessageService } from './message.service';//某个自定义的服务import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';@Injectable({ providedIn: 'root'})export class HeroService { private localhost = 'http://localhost:3001'; private all_hero_api = this.localhost + '/hero/all';//查询所有英雄 private query_hero_api = this.localhost + '/hero/query';//查询指定英雄 constructor(private http:HttpClient) { } /*一般get请求*/ getHeroes(): Observable<HttpResponse<Hero[]>>{ return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'}); } /*带参数的get请求*/ getHero(id: number): Observable<HttpResponse<Hero>>{ let params = new HttpParams(); params.set('id', id+''); return this.http.get<Hero>(this.query_hero_api,{params:params,observe:'response'}); } /*带请求体的post请求,any可以自定义响应体格式*/ createHero(newhero: object): Observable<HttpResponse<any>>{ return this.http.post<HttpResponse<any>>(this.create_hero_api,{data:newhero},{observe:'response'}); } }在express中写一些用于测试的虚拟数据: ...

July 13, 2019 · 2 min · jiezi

响应式编程的思维艺术-4从打飞机游戏理解并发与流的融合

本文是Rxjs 响应式编程-第三章: 构建并发程序这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnowords/blogs 更多博文:《大史住在大前端》原创博文目录 一. 划重点尽量避免外部状态 在基本的函数式编程中,纯函数可以保障构建出的数据管道得到确切的可预测的结果,响应式编程中有着同样的要求,博文中的示例可以很清楚地看到,当依赖于外部状态时,多个订阅者在观察同一个流时就容易互相影响而引发混乱。 当不同的流之间出现共享的外部依赖时,一般的实现思路有两种: 将这个外部状态独立生成一个可观察对象,然后根据实际逻辑需求使用正确的流合并方法将其合并。将这个外部状态独立生成一个可观察对象,然后使用Subject来将其和其他逻辑流联系起来。管道的执行效率在上一节中通过compose运算符组合纯函数就可以看到,容器相关的方法几乎全都是高阶函数,这样的做法就使得管道在构建过程中并不不会被启用,而是缓存组合在了一起(从上一篇的IO容器的示例中就可以看到延缓执行的形式),当它被订阅时才会真正启动。 Subject类 Subject同时具备Observable和observer的功能,可订阅消息,也可产生数据,一般作为流和观察者的代理来使用,可以用来实现流的解耦。为了实现更精细的订阅控制,Subject还提供了以下几种方法。 AsyncSubjectAsyncSubject观察的序列完成后它才会发出最后一个值,并永远缓存这个值,之后订阅这个AsyncSubject的观察者都会立刻得到这个值。 BehaviorSubjectObserver在订阅BehaviorSubject时,它接收最后发出的值,然后接收后续发出的值,一般要求提供一个初始值,观察者接收到的消息就是距离订阅时间最近的那个数据以及流后续产生的数据。 ReplaySubjectReplaySubject会缓存它监听的流发出的值,然后将其发送给任何较晚的Observer,它可以通过在构造函数中传入参数来实现缓冲时间长度的设定。 二. 从理论到实践 原文中提供了一个非常详细的打飞机游戏的代码,但我仍然建议你在熟悉了其基本原理和思路后自己将它实现出来,然后去和原文中的代码作对比,好搞清楚哪些东西是真的理解了,哪些只是你以为自己理解了,接着找一些很明显的优化点,继续使用响应式编程的思维模式来试着实现它们,起初不知道从何下手是非常正常的(当然也可能是笔者的自我安慰),但这对于培养响应式编程思维习惯大有裨益。笔者在自己的实现中又加入了右键切换飞船类型的功能,必须得说开发游戏的确比写业务逻辑要有意思。 由于没有精确计算雪碧图的坐标,所以在碰撞检测时会有一些偏差。 三. 问题及反思关于canvas的尺寸问题 建议通过以下方式来设置: <!--推荐方式1--><canvas height="300" width="400"></canvas>//推荐方式2canvas = document.getElementById('canvas');canvas.height = 300;canvas.width = 300;需要避免的几种方式(都是只改变画板尺寸,不改变画布尺寸,会造成绘图被拉伸): //1.CSS设置#mycanvas{ height:300px; width:300px;}//2.DOM元素API设置canvas = document.getElementById('canvas');canvas.style.height = 300;canvas.style.width= 300;//3.Jquery设置$('#mycanvas').width(300);同时需要注意canvas的宽高不支持百分比设定。 Rx.Observable.combineLatest以后整体的流不自动触发了combineLatest这个运算符需要等所有的流都emit一次数据以后才会开始emit数据,因为它需要为整合在一起的每一个流保持一个最新值。所以自动启动的方法也很简单,为那些不容易触发首次数据的流添加一个初始值就可以了,就像笔者在上述实现右键来更换飞船外观时所实现的那样,使用startWith运算符提供一个初始值后,在鼠标移动时combineLatest生成的飞船流就会开始生产数据了。另外一点需要注意的就是combineLatest结合在一起后,其中任何一个流产生数据都会导致合成后的流产生数据,由于图例数据的坐标是在绘制函数中实现的,所以被动的触发可能会打乱原有流的预期频率,使得一些舞台元素的位置或形状变化更快,这种情况可以使用sample( )运算符对合并后的流进行取样操作来限制数据触发频率。 一段越来越快的流 笔者自己在生成敌机的时候,第一次写出这样一段代码: let enemyShipStream = Rx.Observable.interval(1500).scan((prev)=>{//敌机信息需要一个数组来记录,所以通过scan运算符将随机出现的敌机信息聚合 prev.push({ shape:[238,178,120,76], x:parseInt(Math.random() * canvas.width,10), y:50 }); return prev},[]).flatMap((enemies)=>{ return Rx.Observable.interval(40).map(()=>{ enemies.forEach(function (enemy) { enemy.y = enemy.y + 2; }); return enemies; })});运行的时候发现敌机的速度变得越来越快,很诡异,如果你看不出问题在哪,建议画一下大理石图,看看flatMap汇聚的总的数据流是如何构成的,就很容易看到随着时间推移,多个流都在操作最初的源数据,所以坐标自增的频率越来越快。 ...

July 11, 2019 · 3 min · jiezi

响应式编程的思维艺术-3flatMap背后的代数理论Monad

本文是Rxjs 响应式编程-第二章:序列的深入研究这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnowords/blogs 更多博文:《大史住在大前端》目录 一. 划重点文中使用到的一些基本运算符: map-映射filter-过滤reduce-有限列聚合scan-无限列聚合flatMap-拉平操作(重点)catch-捕获错误retry-序列重试from-生成可观测序列range-生成有限的可观测序列interval-每隔指定时间发出一次顺序整数distinct-去除出现过的重复值建议自己动手尝试一下,记住就可以了,有过lodash使用经验的开发者来说并不难。原文中使用flatMap转换序列时有一处应该是手误: 二. flatMap功能解析原文中在http请求拿到获取到数据后,最初使用了forEach实现了手动流程管理,于是原文提出了优化设想,试图探究如何依赖响应式编程的特性将手动的数据加工转换改造为对流的转换,好让最终的消费者能够拿到直接可用的数据,而不是得到一个响应后手动进行很多后处理。在代码层面需要解决的问题就是,如何在不使用手动遍历的前提下将一个有限序列中的数据逐个发给订阅者,而不是一次性将整个数据集发过去。 假设我们现在并不知道有flatMap这样一个可以使用的方法,那么先来做一些尝试: var quakes = Rx.Observable.create(function(observer) { //模拟得到的响应流 var response = { features:[{ earth:1 },{ earth:2 }], test:1 }/* 最初的手动遍历代码 var quakes = response.features; quakes.forEach(function(quake) { observer.onNext(quake); });*/ observer.onNext(response);})//为了能将features数组中的元素逐个发送给订阅者,需要构建新的流.map(dataset){ return Rx.Observable.from(dataset.features)}当我们订阅quakes这个事件流的时候,每次都会得到另一个Observable,它是因为数据源经过了映射变换,从数据变成了可观测对象。那么为了得到最终的序列值,就需要再次订阅这个Observable,这里需要注意的是可观测对象被订阅前是不启动的,所以不用担心它的时序问题。 quakes.subscribe(function(data){ data.subscribe(function(quake){ console.log(quake); })});如果将Observable看成一个盒子,那么每一层盒子只是实现了流程控制功能性的封装,为了取得真正需要使用的数据,最终的订阅者不得不像剥洋葱似的通过subscribe一层层打开盒子拿到最里面的数据,这样的封装性对于数据在流中的传递具有很好的隔离性,但是对最终的数据消费者而言,却是一件很麻烦的事情。 这时flatMap运算符就派上用场了,它可以将冗余的包裹除掉,从而在主流被订阅时直接拿到要使用的数据,从大理石图来直观感受一下flatMap: 乍看之下会觉得它和merge好像是一样的,其实还是有一些区别的。merge的作用是将多个不同的流合并成为一个流,而上图中A1,A2,A3这三个流都是当主流A返回数据时新生成的,可以将他们想象为A的支流,如果你想在支流里捞鱼,就需要在每个支流里布网,而flatMap相当于提供了一张大网,将所有A的支流里的鱼都给捞上来。 所以在使用了flatMap后,就可以直接在一级订阅中拿到需要的数据了: var quakes = Rx.Observable.create(function(observer) { var response = { features:[{ earth:1 },{ earth:2 }], test:1 } observer.onNext(response);}).flatMap((data)=>{ return Rx.Observable.from(data.features);});quakes.subscribe(function(quake) { console.log(quake)});三. flatMap的推演3.1 函数式编程基础知识回顾如果本节的基本知识你尚不熟悉,可以通过javascript基础修炼(8)——指向FP世界的箭头函数这篇文章来简单回顾一下函数式编程的基本知识,然后再继续后续的部分。 ...

July 7, 2019 · 1 min · jiezi

响应式编程的思维艺术-1Rxjs专题学习计划

[TOC] 一. 响应式编程响应式编程,也称为流式编程,对于非前端工程师来说,可能并不是一个陌生的名词,它是函数式编程在软件开发中应用的延伸,如果你对函数式编程还没有一些感性的认知,那么建议你先阅读我曾经写过的一篇入门文章【javascript基础修炼(8)——指向FP世界的箭头函数】,先理解一下函数式编程的基本思想以及在javascript语言中应用。 响应式编程和函数式编程的思想非常棒,它带给开发者对于编程行为不同角度的理解,当你习惯了“一切皆对象”的思维方式后,换一种“一切皆流”的视角是一件非常有意思的事情,代码以一种陌生却有趣的方式组合在一起,但是它依然能够正常工作,而且更容易让开发者看到一系列处理逻辑的全貌,而暂时忽略其实现细节,编程的实际体验和使用underscore或lodash的工具函数之间的嵌套或链式调用(尤其是lodash的FP模式非常相似)。 至于响应式编程和面向对象编程之间优劣的对比,个人认为没有什么实际意义,它们并不是只能二选一的对立项(比如Angular技术栈中两者就是并存的),能够在恰当的场景使用合适的方式才更重要,相比于面向对象编程的严谨和复杂,响应式编程更容易让人体会到编程的灵动和乐趣。 二. 学习路径规划学习该教程需要一定函数式编程基础,笔者自己认为的难点将通过系列博文来记录。由于Angular技术栈的学习,笔者需要在原来函数式编程知识的基础上,学习Rxjs的使用。笔者在SegmentFault社区发现了一个非常高质量的【Rxjs 响应式编程】系列教程共6篇,从基础概念到实际应用讲解的非常详细,有大量直观的大理石图来辅助理解流的处理,对培养响应式编程的思维方式有很大帮助。笔者将通过系列博文对学习中的疑惑和收获及原文中的示例代码细节进行讲解。对此感兴趣的读者也可以先睹为快,也非常欢迎在我的底盘讨论与此相关的问题和疑惑: Rxjs 响应式编程-第一章:响应式 Rxjs 响应式编程-第二章:序列的深入研究 Rxjs 响应式编程-第三章: 构建并发程序 Rxjs 响应式编程-第四章 构建完整的Web应用程序 Rxjs 响应式编程-第五章 使用Schedulers管理时间 Rxjs 响应式编程-第六章 使用Cycle.js的响应式Web应用程序

July 2, 2019 · 1 min · jiezi

从观察者模式到迭代器模式系统讲解-RxJS-Observable一

RxJS 是 Reactive Extensions for JavaScript 的缩写,起源于 Reactive Extensions,是一个基于可观测数据流 Stream 结合观察者模式和迭代器模式的一种异步编程的应用库。RxJS 是 Reactive Extensions 在 JavaScript 上的实现。 Reactive Extensions(Rx)是对 LINQ 的一种扩展,他的目标是对异步的集合进行操作,也就是说,集合中的元素是异步填充的,比如说从 Web或者云端获取数据然后对集合进行填充。LINQ(Language Integrated Query)语言集成查询是一组用于 C# 和Visual Basic 语言的扩展。它允许编写 C# 或者 Visual Basic 代码以操作内存数据的方式,查询数据库。RxJS 的主要功能是利用响应式编程的模式来实现 JavaScript 的异步式编程(现前端主流框架 Vue React Angular 都是响应式的开发框架)。 RxJS 是基于观察者模式和迭代器模式以函数式编程思维来实现的。学习 RxJS 之前我们需要先了解观察者模式和迭代器模式,还要对 Stream 流的概念有所认识。下面我们将对其逐一进行介绍,准备好了吗?让我们现在就开始吧。 RxJS 前置知识点观察者模式观察者模式又叫发布订阅模式(Publish/Subscribe),它是一种一对多的关系,让多个观察者(Obesver)同时监听一个主题(Subject),这个主题也就是被观察者(Observable),被观察者的状态发生变化时就会通知所有的观察者,使得它们能够接收到更新的内容。 观察者模式主题和观察者是分离的,不是主动触发而是被动监听。 举个常见的例子,例如微信公众号关注者和微信公众号之间的信息订阅。当微信用户关注微信公众号 webinfoq就是一个订阅过程,webinfoq负责发布内容和信息,webinfoq有内容推送时,webinfoq的关注者就能收到最新发布的内容。这里,关注公众号的朋友就是观察者的角色,公众号webinfoq就是被观察者的角色。 示例代码: // 定义一个主题类(被观察者/发布者)class Subject { constructor() { this.observers = []; // 记录订阅者(观察者)的集合 this.state = 0; // 发布的初始状态 } getState() { return this.state; } setState(state) { this.state = state; // 推送新信息 this.notify(); // 通知订阅者有更新了 } attach(observer) { this.observers.push(observer); // 对观察者进行登记 } notify() { // 遍历观察者集合,一一进行通知 this.observers.forEach(observer = { observer.update(); }) }}// 定义一个观察者(订阅)类class Observer { constructor(name, subject) { this.name = name; // name 表示观察者的标识 this.subject = subject; // 观察者订阅主题 this.subject.attach(this); // 向登记处传入观察者实体 } update() { console.log(`${this.name} update, state: ${this.subject.getState()}`); }}// 创建一个主题let subject = new Subject();// 创建三个观察者: observer$1 observer$2 observer$3let observer$1 = new Observer("observer$1", subject);let observer$2 = new Observer("observer$2", subject);let observer$3 = new Observer("observer$3", subject);// 主题有更新subject.setState(1);subject.setState(2);subject.setState(3);// 输出结果// observer$1 update, state: 1// observer$1 update, state: 1// observer$1 update, state: 1// observer$2 update, state: 2// observer$2 update, state: 2// observer$2 update, state: 2// observer$3 update, state: 3// observer$3 update, state: 3// observer$3 update, state: 3迭代器模式迭代器(Iterator)模式又叫游标(Sursor)模式,迭代器具有 next 方法,可以顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表现。 ...

June 10, 2019 · 5 min · jiezi

RxJs脑图

May 14, 2019 · 0 min · jiezi

RxJS原来应该这样用

引言最近帮潘佳琦解决了一个诡异的问题,然后突然发现⾃己对观察者感到迷茫了。 需求是⼀个注销按钮,如果是技术机构登陆,就调用技术机构的注销⽅法,如果是器具用户登陆,就调⽤器具⽤户的注销方法。当然,最优的解决⽅案并不是我下⽂所列的,既然功能不同,那就应该是两个对象。看来我们的⾯向对象运用得还不不够灵活。 原问题解决问题描述注销代码如下,只表达思想,别去深究具体的语法: logout(): void { this.departmentService.isLogin$.subscribe((isDepartmentLogin) => { if (isDepartmentLogin) { this.departmentLogout(); } }); this.systemService.isLogin$.subscribe((isTechnicalLogin) => { if (isTechnicalLogin) { this.technicalLogout(); } });}看着好像没啥错误啊?订阅获取当前登录用户状态,如果departmentService.isLogin$为true,表示是器具用户登录,则调用器具用户的注销方法;如果是systemService.isLogin$为true,表示是技术机构登录,则调用技术机构的注销方法。 然而,诡异的事情发生了: 首次打开系统,登录,登录成功。点击注销,也能注销成功。但是再登录系统,就登不进去了。也就是说,这个注销方法影响了后续的登录,当时就很懵圈,为什么呢? 后来多打了几条日志,才发现了问题所在: 原因分析根据Spring官方的Spring Security and Angular所述,官方做法是用一个boolean值来判断当前是否登录,从而进行视图的展示。 潘老师在新系统中也是根据官方的推荐进行实现的: let isLogin$ = new BehaviorSubject<boolean>();login() { ...... this.isLogin$.next(true);}logout() { ...... this.isLogin$.next(false);}一个可观察的boolean对象来判断当前用户是否登录,然后main组件订阅这个值,根据是否登录并结合ngIf来判断当前应该显示登录组件还是应用组件。 看这个图大家应该就明白了,问题出在了对subscribe的理解上。 点击注销,发起订阅,当isLogin$为true的时候就注销,注销成功。 下次登录时,这个订阅还在呢!然后点击登录,执行登录逻辑,将isLogin$设置为true,观察者就通知了订阅者,又执行了一遍注销逻辑。肯定登不上去了。 执行订阅之后,应该获取返回值,再执行取消订阅。 迷茫所以,这个问题出在本该订阅一次,但是subscribe是只要订阅过了,在取消订阅之前,我一直是这个可观察对象的观察者。 想到这我就迷茫了,就是我一订阅,一直到我取消订阅之前,这个可观察对象都要维护着观察者的列表。 那我们的网络请求用的不也是Observable吗?只是此Observable是由HttpClient构建好返回给我们的。那我们订阅了也没取消,那它是不是一直维护着这个关系,会不会有性能问题?难道我们之前的用法都错了吗? 这个问题一直困扰了我好多天,知道今天才在Angular官网上看到相关的介绍,才解决了我的迷茫。 HttpClient.get()方法正常情况下只会返回一个可观察对象,它或者发出数据,或者发出错误。有些人说它是“一次性完成”的可观察对象。 The HttpClient.get() method normally returns an observable that either emits the data or an error. Some folks describe it as a "one and done" observable. ...

April 26, 2019 · 1 min · jiezi

使用RxJS管理React应用状态的实践分享

随着前端应用的复杂度越来越高,如何管理应用的数据已经是一个不可回避的问题。当你面对的是业务场景复杂、需求变动频繁、各种应用数据互相关联依赖的大型前端应用时,你会如何去管理应用的状态数据呢?我们认为应用的数据大体上可以分为四类:事件:瞬间产生的数据,数据被消费后立即销毁,不存储。异步:异步获取的数据;类似于事件,是瞬间数据,不存储。状态:随着时间空间变化的数据,始终会存储一个当前值/最新值。常量:固定不变的数据。RxJS天生就适合编写异步和基于事件的程序,那么状态数据用什么去管理呢?还是用RxJS吗? 合不合适呢?我们去调研和学习了前端社区已有的优秀的状态管理解决方案,也从一些大牛分享的关于用RxJS设计数据层的构想和实践中得到了启发:使用RxJS完全可以实现诸如Redux,Mobx等管理状态数据的功能。应用的数据不是只有状态的,还有事件、异步、常量等等。如果整个应用都由observable来表达,则可以借助RxJS基于序列且可响应的的特性,以流的方式自由地拼接和组合各种类型的数据,能够更优雅更高效地抽象出可复用可扩展的业务模型。出于以上两点原因,最终决定基于RxJS来设计一套管理应用的状态的解决方案。原理介绍对于状态的定义,通常认为状态需要满足以下3个条件:是一个具有多个值的集合。能够通过event或者action对值进行转换,从而得到新的值。有“当前值”的概念,对外一般只暴露当前值,即最新值。那么,RxJS适合用来管理状态数据吗?答案是肯定的!首先,因为Observable本身就是多个值的推送集合,所以第一个条件是满足的!其次,我们可以实现一个使用dispatch action模式来推送数据的observable来满足第二个条件!众所周知,RxJS中的observable可以分为两种类型:cold observable: 推送值的生产者(producer)来自observable内部。将会推送几个值以及推送什么样的值已在observable创建时被定义下来,不可改变。producer与观察者(observer) 是一对一的关系,即是单播的。每当有observer订阅时,producer都会把预先定义好的若干个值依次推送给observer。hot observable: 推送值的producer来自observable外部。将会推送几个值、推送什么样的值以及何时推送在创建时都是未知的。producer与observer是一对多的关系,即是多播的。每当有observer订阅时,会将observer注册到观察者列表中,类似于其他库或语言中的addListener的工作方式。当外部的producer被触发或执行时,会将值同时推送给所有的observer;也就是说,所有的observer共享了hot observable推送的值。RxJS提供的BehaviorSubject就是一种特殊的hot observable,它向外暴露了推送数据的接口next函数;并且有“当前值”的概念,它保存了发送给observer的最新值,当有新的观察者订阅时,会立即从BehaviorSubject那接收到“当前值”。那么这说明使用BehaviorSubject来更新状态并保存状态的当前值是可行的,第三个条件也满足了。简单实现请看以下的代码:import { BehaviorSubject } from ‘rxjs’;// 数据推送的生产者class StateMachine { constructor(subject, value) { this.subject = subject; this.value = value; } producer(action) { let oldValue = this.value; let newValue; switch (action.type) { case ‘plus’: newValue = ++oldValue; this.value = newValue; this.subject.next(newValue); break; case ’toDouble’: newValue = oldValue * 2; this.value = newValue; this.subject.next(newValue); break; } }}const value = 1; // 状态的初始值const count$ = new BehaviorSubject(value);const stateMachine = new StateMachine(count$, value);// 派遣actionfunction dispatch(action) { stateMachine.producer(action);}count$.subscribe(val => { console.log(val);});setTimeout(() => { dispatch({ type: “plus” });}, 1000);setTimeout(() => { dispatch({ type: “toDouble” });}, 2000);执行代码控制台会打印出三个值:Console 1 2 4上面的代码简单实现了一个简单管理状态的例子:状态的初始值: 1执行plus之后的状态值: 2执行toDouble之后的状态值: 4实现方法挺简单的,就是使用BehaviorSubject来表达状态的当前值:第一步,通过调用dispatch函数使producer函数执行第二部,producer函数在内部调用了BehaviorSubject的next函数,推送了新数据,BehaviorSubject的当前值更新了,也就是状态更新了。不过写起来略微繁琐,我们对其进行了封装,优化后写法见下文。使用操作符来创建状态数据我们自定义了一个操作符state用来创建一个能够通过dispatch action模式推送新数据的BehaviorSubject,我们称她为stateObservable。const count$ = state({ // 状态的唯一标识名称 name: “count”, // 状态的默认值 defaultValue: 1, // 数据推送的生产者函数 producer(next, value, action) { switch (action.type) { case “plus”: next(value + 1); break; case “toDouble”: next(value * 2); break; } }});更新状态在你想要的任意位置使用函数dispatch派遣action即可更新状态!dispatch(“count”, { type: “plus”})异步数据RxJS的一大优势就在于能够统一同步和异步,使用observable处理数据你不需要关注同步还是异步。下面的例子我们使用操作符from将promise转换为observable。指定observable作为状态的初始值(首次推送数据)const todos$ = state({ name: “todos”, // observable推送的数据将作为状态的初始值 initial: from(getAsyncData()) //… });producer推送observableconst todos$ = state({ name: “todos”, defaultValue: [] // 数据推送的生产者函数 producer(next, value, action) { switch (action.type) { case “getAsyncData”: next( from(getAsyncData()) ); break; } }});执行getAsyncData之后,from(getAsyncData())的推送数据将成为状态的最新值。衍生状态由于状态todos$是一个observable,所以可以很自然地使用RxJS操作符转换得到另一个新的observable。并且这个observable的推送来自todos$;也就是说只要todos$推送新数据,它也会推送;效果类似于Vue的计算属性。// 未完成任务数量const undoneCount$ = todos$.pipe( map(todos => { let _conut = 0; todos.forEach(item => { if (!item.check) ++_conut; }); return _conut; }));React视图渲染我们可能会在组件的生命周期内订阅observable得到数据渲染视图。class Todos extends React.Component { componentWillMount() { todos$.subscribe(data => { this.setState({ todos: data }); }); }}我们可以再优化下,利用高阶组件封装一个装饰器函数@subscription,顾名思义,就是为React组件订阅observable以响应推送数据的变化;它会将observable推送的数据转换为React组件的props。@subscription({ todos: todos$})class TodoList extends React.Component { render() { return ( <div className=“todolist”> <h1 className=“header”>任务列表</h1> {this.props.todos.map((item, n) => { return <TodoItem item={item} key={item.desc} />; })} </div> ); }}总结使用RxJS越久,越令人受益匪浅。因为它基于observable序列提供了较高层次的抽象,并且是观察者模式,可以尽可能地减少各组件各模块之间的耦合度,大大减轻了定位BUG和重构的负担。因为是基于observable序列来编写代码的,所以遇到复杂的业务场景,总能按照一定的顺序使用observable描述出来,代码的可读性很强。并且当需求变动时,我可能只需要调整下observable的顺序,或者加个操作符就行了。再也不必因为一个复杂的业务流程改动了,需要去改好几个地方的代码(而且还容易改出BUG,笑~)。所以,以上基于RxJS的状态管理方案,对我们来说是一个必需品,因为我们项目中大量使用了RxJS,如果状态数据也是observable,对我们抽象可复用可扩展的业务模型是一个非常大的助力。当然了,如果你的项目中没有使用RxJS,也许Redux和Mobx是更合适的选择。这套基于RxJS的状态管理方案,我们已经用于开发公司的商用项目,反馈还不错。所以我们决定把这套方案整理成一个js lib,取名为:Floway,并在github上开源:github源码:https://github.com/shayeLee/floway使用文档:https://shayelee.github.io/floway欢迎大家star,更欢迎大家来共同交流和分享RxJS的使用心得!参考文章:复杂单页应用的数据层设计DaoCloud 基于 RxJS 的前端数据层实践 ...

April 2, 2019 · 2 min · jiezi

通过超级直观的图表学习合并Rxjs

内容来自于Max Koretskyi aka Wizard的《Learn to combine RxJs sequences with super intuitive interactive diagrams》在足够复杂的应用程序上工作时,通常会有来自多个数据源的数据。它可以是一些多个外部数据点。序列合成是一种技术,通过将相关流组合成一个流,可以跨多个数据源创建复杂的查询。RxJs提供了各种各样的操作符,可以帮助你做到这一点,在本文中,我们将看看最常用的操作符。下面是我将在接下里的文章里将会用到的图表的样例:同时合并多个序列我们将要看到的第一个操作符是merge。该运算符将多个可观察流组合在一起,同时从每个给定的输入流中发出所有值。当所有组成这个流的输入流产生值的时候,这些值都会作为合成流的结果被发出。这个过程在文档中经常被称为扁平化。当所有输入流都结束了,那这个流就结束了。任何一个输入流引发了错误,则这个流引发错误。只要有一个流没有完成,则这个流就不会完成。如果您不关心排放顺序,只关心来自多个组合流的所有值,就像它们是由一个流产生的一样,请使用此运算符。在下图中,你可以看到merge合并了A,B两个流,每一个流都产生了3个值,当值被发出的时候,值会落入合成流中,最终由合成流发出。下面是演示代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 200, 3, ‘partial’);merge(a, b).subscribe(fullObserver(‘merge’));// can also be used as an instance operatora.pipe(merge(b)).subscribe(fullObserver(‘merge’));顺序连接多个序列接下来我们要讲到的操作符是concat。它将所有的输入流串联起来,顺序的订阅并发送每一个流的值。一旦当前流完成,它会订阅下一个流,并将输入流发出的值传递到结果流中。当所有输入流完成时,该流完成,如果某些输入流引发错误,将引发错误。如果一些输入流没有完成,它将永远不会完成,这也意味着一些流将永远不会被订阅。如果排放顺序很重要,并且您希望首先看到由您首先传递给操作符的流发送的值,请使用此运算符。例如,您可能有一个从缓存传递值的可观察序列和另一个从远程服务器传递值的序列。如果您想要合并它们并确保首先传递来自缓存的值,请使用concat。在下图中,您可以看到concat运算符将两个流A和B组合在一起,每个流产生3个值,值首先从A开始,然后从B开始,一直到结果流。下面是演示代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 200, 3, ‘partial’);concat(a, b).subscribe(fullObserver(‘concat’));// can also be used as an instance operatora.pipe(concat(b)).subscribe(fullObserver(‘concat’));多个流竞争接下来我们要讲到的这个操作符race,相当的有趣。它并不是将多个输入流合成一个流输出,而是多个流竞争,一旦有一个输入流最先发出值,那其他流将被取消订阅并完全忽略。当选定的输入流完成时,结果流完成,如果这个流出错,将抛出一个错误。如果内部流不完成,它也永远不会完成。如果你有多个可以提供价值的资源,例如世界各地的服务器,该运算符可能会很有用,但是由于网络条件的原因,延迟是不可预测的,并且变化很大。使用这个运算符,你可以将同一个请求发送到多个数据源,并使用第一个响应的结果。在下图中,您可以看到race操作符将两个流A和B组合在一起,每个流产生3个项目,但是只有流A中的值被发出,因为这个流首先开始发出值。下面是演示代码:const a = intervalProducer(‘a’, 200, 3, ‘partial’);const b = intervalProducer(‘b’, 500, 3, ‘partial’);race(a, b).subscribe(fullObserver(‘race’));// can also be used as an instance operatora.pipe(race(b)).subscribe(fullObserver(‘race’));组合为止数量的流和高阶可观察对象之前讲到的操作,都只能组合已知数量的流。但是如果您事先不知道所有的流,并且想要合并可以在运行时延迟评估的流,会怎么样呢?事实上,这是使用异步代码时非常常见的情况。例如,对某些资源的网络调用可能会导致由原始请求的结果值决定的许多其他请求。RxJs有我们在上面看到的操作符的变体,这些操作符采用一系列序列,被称为高阶Observable或Observable。MergeAll该运算符组合所有发出的内部流,就像普通合并一样,同时从每个流中生成值。在下图中,你将看到一个高阶流H,它发出两个内部类A和B。mergeAll运算符将这两个流中的值组合起来,然后在它们发出值时将它们传递给结果流。下面是演示代码:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(mergeAll()).subscribe(fullObserver(‘mergeAll’));ConcatAll该运算符将所有发出的内部流组合起来,就像普通concat一样,从每个流中顺序生成值。在下图中,您可以看到产生两个内部流A和B的高阶流H。串联运算符首先从A流中获取值,然后从流B中获取值,并将它们传递给结果序列。下面是演示代码:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(concatAll()).subscribe(fullObserver(‘concatAll’));SwitchAll有时从所有内部Observable中接收值不是我们需要的。在某些情况下,我们可能只对最新内部序列中的值感兴趣。搜索功能是一个很好的例子。当用户在输入框输入一些值后,我们想服务器发送一些网络请求,但这些网络请求是异步的。如果用户在返回结果之前又更新了输入框中的值,会发生什么?第二个网络请求被发送了出去,所以现在我们已经向服务器发送了两个搜索的网络请求。然而,我们对第一次搜索的结果已经不感兴趣了,并且,如果将两次搜索结果都显示给用户,这将不符合我们的设想。所以我们使用switchAll操作符,它只会订阅最新的内部流并产生值,并忽略之前的流。在下图中,您可以看到产生两个内部流A和B的高阶流H。开关操作符首先从A流中获取值,然后从B流中获取值,并将它们传递给结果序列。下面是演示代码:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(switchAll()).subscribe(fullObserver(‘switchAll’));concatMap,mergeMap,switchMap有趣的是,这些映射操作符concatMap,mergeMap,switchMap的使用频率比和他们相对应的concatAll,‘mergeMap’,switchAll要高得多。然而,如果你仔细想想,它们几乎是一样的。所有的*Map操作符都是由两个parts — producing流通过映射和使用组合逻辑,在由高阶Observable产生的内部流上进行观察。让我们来看看下面熟悉的代码,它演示了mergeAll运算符是如何工作的:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(mergeAll()).subscribe(fullObserver(‘mergeAll’));这里的map操作符产生Observable流,mergeAll合并这些Observable流。所以我们可以使用mergeMap轻松替代mergeAll。const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), mergeMap(i => [a, b][i]));h.subscribe(fullObserver(‘mergeMap’));这两个结果是完全一样的。concaMap和switchMap操作也是如此。你可以自己尝试一下。配对序列组合前面的操作符允许我们展平多个序列,并通过结果流不变地传递来自这些序列的值,就好像它们都来自这个序列一样。接下来我们要看的这组运算符仍然将多个序列作为输入,但不同之处在于它们将每个序列的值配对,为输出序列产生一个组合值。每个运算符都可以选择一个所谓的投影函数作为最后一个参数,该参数定义了结果序列中的值应该如何组合。在我的示例中,我将使用默认的投影函数,该函数简单地使用逗号作为分隔符来连接值。在这一节的最后,我将展示如何提供一个定制的投影函数。CombineLatest我们要看到的第一个操作符是combineLatest。它允许您从输入序列中获取最新的值,并将这些值转换为结果序列的一个值。RxJs缓存每个输入序列的最后一个值,一旦所有序列产生了至少一个值,它就使用从缓存中获取最新值的投影函数来计算结果值,然后通过结果流发出该计算的输出。如果任何一个内部流不完成,它将永远不会完成。另一方面,如果任何一个流不发出值而是完成了,则结果流将在同一时刻完成而不发出任何信号,因为现在不可能在结果序列中包含来自完成的输入流的值。此外,如果某个输入流不发出任何值并且永远不会完成,combineLatest也永远不会发出并且永远不会完成,因为它将再次等待所有流发出某个值。如果您需要评估一些状态组合,而这些状态组合需要在部分状态发生变化时保持最新,那么这个运算符会很有用。一个简单的例子就是监控系统。每个服务都由一个返回布尔值的序列表示,该值指示所述服务的可用性。如果所有服务都可用,则监控状态为绿色,因此投影功能只需执行逻辑“与”。在下图中,你可以看到combineLatest操作组合了两个流A和B。一旦所有流都发射了至少一个值,每个新发射通过结果流产生一个组合值。下面是实例代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);combineLatest(a, b).subscribe(fullObserver(’latest’));Zip这个操作符也是一个非常有趣的合并操作符,它在某种程度上类似于衣服或袋子上拉链的机械结构。它将两个或多个相应值的序列集合成一个元组(在两个输入流的情况下是一对)。它等待从所有输入流中发出相应的值,然后使用投影函数将它们转换成单个值并发出结果。只有当每个源序列中有一对新值时,它才会发布,因此如果其中一个源序列发布值的速度快于另一个序列,发布速率将由两个序列中较慢的一个决定。当任何内部流完成并且相应的匹配对从其他流发出时,结果流完成。如果任何内部流没有完成,它将永远不会完成,如果任何内部流出错,它将抛出一个错误。该运算符可方便地用于实现一个流,该流产生一系列具有间隔的值。以下是投影函数仅从range流返回值的基本示例:zip(range(3, 5), interval(500), v => v).subscribe();在下图中,您可以看到zip运算符将两个流A和B组合在一起。一旦对应的流对匹配,结果序列就会产生一个组合值:以下是示例代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);zip(a, b).subscribe(fullObserver(‘zip’));forkjoin有时,您有一组流,只关心每个流的最终发射值。通常这种序列只有一次发射。例如,您可能希望发出多个网络请求,并且只希望在收到所有请求的响应后采取措施。在某种程度上,它类似于Promise.all的功能。但是,如果您有一个发出多个值的流,除了最后一个值之外,这些值将被忽略。当所有内部流完成时,生成的流只发出一次。如果任何内部流没有完成,它将永远不会完成,如果任何内部流出错,它将抛出一个错误。在下图中,您可以看到forkJoin运算符将两个流A和b组合在一起。一旦对应的流对匹配,结果序列就会产生一个组合值:下面是示例代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);forkJoin(a, b).subscribe(fullObserver(‘forkJoin’));WithLatestFrom我们在本文中最后要看的运算符是withLatestFrom。当您有一个引导流,但还需要来自其他流的最新值时,使用该运算符。在某种程度上,它类似于combineLatest操作符,每当任何输入流有新的排放时,都会发出新的值。withLatestFrom只有在引导流发出值后,才会发出新值。正如combineLatest一样,它仍然等待来自每个流的至少一个发射值,并且当引导流完成时,可以在没有单个发射的情况下完成。如果引导流没有完成,它将永远不会完成,如果任何内部流出错,它将抛出一个错误。在下图中,您可以看到withLatestFrom运算符将两个流A和流B组合在一起,其中流B是引导流。每当流B发出一个新值时,产生的序列使用来自流A的最新值产生一个组合值。下面是示例代码:const a = stream(‘a’, 3000, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);b.pipe(withLatestFrom(a)).subscribe(fullObserver(’latest’));Projection function(投影函数)如本节开头所述,通过配对组合值的所有运算符都采用可选的投影函数。该函数定义结果值的转换。使用此函数,您可以选择只从特定的输入序列中发出一个值,或者以任何您想要的方式连接值:// return value from the second sequencezip(s1, s2, s3, (v1, v2, v3) => v2)// join values using dash as a separatorzip(s1, s2, s3, (v1, v2, v3) => ${v1}-${v2}-${v3})// return single boolean resultzip(s1, s2, s3, (v1, v2, v3) => v1 && v2 && v3) ...

March 28, 2019 · 2 min · jiezi

一周总结

又到了一周写汇报的时候,细细想来,这周的状态其实不太好,然后又因为报了驾校,课余时间又得分出去一部分,所以这周的项目进展主要是在靠学长和潘哥的推动,自己只写了几个小功能。就简单的总结一下这周遇到的问题。observable未订阅在项目中写了一个http请求的函数,但是无论如何这个函数就是没有网络请求,后来偶然发现没有订阅observable,订阅一下就可以了。通过这件事牢牢记住了obervable的对象必须订阅。使用timer()遇到的坑timer定时器是很好用的,但由于开始对angular的生命周期和timer不太熟悉,费了好长时间才在学长和潘哥的帮助下解决遇到的一个bug。在说bug之前先简单的说说timer怎么使用。timner的简单用法想要完整的学习,自然得通过官方文档的方式,但只是想简单使用,可以参照下面:先定义一个Subscription定义定时器的操作一个简单的定时器就完成了。突然出现的报错在计算列表为空的情况下会发出带数据的请求。开始怎么检查都觉得代码没问题,找不到他产生的原因,研究了半天,发现他是几个一出现几个一出现,而且几个任务执行的间隔绝对不到定时器执行的时间,学长突然想到了是不是timer不会随着组件销毁而销毁,经过测试果然是这样。angular生命周期既然定时器不能自动销毁,只能靠我们自己销毁了,这时候就要用到ngOnDestroy,当组件销毁时,主动去取消timer的订阅。 /** * 组件销毁方法 */ public ngOnDestroy() { // 取消定时器订阅 this.yunzhiTimer.unsubscribe(); }

March 8, 2019 · 1 min · jiezi

RxJS不完全指北(入门篇)

什么是RxJS?RxJS是一个JavaScript库,用来编写异步和基于事件的程序。RxJS结合了观察者模式、迭代器模式和使用集合的函数式编程,以满足以一种理想方式来管理事件序列所需要的一切。可以把RxJS当作用来处理事件的Lodash。为什么要学Rxjs?在现在的Web开发中,异步(Async)操作随处可见,比如使用ajax提交一个表单数据,我们需要等待服务端返回提交结果后执行后续操作,这就是一个典型的异步操作。虽然JavaScript为了方便开发者进行异步操作,提出了很多解决方案(callback,Promise,Async/await等等),但是随着需求愈加复杂,如何优雅的管理异步操作仍然是个难题。此外,异步操作API千奇百怪,五花八门:DOM EventsXMLHttpRequestfetchWebSocketsService WorkerTimer……以上这些常用的API全部都是异步的,但是每个使用起来却完全不同,无形中给开发者增加了很大的学习和记忆成本。使用RxJS可以很好的帮助我们解决上面两个问题,控制大量异步代码的复杂度,保持代码可读性,并统一API。举个栗子:页面上有一个搜索框,用户可以输入文本进行搜索,搜索时要向服务端发送异步请求,为了减小服务端压力,前端需要控制请求频率,1秒最多发送5次请求,并且输入为空时不发送请求,最后将搜索的结果显示在页面上。通常我们的做法是这样的,先判断输入是否为空,如果不为空,则构造一个截流函数来控制请求频率,这其中涉及到创建和销毁定时器,此外,由于每个请求返回时间不确定,如何获取最后一次搜索结果,需要构造一个栈来保存请求顺序, 想完美实现需求并不简单。RxJS是如何解决这个问题的呢?请看下面的代码:// 1.获取dom元素const typingInput = document.querySelector("#typing-input"); // 输入const typingBack = document.querySelector("#typing-back"); // 输出// 2.模拟异步请求const getData = value => new Promise(resolve => setTimeout(() => resolve(async data ${value}), 1000) );// 3.RxJS操作const source$ = fromEvent(typingInput, “input”) // 创建事件数据流.pipe( // 管道式操作 map(e => e.target.value), // 获取输入的数据 filter(i => i), // 过滤空数据 debounceTime(200), // 控制频率 switchMap(getData) // 转化数据为请求);// 4.输入结果source$.subscribe(asyncData => (typingBack.innerHTML = asyncData));这就是全部代码,也许有些地方看不太懂 ,没关系,先不要着急,我们分步解读一下。使用选择器获取了两个dom元素,第一个是输入框,第二个是搜索结果的容器;使用Promise来模拟一个异步请求的函数,1秒后返回请求结果;这部分是RxJS操作,这里我们要先介绍一个概念,“数据流”(stream,简称“流”),“流”是RxJS中一种特殊的对象,我们可以想象数据流就像一条河流,而数据就是河里的水,顺流而下。代表“流”的变量一般用“$”结尾,这是RxJS编程的一种约定,被成为“芬兰式命名法”。代码中的source$就是输入框的输入事件产生的数据流,我们可以使用pipe方法,像搭建“管道”一样对流中的数据进行加工,先使用map函数将事件对象转化成输入值,然后使用fllter方法过滤掉无效的输入,接着使用debounceTime控制数据向下流转的频率,最后使用switchMap把输入值转化成异步请求,整个数据流就构建完成了。最后我们使用数据流的subscribe方法添加对数据的操作,也就是将请求的结果输出到页面上。注意,这段代码我们使用的全部变量都是用const声明的,全部是不可变的,也即是变量声明时是什么值,就永远是什么值,就像定义函数一样。相对于传统的指令式编程,RxJS的代码就是由一个一个不可变的函数组成,每个函数只是对输入参数作出相应,然后返回结果,这样的代码写起来更加清爽,也更好维护。RxJS结合了函数式和响应式这两种编程思想,为了更深入的了解RxJS,先来介绍一下什么是函数式编程和响应式编程。函数式编程函数式编程(Functional Porgramming)是一种编程范式,就像“面向对象编程”一样,是一种编写代码的“方法论”,告诉我们应该如何思考和解决问题。不同于面向对象编程,函数式编程强调使用函数来解决问题。这里有两个问题:任何语言都支持函数式编程么?并不是,能够支持函数式编程的语言至少要满足“函数是一等公民(First Class)”这个要求,意思是函数可以被赋值给一个变量,并且可以作为参数传递给另一个函数,也可以作为另一个函数的返回值。显然JavaScript满足这个条件。函数式编程里的函数有什么特别之处?函数式编程里要求函数满足以下几个要求:声明式、纯函数、数据不可变。声明式(Declarative)与之对应的是命令式编程,也是最常见的编程模式。举个例子,我们希望写个函数,把数组中的每个元素乘以2,使用命令式编程,大概是这个样子的:function double(arr) { const result = [] for(let i=0,l=arr.length;i<l;i++) { result.push(arr[i] * 2) } return result}我们将整个逻辑过程完整描述了一遍,完美。但如果又来了一个新需求,实现一个新函数,把数组中每个元素加1,简单,再来一遍:function addOne(arr) { const result = [] for(let i=0,l=arr.length;i<l;i++) { result.push(arr[i] + 1) } return result}是不是感觉哪里不对?double和addOne百分之九十的代码完全一样,“重复的代码是万恶之源。”我们应该想办法改进一下。这里就体现了命令式编程的一个问题,程序按照逻辑过程来执行,但是很多问题都有相似的模式,比如上面的double和addOne。很自然我们想把这个模式抽象一下,减少重复代码。接下来我们使用JavaScript的map函数来重写double和addOne:function double(arr) { return arr.map(function(item) { return item * 2 })}function addOne(arr) { return arr.map(function(item) { return item + 1 })}重复代码全部被封装到map函数中。而我们需要做的只是告诉map函数应该如何映射数据,这就是声明式编程。相比较之前的代码,这样的代码更容易维护。如果使用箭头函数,代码还可以进一步简化:const double = arr => arr.map(item => item * 2)const addOne = arr => arr.map(item => item + 1)注意以上两个函数的返回结果都是一个新的数组,而并没有对原数组进行修改,这符合函数式编程的另外一个要求:纯函数。纯函数(Pure Function)纯函数是指满足以下两个条件的函数:相同的参数输入,返回相同的输出结果;函数内不会修改任何外部状态,比如全局变量或者传入的参数对象;举个栗子:const arr = [1, 2, 3, 4, 5]arr.slice(0, 3) // [1, 2, 3]arr.slice(0, 3) // [1, 2, 3]arr.slice(0, 3) // [1, 2, 3]JavaScript中数组的slice方法不管执行几次,返回值都相同,并且没有改变任何外部状态,所以slice就是一个纯函数。const arr = [1, 2, 3, 4, 5]arr.splice(0, 3) // [1, 2, 3]arr.splice(0, 3) // [4, 5]arr.slice(0, 3) // []相反,splice方法每次调用的结果就不同,因为splice方法改变了全局变量arr的值,所以splice就不是纯函数。 不纯的函数往往会产生一些副作用(Side Effect),比如以下这些:改变全局变量;改变输入参数引用对象;读取用户输入,比如调用了alert或者confirm函数;抛出一个异常;网络I/O,比如发送了一个AJAX请求;操作DOM;使用纯函数可以大大增强代码的可维护性,因为固定输入总是返回固定输出,所以更容易写单元测试,也就更不容易产生bug。数据不可变(Immutability)数据不可变是函数式编程中十分重要的一个概念,意思是如果我们想改变一个变量的值,不是直接对这个变量进行修改,而是通过调用函数,产生一个新的变量。如果你是一个前端工程师,肯定已经对数据不可变的好处深有体会。在JavaScript中,字符串(String),数字(Number)这两种类型就是不可变的,使用他们的时候往往不容易出错,而数组(Array)类型就是可变的, 使用数组的pop、push等方法都会改变原数组对象,从而引发各种bug。注意,虽然ES6已经提出了使用const声明一个常量(不可变数据),但是这只能保证声明的对象的引用不可改变,而这个对象自身仍然可以变化。比如用const声明一个数组,使用push方法仍然可以像数组中添加元素。和面向对象编程相比,面向对象编程更倾向把状态的改变封装到对象内部,以此让代码更清晰。而函数式编程倾向数据和函数分离,函数可以处理数据,但不改变原数据,而是通过产生新数据的方式作为运算结果,以此来尽量减少变化的部分,让我们的代码更清晰。响应式编程和函数式编程类似,响应式编程(Reactive Programming)也是一种编程的范式。从设计模式的角度来说,响应式编程就是“观察者模式”的一种有效实践。简单来说,响应式编程指当数据发生变化时,主动通知数据的使用者这个变化。很多同学都使用过vue框架开发,vue中很出名的数据双向绑定就是基于响应式编程的设计思想实现的。当我们在通过v-bind绑定一个数据到组件上以后,不管这个数据何时发生变化,都会主动通知绑定过的组件,使我们开发时可以专注处理数据本身,而不用关心如何同步数据。而在相应时编程里最出名的框架就是微软开发的Reactive Extension。这套框架旨在帮助开发者解决复杂的异步处理问题。我们的主角RxJS就是这个框架的JS版本。怎么使用RxJS安装npm install rxjs导入import Rx from “rxjs”;请注意,这样导入会将整个RxJS库全部导入进来,而实际项目未必会用上Rxjs的全部功能,全部导入会让项目打包后变得非常大,我们推荐使用深链(deep link)的方式导入Rxjs,只导入用的上的功能,比如我们要使用Observable类,就只导入它:import { Observable } from “rxjs/Observable”;实际项目中,按需导入是一个好办法,但是如果每个文件都写一堆import语句,那就太麻烦了。所以,更好的实践是用一个文件专门导入RxJS相关功能,其他文件再导入这个文件,把RxJS导入工作集中管理。篇幅有限,下一讲将会讲解RxJS中几个核心概念,欢迎各位留言拍砖~ ...

March 1, 2019 · 1 min · jiezi

RxJS 实现摩斯密码(Morse) 【内附脑图】

参加 2018 ngChina 开发者大会,特别喜欢 Michael Hladky 奥地利帅哥的 RxJS 分享,现在拿出来好好学习工作坊的内容(工作坊Demo地址),结合这个示例,做了一个改进版本,实现更简洁,逻辑更直观。一、摩斯密码是什么?了解者可跳过次章节摩斯密码(Morse),是一种时通时断的信号代码,这种信号代码通过不同的排列组合来表达不同的英文字母、数字和标点符号等。地球人都知道的 SOS 求救信号,就是 Morse,三短(S) 三长(O) 三短(S)。信号对应表如下:二、业务逻辑分析分析关键步骤,很巧,和把大象装进冰箱里同样都只需要三步耶:第一步,识别点信号,短为 “滴” 长为“嗒”。第二步,根据 “长间隔” 来切片分组。第三步,分组数据根据对应表转化出最终结果。三、撸代码,优化后版本(完整在线示例)开始前要做好热身活动:Morse 的最小单元,"." 代表嘀,"-" 代表嗒,点击事件用 Down 代表 mousedown,Up 代表 mouseup。200ms 间隔用来区别嘀嗒,1s 间隔用来区分一个 Morse 单元组的结束。 // 点信号 = Down - Up = 间隔 < 200ms ?"." : “-”; // Down <200ms Up >1s = “.” = E// Down <200ms Up <1s Down >200ms Up >1s = “.”, “-” = A// 直接使用 fromEvent 操作符,来生成点击操作的流,然后用 map 操作符转化成时间戳,// takeUntil 用来控制流的结束,避免重复订阅。const clickBegin$ = fromEvent(this.sendButtonElementRef.nativeElement, ‘mousedown’) .pipe( takeUntil(this.onDestroy$), map(n => Date.now()) )const clickEnd$ = fromEvent(this.sendButtonElementRef.nativeElement, ‘mouseup’) .pipe( takeUntil(this.onDestroy$), map(n => Date.now()) ) 第一步,识别点信号为 “滴” “嗒”前面代码已经拿到点击事件的流,并且用 “map” 操作符,把数据转化为当前的时间戳。下面开始计算 Down & Up 之间的间隔时间,思考,合并两个流的的操作符有哪些呢?forkJoin、concat ?需要两个流 complate 状态后才返回数据,不适应数据持续输出的场景。merge ?Down & Up 的时间戳不会同时获得,还需要处理存储的问题,不完全适应场景。combineLatest ?满足数据持续输出,满足同时获得,哎哟,还不错。但是这个操作符的特点是,会缓存上一次的值,所以第二次 Down 也会获得到数据,Up - Down 也就会为负值,取绝对值后可以用来判断是否 >1s,来区分一个 Morse 单元组的结束。zip ?哎呀哈,这个更合适呢,盘它!单词选的很到位,这个操作符功能可以理解为像拉链一样,确保获得数据每一次都是一个纯净的 Down & Up。但是需要注意 zip 会自动缓存数据,例如 zip(A$, B$),A$收到的数据一直比B$多太多,有内存溢出风险,就像拉错位的拉链,很蓝瘦。 // zip的实现zip(clickBegin$, clickEnd$) .pipe( // 计算 Down - Up 间隔时间 map(this.toTimeDiff), // 根据间隔时间,转化为嘀嗒替代字符 “.” “-” map(this.msToMorseCharacter) ) .subscribe(result => { // 发送到主信号流 morseSignal$.next(result); }); 第二步,根据 “长间隔” 来切片分组分组的操作符有哪些?partition?根据函数拆成两个流。groupBy?根据函数拆成 n 个流。window?根据流拆成 n 个流。以上各位都打扰了,我还要自己处理数据缓存,再见。buffer?哇,初恋般的感觉,用流控制来做切片数据成数组,拿到数组只需要 join 一下就好,就可以去去匹配对应表了,好棒!“长间隔”的切片流,怎么获得呢?拿出法宝 debounceTime(1000) ,当点击的 Down Up 周期完成后,间隔 1s 就认为是一个Morse 单元组的结束。然后又遇到了问题,怎么判断一个点击周期呢?不用单纯用 Up ,因为下一个 Down Up 周期可能会超出 1s,就会导致切片时机错误。所以模拟了点击持续的流 clickKeeping$,用 switchMap 替换为新的流且不影响原来的流,timer 产生一个小于 1s 间隔的持续流信号,用 takeUntil 在 Up 事件流 clickEnd$ 后把整个流结束。 // 点击持续状态流const clickKeeping$ = clickBegin$ .pipe( // 替换为新的流,不影响原来的流 switchMap(() => { // 定时在持续发送数据,维持点击中状态 return timer(0, morseTimeRanges.lessThenlongBreak).pipe( // 直到 Up 后结束点击状态 takeUntil(clickEnd$) ); }) )// “长间隔”的切片流const morseBreak$ = clickKeeping$.pipe( debounceTime(morseTimeRanges.longBreak));// 获得 Morse 单元组morseSignal$ .pipe( // 切片分组主信号流 buffer(morseBreak$) // 转化为,例如 [’.’, ‘.’, ‘.’] ) 第三步,分组数据根据对应表转化出最终结果join(’’) Morse 单元组去匹配对应表,很简单不用说。错误发生在 switchMap 中,分支流报错,但是主流不会收到影响,然后用 catchError 捕捉错误。 // Morse 单元组去匹配对应表private translateSymbolToLetter = morseArray => { const morseCharacters = morseArray.join(’’); const find = morseTranslations.find(n => n.symbol === morseCharacters) // 这里 find 可能为 undefined 导致报错,但是错误会被 catchError 捕捉 return find.char;}// 转化+错误处理,最终完成morseSignal$ .pipe( buffer(morseBreak$), switchMap(n => { return of(n).pipe( // 只为了 Demo 演示中的展示用 tap(n => this.lastMorseGroupCharacters = n.join(’ ‘)), // 转化成对应表中字符 map(this.translateSymbolToLetter), // 捕捉错误 catchError(n => { return of(morseCharacters.errorString); }) ) }) ) .subscribe(result => { // 输出最终转化结果 this.morseLog.push(result); console.log(‘结果:’, result) }); 四、解读 Michael Hladky 大神的示例整体上,把 “嘀嗒” “短间隔” “长间隔” 都转化成替代符,过滤无用的替代符,然后 filter “长间隔” 替代符的流,来做 buffer 切片数据。其他还有因为使用 combineLatest 操作符导致的不同。// 识别 “嘀” “嗒”const morseCharFromEvents$ = observableCombineLatest(this.startEvents$, this.stopEvents$) .pipe( // 计算 mousedown mouseup 时间间隔 map(this.toTimeDiff), // 转化成标识符 map(this.msToMorseChar), // 过滤 Morse 单元组中的 “短间隔“ 标识符 filter(this.isCharNoShortBreak as any) );// 主信号流this.morseChar$ = observableMerge(morseCharFromEvents$, this.injectMorseChar$)// 识别 “长间隔“ 标识符,来作为切片流const longBreaks$ = this.morseChar$ .pipe(filter(this.isCharLongBreak as any));// 切片成 Morse 单元组this.morseSymbol$ = this.morseChar$ .pipe( buffer(longBreaks$), map(this.charArrayToSymbol), filter(n => (n !== ‘’) as any) )// 错误处理 + 标识符对应表转化this.morseLetter$ = this.morseSymbol$ .pipe( switchMap(n => observableOf(n).pipe(this.saveTranslate(‘ERROR’))) );// Up 后补4个 “长间隔“ 标识符,用来做 Morse 单元组的结束const breakEmitter$ = observableTimer(this.msLongBreak, this.msLongBreak) .pipe( mapTo(this.mC.longBreak), take(4) );this.stopEventsSubject .pipe( switchMapTo( breakEmitter$.pipe(takeUntil(this.startEventsSubject)) ) ) .subscribe(n => this.injectMorseChar(n)); 总结下图是读完《深入浅出RxJS》后的学习笔记,标注了一些操作符的快速记忆特点,方便使用的适合查阅。 ...

February 19, 2019 · 2 min · jiezi

【Rxjs】Rxjs_Subject 及其衍生类

Rxjs_Subject 及其衍生类在 RxJS 中,Observable 有一些特殊的类,在消息通信中使用比较频繁,下面主要介绍较常用的几个类:1/ SubjectSubject 可以实现一个消息向多个订阅者推送消息。Subject 是一种特殊类型的 Observable,它允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。每个 Subject 都是观察者。 - Subject 是一个有如下方法的对象: next(v)、error(e) 和 complete() 。要给 Subject 提供新值,只要调用 next(theValue),它会将值多播给已注册监听该 Subject 的观察者们。var subject = new Rx.Subject(); //实例化一个Subject对象subject.next(1); //向接受者发送一个消息流subject.subscribe({ next: value => console.log(“observerA: " + value) //接受者A订阅消息,获取消息流中的数据});subject.subscribe({ next: value => console.log(“observerB: " + value) //接受者B订阅消息,获取消息流中的数据});这样两路接受者都能拿到发送的数据流:observerA:1observerB:12/ BehaviorSubjectBehaviorSubject 是 Subject 的一个衍生类,它将数据流中的最新值推送给接受者。var subject = new Rx.BehaviorSubject(0); //声明一个 BehaviorSubject 对象subject.next(1); //发送一个数据流subject.next(2); //再发送一个数据流subject.subscribe({ next: v => console.log(“observerA: " + v) //接受者 A 订阅消息});subject.subscribe({ next: v => console.log(“observerB: " + v) //接受者 B 订阅消息});subject.next(3); //再发送一个数据流这样,每次接受者只会接受最新最送的那个消息:observerA:2observerB:2observerA:3observerB:33/ ReplaySubjectReplaySubject 类似于 BehaviorSubject,它可以发送旧值给新的订阅者,但它还可以记录 Observable 执行的一部分。当创建 ReplaySubject 时,你可以指定回放多少个值:var subject = new Rx.ReplaySubject(3); // 为新的订阅者缓冲3个值subject.subscribe({ next: v => console.log(“observerA: " + v)});subject.next(1);subject.next(2);subject.next(3);subject.next(4);subject.subscribe({ next: v => console.log(“observerB: " + v)});subject.next(5);输出:observerA: 1observerA: 2observerA: 3observerA: 4observerB: 2observerB: 3observerB: 4observerA: 5observerB: 54/ AsyncSubjectAsyncSubject 是另一个 Subject 变体,只有当 Observable 执行完成时(执行 complete()),它才会将执行的最后一个值发送给观察者。var subject = new Rx.AsyncSubject();subject.subscribe({ next: v => console.log(“observerA: " + v)});subject.next(1);subject.next(2);subject.next(3);subject.next(4);subject.subscribe({ next: v => console.log(“observerB: " + v)});subject.next(5);subject.complete();输出:observerA: 5observerB: 5参考文档《PublishSubject,ReplaySubject,BehaviorSubject,AsyncSubject》 ...

January 31, 2019 · 1 min · jiezi

Rxjs 学习

包含了学习资料,和一些demohttps://github.com/ladybug2fi…

January 25, 2019 · 1 min · jiezi

[WIP] 一个基于Angular最新版搭建的博客类小项目

前言:因为项目需要学习了一下Angular,按照官网的教程做了一个简单粗糙的小项目。大致的功能有博客的新建,删除,查看详情,列表展示。使用内存Web API模拟远程服务器,进行数据操作。临时安排的变动,可能没有太多的时间持续研究,所以现在这里记录一下。以下是我边学习边记的一些笔记,方便日后回来可以快速回忆起相关技术点。路由一个配置了路由的Angular应用有一个路由服务的单例。当浏览器的url改变的时候,路由就会去配置里找到相应的路由信息,根据该路由就可以决定展示哪一个组件。路由器会把类似URL的路径映射到视图而不是页面。浏览器本应该加载一个新页面,但是路由器拦截了这一行为。<router-outlet>标签官网上说它起到了placeholder的作用,当路由根据url找到要展示的组件时,就会把这个组件填充到<router-outlet>中去。获取路由中的参数信息: +this.route.snapshot.paramMap.get(‘id’);route.snapshot 是一个路由信息的静态快照,抓取自组建刚刚创建完毕之后paramMap是一个从URL中提取的路由参数值的字典。javascript(+)操作符会把字符串转为数字数据绑定用指令 [(ngModel)]=“data.content"可实现遇到报错 在app.module中导入import { FormsModule } from ‘@angular/forms’;HTTP获取资源主要是使用了common包下的HttpClientModule这一个模块,使用它则需要把它先导入进app.module中,然后在需要用到的service中导入相应的模块,例如HttpClient,HttpHeaders等。HttpClient提供了很丰富的Http协议的请求方法。实际使用时可参考源码。请求失败处理:catchError() 操作符会拦截失败的 Observable。 它把错误对象传给错误处理器,错误处理器会处理这个错误。因此异常被处理后,浏览器的控制台不会报错。状态管理 ngrx与Redux的关系:ngrx/store的灵感来源于Redux,是一款集成RxJS的Angular状态管理库,由Angular的布道者Rob Wormald开发。它和Redux的核心思想相同,但使用RxJS实现观察者模式。它遵循Redux核心原则,但专门为Angular而设计。Actions(行为)是信息的载体,它发送数据到reducer(归约器),然后reducer更新store(存储)。Actions是store能接受数据的唯一方式。组件化思想把所有特性都放在同一个组件中,将会使应用变得难以维护。因此需要把大型组件分成小一点的子组件,每个子组件都要集中经历处理某个特定的任务或者工作流。依赖注入DI,是一种重要的应用设计模式。实现方式:类从外部源中请求获取依赖,无需自己创建。DI框架会在实例化该类时向其提供这个类所声明的依赖项。@Injectable()装饰器是每个Angular服务定义中的基本要素,把当前类标志为可注入的服务。注入器:负责创建服务实例提供商:告诉注入器如何创建实例服务组件不直接获取或者保存数据,而是把这一块的功能放在service中,让组件专注于如何展示数据,如此开发使得项目的每个模块功能更加明确。service里的方法应该都是异步的。Observable(可观察对象)的版本可以实现。ES6相关一个模块从另一个模块import进来,若是一个值 :直接取值取到的是一个会被缓存的结果(例如获取WebStorage中更新的值,必须要刷新页面)函数,获取一个值的引用,会调用该函数,取到最新的值控制台输出与Debug有时候会发现控制台输出的结果与直接在js文件里进行debug所显示的结果不一致,那是因为,console.log()采用了懒加载的机制,它展示的是一个引用的值,当你点击三角箭头的时候,它会去加载当前变量的值。但debug的数据具有“实时性”。不一致的情况经常出现在一些异步代码中。如果你看到这里,觉得内容有误或有可改进之处,欢迎你的提出~

January 17, 2019 · 1 min · jiezi

面试题 LazyMan 的Rxjs实现方式

前言笔者昨天在做某公司的线上笔试题的时候遇到了最后一道关于如何实现LazyMan的试题,题目如下实现一个LazyMan,可以按照以下方式调用:LazyMan(“Hank”)输出:Hi! This is Hank!LazyMan(“Hank”).sleep(10).eat(“dinner”)输出Hi! This is Hank!//等待10秒..Wake up after 10Eat dinnerLazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出Hi This is Hank!Eat dinnerEat supper~LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出//等待5秒Wake up after 5Hi This is Hank!Eat supper以此类推。鉴于时间的原因只可惜本人当时并没写出来,我当时脑海里其实看到提意就知道要用到队列、Promise等异步操作。然后我查阅了网上的资料好像关于这个LazyMan的实现方式倒不少,就说明这道题其实蛮有意思的,但大多都是关于Promise或setTimeout的实现,并没有Rxjs的实现方式,所以我就用一些操作符实现了这个LazyManclass LazyManModel { queue: { timeout: number, fn: Function }[] = [] constructor() { setTimeout(() => { from(this.queue).pipe( map(e => { if (e.timeout) return of(e).pipe(delay(e.timeout * 1000)); return of(e) }), concatAll() ).subscribe(value => { value.fn() }) }) } sleep(time: number): this { this.queue.push({ timeout: time, fn: () => { console.log(Wake up after ${time}) } }) return this } eat(foot: string): this { this.queue.push({ timeout: null, fn: () => { console.log(Eat ${foot}~) } }) return this } sleepFirst(time: number): this { this.queue.unshift({ timeout: time, fn: () => { console.log(Wake up after ${time}) } }) return this } exported(): (name: string) => this { return (name): this => { this.queue.push({ timeout: null, fn: () => { console.log(Hi! This is ${name}!) } }) return this } }}示例const LazyMan = new LazyManModel().exported();LazyMan(‘Hank’).eat(‘foot’).eat(‘ping’).sleep(10).eat(‘pizza’).sleepFirst(5)关于setTimeout我在constructor构造函数里使用了setTimeout是因为,在调用的时候是链式的,其作用域一直都在同一堆栈,而setTimeout里则是把订阅的方法放到的最后一个栈执行 ...

January 10, 2019 · 1 min · jiezi

使用 React + Rxjs 实现一个虚拟滚动组件

原文同样发布在知乎专栏https://zhuanlan.zhihu.com/p/…为什么使用虚拟列表在我们的业务场景中遇到这么一个问题,有一个商户下拉框选择列表,我们简单的使用 antd 的 select 组件,发现每次点击下拉框,从点击到弹出会存在很严重的卡顿,在本地测试时,数据库只存在370条左右数据,这个量级的数据都能感到很明显的卡顿了(开发环境约700+ms),更别提线上 2000+ 的数据了。Antd 的 select 性能确实不敢恭维,它会简单的将全部数据 map 出来,在点击的时候初始化并保存在 document.body 下的一个 DOM 节点中缓存起来,这又带来了另一个问题,我们的场景中,商户选择列表很多模块都用到了,每次点击之后都会新生成 2000+ 的 DOM 节点,如果把这些节点都存到 document 下,会造成 DOM 节点数量暴涨。虚拟列表就是为了解决这种问题而存在的。虚拟列表原理虚拟列表本质就是使用少量的 DOM 节点来模拟一个长列表。如下图左所示,不论多长的一个列表,实际上出现在我们视野中的不过只是其中的一部分,这时对我们来说,在视野外的那些 item 就不是必要的存在了,如图左中 item 5 这个元素)。即使去掉了 item 5 (如右图),对于用户来说看到的内容也完全一致。下面我们来一步步将步骤分解,具体代码可以查看 Online Demo。这里是我通过这种思想实现的一个库,功能会更完善些。https://github.com/musicq/vist创建适合容器高度的 DOM 元素以上图为例,想象一个拥有 1000 元素的列表,如果使用上图左的方式的话,就需要创建 1000 个 DOM 节点添加在 document 中,而其实每次出现在视野中的元素,只有4个,那么剩余的 996 个元素就是浪费。而如果就只创建 4 个 DOM 节点的话,这样就能节省 996 个DOM 节点的开销。解题思路真实 DOM 数量 = Math.ceil(容器高度 / 条目高度)定义组件有如下接口interface IVirtualListOptions { height: number}interface IVirtualListProps { data$: Observable<string[]> options$: Observable<IVirtualListOptions>}首先需要有一个容器高度的流来装载容器高度 private containerHeight$ = new BehaviorSubject<number>(0)需要在组件 mount 之后,才能测量容器的真实高度。可以通过一个 ref 来绑定容器元素,在 componentDidMount 之后,获取容器高度,并通知 containerHeight$。this.containerHeight$.next(virtualListContainerElm.clientHeight)获取了容器高度之后,根据上面的公式来计算视窗内应该显示的 DOM 数量const actualRows$ = combineLatest(this.containerHeight$, this.props.options$).pipe( map(([ch, { height }]) => Math.ceil(ch / height)))通过组合 actualRows$ 和 data$ 两个流,来获取到应当出现在视窗内的数据切片const dataInViewSlice$ = combineLatest(this.props.data$, actualRows$).pipe( map(([data, actualRows]) => data.slice(0, actualRows)))这样,一个当前时刻的数据源就获取到了,订阅它来将列表渲染出来dataInViewSlice$.subscribe(data => this.setState({ data }))效果给定的数据有 1000 条,只渲染了前 7 条数据出来,这符合预期。现在存在另一个问题,容器的滚动条明显不符合 1000 条数据该有的高度,因为我们只有 7 条真实 DOM,没有办法将容器撑开。撑开容器在原生的列表实现中,我们不需要处理任何事情,只需要把 DOM 添加到 document 中就可以了,浏览器会计算容器的真实高度,以及滚动到什么位置会出现什么元素。但是虚拟列表不会,这就需要我们自行解决容器的高度问题。为了能让容器看起来和真的拥有1000条数据一样,就需要将容器的高度撑开到 1000 条元素该有的高度。这一步很容易,参考下面公式解题思路真实容器高度 = 数据总数 * 每条 item 的高度将上述公式换成代码const scrollHeight$ = combineLatest(this.props.data$, this.props.options$).pipe( map(([data, { height }]) => data.length * height))效果这样看起来就比较像有 1000 个元素的列表了。但是滚动之后发现,下面全是空白的,由于列表只存在7个元素,空白是正常的。而我们期望随着滚动,元素能正确的出现在视野中。滚动列表这里有三种实现方式,而前两种基本一样,只有细微的差别,我们先从最初的方案说起。完全重刷列表这种方案是最简单的实现,我们只需要在列表滚动到某一位置的时候,去计算出当前的视窗中列表的索引,有了索引就能得到当前时刻的数据切片,从而将数据渲染到视图中。为了让列表效果更好,我们将渲染的真实 DOM 数量多增加 3 个const actualRows$ = combineLatest(this.containerHeight$, this.props.options$).pipe( map(([ch, { height }]) => Math.ceil(ch / height) + 3))首先定义一个视窗滚动事件流const scrollWin$ = fromEvent(virtualListElm, ‘scroll’).pipe( startWith({ target: { scrollTop: 0 } }))在每次滚动的时候去计算当前状态的索引const shouldUpdate$ = combineLatest( scrollWin$.pipe(map(() => virtualListElm.scrollTop)), this.props.options$, actualRows$).pipe( // 计算当前列表中最顶部的索引 map(([st, { height }, actualRows]) => { const firstIndex = Math.floor(st / height) const lastIndex = firstIndex + actualRows - 1 return [firstIndex, lastIndex] }))这样就能在每一次滚动的时候得到视窗内数据的起止索引了,接下来只需要根据索引算出 data 切片就好了。const dataInViewSlice$ = combineLatest(this.props.data$, shouldUpdate$).pipe( map(([data, [firstIndex, lastIndex]]) => data.slice(firstIndex, lastIndex + 1)));拿到了正确的数据,还没完,想象一下,虽然我们随着滚动的发生计算出了正确的数据切片,但是正确的数据却没有出现在正确的位置,因为他们的位置是固定不变的。因此还需要对元素的位置做位移(逮虾户)的操作,首先修改一下传给视图的数据结构const dataInViewSlice$ = combineLatest( this.props.data$, this.props.options$, shouldUpdate$).pipe( map(([data, { height }, [firstIndex, lastIndex]]) => { return data.slice(firstIndex, lastIndex + 1).map(item => ({ origin: item, // 用来定位元素的位置 $pos: firstIndex * height, $index: firstIndex++ })) }));接下把 HTML 结构也做一下修改,将每一个元素的位移添加进去this.state.data.map(data => ( <div key={data.$index} style={{ position: ‘absolute’, width: ‘100%’, // 定位每一个 item transform: translateY(${data.$pos}px) }} > {(this.props.children as any)(data.origin)} </div>))这样就完成了一个虚拟列表的基本形态和功能了。效果如下但是这个版本的虚拟列表并不完美,它存在以下几个问题计算浪费DOM 节点的创建和移除计算浪费每次滚动都会使得 data 发生计算,虽然借助 virtual DOM 会将不必要的 DOM 修改拦截掉,但是还是会存在计算浪费的问题。实际上我们确实应该触发更新的时机是在当前列表的索引发生了变化的时候,即开始我的列表索引为 [0, 1, 2],滚动之后,索引变为了 [1, 2, 3],这个时机是我们需要更新视图的时机。借助于 rxjs 的操作符,可以很轻松的搞定这个事情,只需要把 shouldUpdate$ 流做一次过滤操作即可。const shouldUpdate$ = combineLatest( scrollWin$.pipe(map(() => virtualListElm.scrollTop)), this.props.options$, actualRows$).pipe( // 计算当前列表中最顶部的索引 map(([st, { height }, actualRows]) => [Math.floor(st / height), actualRows]), // 如果索引有改变,才触发重新 render filter(([curIndex]) => curIndex !== this.lastFirstIndex), // update the index tap(([curIndex]) => this.lastFirstIndex = curIndex), map(([firstIndex, actualRows]) => { const lastIndex = firstIndex + actualRows - 1 return [firstIndex, lastIndex] }))效果DOM 节点的创建和移除如果仔细对比会发现,每次列表发生更新之后,是会发生 DOM 的创建和删除的,如下图所示,在滚动了之后,原先位于列表中的第一个节点被移除了。而我期望的理想的状态是,能够重用 DOM,不去删除和创建它们,这就是第二个版本的实现。复用 DOM 重刷列表为了达到节点的复用,我们需要将列表的 key 设置为数组索引,而非一个唯一的 id,如下this.state.data.map((data, i) => <div key={i}>{data}</div>)只需要这一点改动,再看看效果可以看到数据变了,但是 DOM 并没有被移除,而是被复用了,这是我想要的效果。观察一下这个版本的实现与上一版本有何区别是的,这个版本,每一次 render 都会使得整个列表样式发生变化,而且还有一个问题,就是列表滚动到最后的时候,会发生 DOM 减少的情况,虽然并不影响显示,但是还是有 DOM 的创建和移除的问题存在。复用 DOM + 按需更新列表为了能让列表只按照需要进行更新,而不是全部重刷,我们就需要明确知道有哪些 DOM 节点被移出了视野范围,操作这些视野范围外的节点来补充列表,从而完成列表的按需更新,如下图假设用户在向下滚动列表的时候,item 1 的 DOM 节点被移出了视野,这时我们就可以把它移动到 item 5 的位置,从而完成一次滚动的连续,这里我们只改变了元素的位置,并没有创建和删除 DOM。dataInViewSlice$ 流依赖props.data$、props.options$、shouldUpdate$三个流来计算出当前时刻的 data 切片,而视图的数据完全是根据 dataInViewSlice$ 来渲染的,所以如果想要按需更新列表,我们就需要在这个流里下手。在容器滚动的过程中存在如下几种场景用户慢慢地向上或者向下滚动:移出视野的元素是一个接一个的用户直接跳转到列表的一个指定位置:这时整个列表都可能完全移出视野但是这两种场景其实都可以归纳为一种情况,都是求前一种状态与当前状态之间的索引差集。实现在 dataInViewSlice$ 流中需要做两步操作。第一,在初始加载,还没有数组的时候,填充一个数组出来;第二,根据滚动到当前时刻时的起止索引,计算出二者的索引差集,更新数组,这一步便是按需更新的核心所在。先来实现第一步,只需要稍微改动一下原先的 dataInViewSlice$ 流的 map 实现即可完成初始数据的填充const dataSlice = this.stateDataSnapshot;if (!dataSlice.length) { return this.stateDataSnapshow = data.slice(firstIndex, lastIndex + 1).map(item => ({ origin: item, $pos: firstIndex * height, $index: firstIndex++ }))}接下来完成按需更新数组的部分,首先需要知道滚动前后两种状态之间的索引差异,比如滚动前的索引为 [0,1,2],滚动后的索引为 [1,2,3],那么他们的差集就是 [0],说明老数组中的第一个元素被移出了视野,那么就需要用这第一个元素来补充到列表最后,成为最后一个元素。首先将数组差集求出来// 获取滚动前后索引差集const diffSliceIndexes = this.getDifferenceIndexes(dataSlice, firstIndex, lastIndex);有了差集就可以计算新的数组组成了。还以此图为例,用户向下滚动,当元素被移除视野的时候,第一个元素(索引为0)就变成最后一个元素(索引为4),也就是,oldSlice [0,1,2,3] -> newSlice [1,2,3,4]。在变换的过程中,[1,2,3] 三个元素始终是不需要动的,因此我们只需要截取不变的 [1,2,3]再加上新的索引 4 就能变成 [1,2,3,4]了。// 计算视窗的起始索引let newIndex = lastIndex - diffSliceIndexes.length + 1;diffSliceIndexes.forEach(index => { const item = dataSlice[index]; item.origin = data[newIndex]; item.$pos = newIndex * height; item.$index = newIndex++;});return this.stateDataSnapshot = dataSlice;这样就完成了一个向下滚动的数组拼接,如下图所示,DOM 确实是只更新超出视野的元素,而没有重刷整个列表。但是这只是针对向下滚动的,如果往上滚动,这段代码就会出问题。原因也很明显,数组在向下滚动的时候,是往下补充元素,而向上滚动的时候,应该是向上补充元素。如 [1,2,3,4] -> [0,1,2,3],对它的操作是 [1,2,3] 保持不变,而 4号元素变成了 0号元素,所以我们需要根据不同的滚动方向来补充数组。先创建一个获取滚动方向的流 scrollDirection$const scrollDirection$ = scrollTop$.pipe( map(scrollTop => { const dir = scrollTop - this.lastScrollPos; this.lastScrollPos = scrollTop; return dir > 0 ? 1 : -1; }));将 scrollDirection$ 流加入到 dataInViewSlice$ 的依赖中const dataInViewSlice$ = combineLatest(this.props.data$, this.options$, shouldUpdate$).pipe( withLatestFrom(scrollDirection$))有了滚动方向,我们只需要修改 newIndex 就好了// 向下滚动时 [0,1,2,3] -> [1,2,3,4] = 3// 向上滚动时 [1,2,3,4] -> [0,1,2,3] = 0let newIndex = dir > 0 ? lastIndex - diffSliceIndexes.length + 1 : firstIndex;至此,一个功能完善的按需更新的虚拟列表就基本完成了,效果如下是不是还差了什么?没错,我们还没有解决列表滚动到最后时会创建、删除 DOM 的问题了。分析一下问题原因,应该能想到是 shouldUpdate$ 这里在最后一屏的时候,计算出来的索引与最后一个索引的差小于了 actualRows$ 中计算出来的数,所以导致了列表数量的变化,知道了原因就好解决问题了。我们只需要计算出数组在维持真实 DOM 数量不变的情况下,最后一屏的起始索引应为多少,再和计算出来的视窗中第一个元素的索引进行对比,取二者最小为下一时刻的起始索引。计算最后一屏的索引时需要得知 data 的长度,所以先将 data 依赖拉进来const shouldUpdate$ = combineLatest( scrollWin$.pipe(map(() => virtualListElm.scrollTop)), this.props.data$, this.props.options$, actualRows$)然后来计算索引// 计算当前列表中最顶部的索引map(([st, data, { height }, actualRows]) => { const firstIndex = Math.floor(st / height) // 在维持 DOM 数量不变的情况下计算出的索引 const maxIndex = data.length - actualRows < 0 ? 0 : data.length - actualRows; // 取二者最小作为起始索引 return [Math.min(maxIndex, firstIndex), actualRows];})这样就真正完成了完全复用 DOM + 按需更新 DOM 的虚拟列表组件。Githubhttps://github.com/musicq/vist上述代码具体请看在线 DEMOOnline Demo。 ...

January 10, 2019 · 3 min · jiezi

【Rxjs】Rxjs_观察者模式和发布订阅模式

Rxjs_观察者模式和发布订阅模式设计模式捡起大学所学的《设计模式》吧 Orz观察者模式和发布订阅模式特别容易被人们混淆,很多书里面也将这两个概念混为一谈,所以首先要搞清楚这两种模式的区别。观察者模式╭─────────────╮ Fire Event ╭──────────────╮│ │─────────────>│ ││ Subject │ │ Observer ││ │<─────────────│ │╰─────────────╯ Subscribe ╰──────────────╯观察者其模式实很好理解,模式中只有两种角色,观察者和被观察者。观察者模式属于行为型模式,用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。Subject(目标) 目标又称为主题,它是指被观察的对象。Observer(观察者) 观察者将对观察目标的改变做出反应代码例子:jQuery function refresh() { $(‘div’).empty().text(‘you are stupid.’) $(‘div’).trigger(‘refresh’) } … $(‘div’).on(‘refresh’, () => { $(‘span’).empty().text(‘go to find it.’) })发布-订阅模式 ╭─────────────╮ ╭───────────────╮ Fire Event ╭──────────────╮ │ │ Publish Event │ │───────────────>│ │ │ Publisher │────────────────>│ Event Channel │ │ Subscriber │ │ │ │ │<───────────────│ │ ╰─────────────╯ ╰───────────────╯ Subscribe ╰──────────────╯发布订阅模式属于广义上的观察者模式发布订阅模式与观察者模式非常接近,仅仅只是多了一个中间层用于管理消息(信息通道),可以看成是一种优化的观察者模式。生活中有一个很好的例子——广播电台,人们会把频道调到他们最喜欢的节目。广播站不知道观众听得是什么或者他们正在听什么,只需要发布他们的节目就可以了。而观众也不知道广播站制作节目的过程,他们只要在他们最喜欢的节目运行的时候把台调到对应的频道或者告知朋友就行。观察者模式和发布-订阅模式的比较两者的比较如下图所示:观察者模式必须知道具体的Subject,两者可以直接联系紧耦合大多数是同步的在单个应用程序地址空间中实现发布订阅模式无直接依赖关系,通过消息代理松耦合大多数是异步的(使用消息队列)交叉应用模式参考链接《对象间的联动——观察者模式(二) - 设计模式之行为型模式 - 极客学院Wiki》[《[译] 设计模式:发布/订阅模式解析 - 记录技术的点滴 - SegmentFault 思否》][5]《观察者模式和发布订阅模式有什么不同? - 知乎》 ...

January 9, 2019 · 1 min · jiezi

ngrx

ngrx/store本文档会持续更新。StoreStrore是Angular基于Rxjs的状态管理,保存了Redux的核心概念,并使用RxJs扩展的Redux实现。使用Observable来简化监听事件和订阅等操作。在看这篇文章之前,已经假设你已了解rxjs和redux。官方文档 有条件的话,请查看官方文档进行学习理解。安装npm install @ngrx/storeTutorial下面这个Tutorial将会像你展示如何管理一个计数器的状态和如何查询以及将它显示在Angular的Component上。你可以通过StackBlitz来在线测试。1.创建actionssrc/app/counter.actions.tsimport {Action} from ‘@ngrx/store’;export enum ActionTypes { Increment = ‘[Counter Component] Increment’, Decrement = ‘[Counter Component] Decrement’, Reset = ‘[Counter Component] Reset’,}export class Increment implements Action { readonly type = ActionTyoes.Increment;}export class Decrement implements Action { readonly type = ActionTypes.Decrement;}export class Reset implements Action { readonly tyoe = Actiontypes.Reset;}2.定义一个reducer通过所提供的action来处理计数器state的变化。src/app/counter.reducer.tsimport {Action} from ‘@ngrx/store’;import {ActionTypes} from ‘./conter.actions’;export const initailState = 0;export function conterReducer(state = initialState, action: Action) { switch(action.type) { case ActionTypes.Increment: return state + 1; case ActionTypes.Decrement: return state - 1; case ActionTypes.Reset: return 0; default: return state; }}3.在src/app/app.module.ts中导入 StoreModule from @ngrx/store 和 counter.reducerimport {StroeModule} from ‘@ngrx/store’;import {counterReducer} from ‘./counter.reducer’;4.在你的AppModule的imports array添加StoreModule.forRoot,并在StoreModule.forRoot中添加count 和 countReducer对象。StoreModule.forRoot()函数会注册一个用于访问store的全局变量。scr/app/app.module.tsimport { BrowserModule } from ‘@angular/platform-browser’;import { NgModule } from ‘@angular/core’; import { AppComponent } from ‘./app.component’; import { StoreModule } from ‘@ngrx/store’;import { counterReducer } from ‘./counter.reducer’;@NgModule({ declaration: [AppComponent], imports: [ BrowserModule, StoreModule.forRoot({count: countReducer}) ], provoders: [], bootstrap: [AppComponent]})export class AppModule {}5.在app文件夹下新创建一个叫my-counter的Component,注入Store service到你的component的constructor函数中,并使用select操作符在state查询数据。更新MyCounterComponent template,添加添加、减少和重设操作,分别调用increment,decrement,reset方法。并使用async管道来订阅count$ Observable。src/app/my-counter/my-counter.component.html<button (click)=“increment()">Increment</button> <div>Current Count: {{ count$ | async }}</div><button (click)=“decrement()">Decrement</button><button (click)=“reset()">Reset Counter</button>更新MyCounterComponent类,创建函数并分发(dispatch)Increment,Decrement和Reset actions.import { Component } from ‘@angular/core’;import { Store, select } from ‘@ngrx/store’;import { Observable } from ‘rxjs’;import { Increment, Decrement, Reset } from ‘../counter.actions’;@Component({ selector: ‘app-my-counter’, templateUrl: ‘./my-counter.component.html’, styleUrls: [’./my-counter.component.css’],})export class MyCounterComponent ( count$: Observable<number>; constructor(private store: Stare<{count: number}>) { this.count$ = store.pipe(select(‘count’)); } increment() { this.store.dispatch(new Increment()); } decrement() { this.store.dispatch(new Decrement()); } reset() { this.store.dispatch(new Reset()); })6.添加MyCounter component到AppComponent template中<app-my-counter></app-my-counter>ActionsActions是NgRx的核心模块之一。Action表示在整个应用中发生的独特的事件。从用户与页面的交互,与外部的网络请求的交互和直接与设备的api交互,这些和更多的事件通过actions来描述。介绍在NgRx的许多地方都使用了actions。Actions是NgRx许多系统的输入和输出。Action帮助你理解如何在你的应用中处理事件。Action接口(Action interface)NgRx通过简单的interface来组成Action:interface Action { type: string;}这个interface只有一个属性:type,string类型。这个type属性将描述你的应用调度的action。这个类型的值以[Source]的形式出现和使用,用于提供它是什么类型的操作的上下文和action在哪里被调度(dispatched)。您可以向actions添加属性,以便为操作提供其他上下文或元数据。最常见的属性就是payload,它会添加action所需的所有数据。下面列出的是作为普通javascript对象编写的操作的示例:{ type: ‘[Auth API] Login Success’}这个action描述了调用后端API成功认证的时间触发。{ type: ‘[Login Page]’, payload: { username: string; password: string; }}这个action描述了用户在登录页面点击登录按钮尝试认证用户的时间触发。payload包含了登录页面提供的用户名和密码。编写 actions有一些编写actions的好习惯:前期——在开始开发功能之前编写编写action,以便理解功能和知识点分类——基于事件资源对actions进行分类编写更多——action的编写容易,所以你可以编写更多的actions,来更好的表达应用流程事件-驱动——捕获事件而不是命令,因为你要分离事件的描述和事件的处理描述——提供针对唯一事件的上下文,其中包含可用于帮助开发人员进行调试的更详细信息遵循这些指南可帮助您了解这些actions在整个应用程序中的流程。下面是一个启动登陆请求的action示例:import {} from ‘@ngrx/store’;export class Login Implements Action { readonly type = ‘[Login Page] Login’ constructor(public: payload: {username: string, password: string}){}}action编写成类,以便在dispatched操作时提供类型安全的方法来构造action。Login action 实现(implements) Action interface。在示例中,payload是一个包含username和password的object,这是处理action所需的其他元数据.在dispatch时,新实例化一个实例。login-page.component.tsclick(username: string, password: string) { store.dispatch(new Login({username:username, password: password}))}Login action 有关于action来自于哪里和事件发生了什么的独特上线文。action的类型包含在[]内类别用于对形状区域的action进行分组,无论他是组件页面,后端api或浏览器api类别后面的Login文本是关于action发生了什么的描述。在这个例子中,用户点击登录页面上的登录按钮来通过用户名密码来尝试认证。创建action unionsactions的消费者,无论是reducers(纯函数)或是effects(带副作用的函数)都使用actions的type来确定是否要执行这个action。在feature区域,多个actions组合在一起,但是每个action都需要提供自己的type信息。看上一个Login action 例子,你将为action定义一些额外的信息。import {Action} from ‘@ngrx/store’;export enum ActionTypes { Login = ‘[Login Page] Login’;}export class Login Implememts Action { readonly type = ActionTypes.Login; constructor(public paylad: {username: string, password: string})}export type Union = Login;将action type string放在enum中而不是直接放在class内。此外,还会使用Union类去导出Loginclass.ReducersNgRx中的Reducers负责处理应用程序中从一个状态到下一个状态的转换。Reducer函数从action的类型来确定如何处理状态。介绍Reducer函数是一个纯函数,函数为相同的输入返回相同的输出。它们没有副作用,可以同步处理每个状态转化。每个reducer都会调用最新的action,当前状态(state)和确定是返回最新修改的state还是原始state。这个指南将会向你展示如何去编写一个reducer函数,并在你的store中注册它,并组成独特的state。关于reducer函数每一个由state管理的reducer都有一些共同点:接口和类型定义了state的形状参数包含了初始state或是当前state、当前actionswitch语句下面这个例子是state的一组action,和相对应的reducer函数。首先,定义一些与state交互的actions。scoreboard-page.actions.tsimport {Action} from ‘@ngrx/store’;export enum Actiontypes { IncrementHome = ‘[Scoreboard Page] Home Score’, IncrementAway = ‘[Scoreboard Page] Away Score’, Reset = ‘[Scoreboard Page] Score Reset’,}export class IncrementHome implements Action { readonly type = ActionTypes.IncrementHome;}export class IncrementAway implements Action { readonly type = ActionTypes.IncrementAway;}export class Reset implements Action { readonly type = ActionTypes.Reset; constructor(public payload: {home: number, away: number}) {}}export type ActionsUnion = IncrementHome | IncrementAway | Reset;接下来,创建reducer文件,导入actions,并定义这个state的形状。定义state的形状每个reducer函数都会监听actions,上面定义的scorebnoard actions描述了reducer处理的可能转化。导入多组actions以处理reducer其他的state转化。scoreboard.reducer.tsimport * as Scoreboard from ‘../actions/scoreboard-page.actions’;export interface State { home: number; away: number;}根据你捕获的内容来定义state的形状,它是单一的类型,如number,还是一个含有多个属性的object。设置初始state初始state给state提供了初始值,或是在当前state是undefined时提供值。您可以使用所需state属性的默认值设置初始state。创建并导出变量以使用一个或多个默认值捕获初始state。scoreboard.reducer.tsexport const initialState: Satate = { home: 0, away: 0,};创建reducer函数reducer函数的职责是以不可变的方式处理state的更变。定义reducer函数来处理actions来管理state。scoreboard.reducer.tsexport function reducer { satate = initialState, action: Scoreboard.ActionsUnion}: State { switch(action.type) { case Scoreboard.ActionTypes.IncrementHome: { return { …state, home: state.home + 1, } } case Scoreboard.ActionTypes.IncrementAway: { return { …state, away: state.away + 1, } } case Scoreboard.ActionTypes.Reset: { return action.payload; } default: { return state; } }}Reducers将switch语句与TypeScript在您的actions中定义的区分联合组合使用,以便在reducer中提供类型安全的操作处理。Switch语句使用type union来确定每种情况下正在使用的actions的正确形状。action的types定在你的action在你的reducer函数的case语句。type union 也约束你的reducer的可用操作。在这个例子中,reducer函数处理3个actions:IncrementHome,IncrementAway,Reset。每个action都有一个基于ActionUnion提供的强类型。每个action都可以不可逆的处理state。这意味着state更变不会修改源state,而是使用spread操作返回一个更变后的新的state。spread语法从当前state拷贝属性,并创建一个新的返回。这确保每次更变都会有新的state,保证了函数的纯度。这也促进了引用完整性,保证在发生状态更改时丢弃旧引用注意:spread操作只执行浅复制,不处理深层嵌套对象。您需要复制对象中的每个级别以确保不变性。有些库可以处理深度复制,包括lodash和immer。当action被调度时,所有注册过的reducers都会接收到这个action。通过switch语句确定是否处理这个action。因为这个原因,每个switch语句中总是包含default case,当这个reducer不处理action时,返回提供的state。注册root statestate在你的应用中定义为一个large object。注册reducer函数。注册reducer函数来管理state的各个部分中具有关联值的键。使用StoreModule.forRoot()函数和键值对来定义你的state,来在你的应用中注册一个全局的Store。StoreModule.forRoot()在你的应用中注册一个全局的providers,将包含这个调度state的action和select的Store服务注入到你的component和service中。app.module.tsimport {NgModule} from ‘@angular/core’;import {StoreModule} form ‘@ngrx/store’;import {scoreboardReducer} from ‘./reducers/scoreboard.resucer’;@NgModule({ imports: [StoreModule.forRoot({game: scoreboardReducer})],})export class AppModule {}使用StoreModule.forRoot()注册states可以在应用启动时定义状态。通常,您注册的state始终需要立即用于应用的所有区域。注册形状state形状states的行为和root state相同,但是你在你的应用中需要定义具体的形状区域。你的state是一个large object,形状state会在这个object中以键值对的形式注册。下面这个state object的例子,你将看到形状state如何以递增的方式构建你的state。让我们从一个空的state开始。app.module.ts@NgModule({ imports: [StoreModule.forRoot({})],})export class AppModule {}这里在你的应用中创建了一个空的state{}现在使用scoreboardreducer和名称为ScoreboarModule的形状NgModule注册一个额外的state。scoreboard.module.tsimport { NgModule } from ‘@angular/core’;import { StoreModule } from ‘@ngrx/store’;import { scoreboardReducer } from ‘./reducers/scoreboard.reducer’;@NgModule({ imports: [StoreModule.forFeature(‘game’, scoreboardReducer)],})export class ScoreboardModule {}添加ScoreboardModule到APPModule。app.module.tsimport { NgModule } from ‘@angular/core’;import { StoreModule } from ‘@ngrx/store’;import { ScoreboardModule } from ‘./scoreboard/scoreboard.module’;@NgModule({ imports: [StoreModule.forRoot({}), ScoreboardModule],})export class AppModule {}每一次ScoreboardModule被加载,这个game将会变为这个object的一个属性,并被管理在state中。{ game: { home: 0, away: 0}}形状state的加载是eagerly还是lazlly的,取决于你的应用。可以使用形状状态随时间和不同形状区域构建状态对象。selectSelector是一个获得store state的切片的纯函数。@ngrx/store提供了一些辅助函数来简化selection。selector提供了很多对state的切片功能。轻便的记忆化组成的可测试的类型安全的当使用createSelector和createFeatureSelector函数时,@ngrx/store会跟踪调用选择器函数的最新参数。因为选择器是纯函数,所以当参数匹配时可以返回最后的结果而不重新调用选择器函数。这可以提供性能优势,特别是对于执行昂贵计算的选择器。这种做法称为memoization。使用selector切片stateindex.tsimport {createSelector} from ‘@ngrx/store’;export interface FeatureState { counter: number;}export interface AppSatte { feature: FeatureState;}export const selectFeature = (state: AppState) => state.feature;export const selectFeatureCount = createSelector( selectFeature, (state: FeatrureState) => state.counter)使用selectors处理多切片createSelector能够从基于同样一个state的几个切片state中获取一些数据。createSelector最多能够接受8个selector函数,以获得更加完整的state selections。在下面这个例子中,想象一下你有selectUser object 在你的state中,你还有book object的allBooks数组。你想要显示你当前用户的所有书。你能够使用createSelector来实现这些。如果你在allBooks中更新他们,你的可见的书将永远是最新的。如果选择了一本书,它们将始终显示属于您用户的书籍,并且在没有选择用户时显示所有书籍。结果将会是你从你的state中过滤一部分,并且他永远是最新的。import {createSelecotr} from ‘@ngrx/store’;export interface User { id: number; name: string;}export interface Book { id: number; userId: number; name: string;}export interface AppState { selectoredUser: User; allBooks: Book[];}export const selectUser = (state: AppSate) => state.selectedUser;export const SelectAllBooks = (state: AppState) => state.allBooks;export const SelectVisibleBooks = createSelector( selectUser, selectAllBooks, (selectedUser: User, allBooks: Books[]) => { if(selectedUser && allBooks) { return allBooks.filter((book: Book) => book.UserId === selectedUser.id); }else { return allBooks; } })使用selecotr props当store中没有一个适合的select来获取切片state,你可以通过selector函数的props。在下面的例子中,我们有计数器,并希望他乘以一个值,我们可以添加乘数并命名为prop:index.tsexport const getCount = createSelector( getCounterValue, (counter, props) => counter * props.multiply);在这个组件内部,我们定义了一个props。ngOnInit() { this.counter = this.store.pipe(select(formRoot.getCount, {multiply: 2}));}记住,selector只将之前的输入参数保存在了缓存中,如果你用另一个乘数来重新使用这个selector,selector总会去重新计算它的值,这是因为他在接收两个乘数。为了正确地记忆selector,将selector包装在工厂函数中以创建选择器的不同实例index.tsexport const getCount = () => { createSelector( (state, props) => state.counter[props.id], (counter, props) => counter * props* multiply );}组件的selector现在调用工厂函数来创建不同的选择器实例: ngOnInit() { this.counter2 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter2’, multiply: 2 })); this.counter4 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter4’, multiply: 4 })); this.counter6 = this.store.pipe(select(fromRoot.getCount(), { id: ‘counter6’, multiply: 6 })); } ...

December 29, 2018 · 4 min · jiezi

用 RxJS 实现 Redux Form

写在前面的话看这篇文章之前,你需要掌握的知识:ReactRxJS (至少需要知道 Subject 是什么)背景form 可以说是 web 开发中的最大的难题之一。跟普通的组件相比,form 具有以下几个特点:更多的用户交互。这意味着可能需要大量的自定义组件,比如 DataPicker,Upload,AutoComplete 等等。频繁的状态改变。每当用户输入一个值,都可能会对应用状态造成改变,从而需要更新表单元素或者显示错误信息。表单校验,也就是对用户输入数据的有效性进行验证。表单验证的形式也很多,比如边输入边验证,失去焦点后验证,或者在提交表单之前验证等等。异步网络通信。当用户输入和异步网络通信同时存在时,需要考虑的东西就更多了。就比如 AutoComplete,需要根据用户的输入去异步获取相应的数据,如果用户每输入一次就发起一次请求,会对资源造成很大浪费。因为每一次输入都是异步获取数据的,那么连续两次用户输入拿到的数据也有可能存在 “后发先至” 的问题。正因为以上这些特点,使 form 的开发变得困难重重。在接下来的章节中,我们会将 RxJS 和 Form 结合起来,帮助我们更好的去解决这些问题。HTML Form在实现我们自己的 Form 组件之前,让我们先来参考一下原生的 HTML Form。保存表单状态对于一个 Form 组件来说,需要保存所有表单元素的信息(如 value, validity 等),HTML Form 也不例外。那么,HTML Form 将表单状态保存在什么地方?如何才能获取表单元素信息?主要有以下几种方法:document.forms 会返回所有 <form> 表单节点。HTMLFormElement.elements 返回所有表单元素。event.target.elements 也能获取所有表单元素。document.forms[0].elements[0].value; // 获取第一个 form 中第一个表单元素的值const form = document.querySelector(“form”);form.elements[0].value; form.addEventListener(‘submit’, function(event) { console.log(event.target.elements[0].value);});Validation表单校验的类型一般分为两种:内置表单校验。默认会在提交表单的时候自动触发。通过设置 novalidate 属性可以关闭浏览器的自动校验。JavaScript 校验。<form novalidate> <input name=‘username’ required/> <input name=‘password’ type=‘password’ required minlength=“6” maxlength=“6”/> <input name=‘email’ type=‘email’/> <input type=‘submit’ value=‘submit’/></form>存在的问题定制化很难。 比如不支持 Inline Validation,只有 submit 时才能校验表单,且 error message 的样式不能自定义。难以应对复杂场景。 比如表单元素的嵌套等。Input 组件的行为不统一,从而难以获取表单元素的值。 比如 checkbox 和 multiple select,取值的时候不能直接取 value,还需要额外的转换。var $form = document.querySelector(‘form’);function getFormValues(form) { var values = {}; var elements = form.elements; // elemtns is an array-like object for (var i = 0; i < elements.length; i++) { var input = elements[i]; if (input.name) { switch (input.type.toLowerCase()) { case ‘checkbox’: if (input.checked) { values[input.name] = input.checked; } break; case ‘select-multiple’: values[input.name] = values[input.name] || []; for (var j = 0; j < input.length; j++) { if (input[j].selected) { values[input.name].push(input[j].value); } } break; default: values[input.name] = input.value; break; } } } return values;}$form.addEventListener(‘submit’, function(event) { event.preventDefault(); getFormValues(event.target); console.log(event.target.elements); console.log(getFormValues(event.target));});React Rx Form感兴趣的同学可以先去看一下源码 https://github.com/reeli/reac...React 与 RxJSRxJS 是一个非常强大的数据管理工具,但它并不具备用户界面渲染的功能,而 React 却特别擅长处理界面。那何不将它们的长处结合起来?用 React 和 RxJS 来解决我们的 Form 难题。既然知道了它们各自的长处,所以分工也就比较明确了: RxJS 负责管理状态,React 负责渲染界面。设计思路与 Redux Form 不同的是,我们不会将 form 的状态存储在 store 中,而是直接保存在 <Form/> 组件中。然后利用 RxJS 将数据通知给每一个 <Field/> ,然后 <Field/> 组件会根据数据去决定自己是否需要更新 UI,需要更新则调用 setState ,否则什么也不做。举个例子,假设在一个 Form 中有三个 Field (如下),当只有 FieldA 的 value 发生变化时, 为了不让 <Form/> 和其子组件也 re-render,Redux Form 内部需要通过 shouldComponentUpdate() 去限制。// 伪代码<Form> <FieldA/> <FieldB/> <FieldC/></Form>而 RxJS 能把组件更新的粒度控制到最小,换句话说,就是让真正需要 re-render 的 <Field/> re-render,而不需要 re-render 的组件不重新渲染 。核心是 Subject从上面的设计思路可以总结出以下两个问题:Form 和 Field 是一对多的关系,form 的状态需要通知给多个 Field。Field 需要根据数据去修改组件的状态。第一个问题,需要的是一个 Observable 的功能,而且是能够支持多播的 Observable。第二个问题需要的是一个 Observer 的功能。在 RxJS 中,既是 Observable 又是 Observer,而且还能实现多播的,不就是 Subject 么!因此,在实现 Form 时,会大量用到 Subject。formState 数据结构Form 组件中也需要一个 State,用来保存所有 Field 的状态,这个 State 就是 formState。那么 formState 的结构应该如何定义呢?在最早的版本中,formState 的结构是长下面这个样子的:interface IFormState { [fieldName: string]: { dirty?: boolean; touched?: boolean; visited?: boolean; error?: TError; value: string; };}formState 是一个对象,它以 fieldName 为 key,以一个 保存了 Field 状态的对象作为它的 value。看起来没毛病对吧?但是。。。。。最后 formState 的结构却变成了下面这样:interface IFormState { fields: { [fieldName: string]: { dirty?: boolean; touched?: boolean; visited?: boolean; error?: string | undefined; }; }; values: { [fieldName: string]: any; };}Note: fields 中不包含 filed value,只有 field 的一些状态信息。values 中只有 field values。为什么呢???其实在实现最基本的 Form 和 Field 组件时,以上两种数据结构都可行。那问题到底出在哪儿?这里先买个关子,目前你只需要知道 formState 的数据结构长什么样就可以了。数据流为了更好的理解数据流,让我们来看一个简单的例子。我们有一个 Form 组件,它的内部包含了一个 Field 组件,在 Field 组件内部又包含了一个 Text Input。数据流可能是像下面这样的:用户在输入框中输入一个字符。Input 的 onChange 事件会被 Trigger。Field 的 onChange Action 会被 Dispatch。根据 Field 的 onChange Action 对 formState 进行修改。Form State 更新之后会通知 Field 的观察者。Field 的观察者将当前 Field 的 State pick 出来,如果发现有更新则 setState ,如果没有更新则什么都不做。setState 会使 Field rerender ,新的 Field Value 就可以通知给 Input 了。核心组件首先,我们需要创建两个基本组件,一个 Field 组件,一个 Form 组件。Field 组件Field 组件是连接 Form 组件和表单元素的中间层。它的作用是让 Input 组件的职责更单一。有了它之后,Input 只需要做显示就可以了,不需要再关心其他复杂逻辑(validate/normalize等)。况且,对于 Input 组件来说,不仅可以用在 Form 组件中,也可以用在 Form 组件之外的地方(有些地方可能并不需要 validate 等逻辑),所以 Field 这一层的抽象还是非常重要的。拦截和转换。 format/parse/normalize。表单校验。 参考 HTML Form 的表单校验,我们可以把 validation 放在 Field 组件上,通过组合验证规则来适应不同的需求。触发 field 状态的 改变(如 touched,visited)给子组件提供所需信息。 向下提供 Field 的状态 (error, touched, visited…),以及用于表单元素绑定事件的回调函数 (onChange,onBlur…)。利用 RxJS 的特性来控制 Field 组件的更新,减少不必要的 rerender。 与 Form 进行通信。 当 Field 状态发生变化时,需要通知 Form。在 Form 中改变了某个 Field 的状态,也需要通知给 Field。Form 组件管理表单状态。 Form 组件将表单状态提供给 Field,当 Field 发生变化时通知 Form。提供 formValues。在表单校验失败的时候,阻止表单的提交。通知 Field 每一次 Form State 的变化。 在 Form 中会创建一个 formSubject&dollar;,每一次 Form State 的变化都会向 formSubject&dollar; 上发送一个数据,每一个 Field 都会注册成为 formSubject&dollar; 的观察者。也就是说 Field 知道 Form State 的每一次变化,因此可以决定在适当的时候进行更新。当 FormAction 发生变化时,通知给 Field。 比如 startSubmit 的时候。组件之间的通信Form 和 Field 通信。Context 主要用于跨级组件通信。在实际开发中,Form 和 Field 之间可能会跨级,因此我们需要用 Context 来保证 Form 和 Field 的通信。Form 通过 context 将其 instance 方法和 formState 提供给 Field。Field 和 Form 通信。Form 组件会向 Field 组件提供一个 d__ispatch__ 方法,用于 Field 和 Form 进行通信。所有 Field 的状态和值都由 Form 统一管理。如果期望更新某个 Field 的状态或值,必须 dispatch 相应的 action。表单元素和 Field 通信表单元素和 Field 通信主要是通过回调函数。Field 会向表单元素提供 onChange,onBlur 等回调函数。接口的设计对于接口的设计来说,简单清晰是很重要的。所以 Field 只保留了必要的属性,没有将表单元素需要的其他属性通过 Field 透传下去,而是交给表单元素自己去定义。通过 Child Render,将对应的状态和方法提供给子组件,结构和层级更加清晰了。Field:type TValidator = (value: string | boolean) => string | undefined;interface IFieldProps { children: (props: IFieldInnerProps)=> React.ReactNode; name: string; defaultValue?: any; validate?: TValidator | TValidator[];}Form:interface IRxFormProps { children: (props: IRxFormInnerProps) => React.ReactNode; initialValues?: { [fieldName: string]: any; }}到这里,一个最最基本的 Form 就完成了。接下来我们会在它的基础上进行一些扩展,以满足更多复杂的业务场景。EnhanceFieldArrayFieldArray 主要用于渲染多组 Fields。回到我们之前的那个问题,为什么要把 formState 的结构分为 fileds 和 values?其实问题就出在 FieldArray,初始长度由 initLength 或者 formValues 决定。formState 整体更新。FormValues通过 RxJS,我们将 Field 更新的粒度控制到了最小,也就是说如果一个 Field 的 Value 发生变化,不会导致 Form 组件和其他 Feild 组件 rerender。既然 Field 只能感知自己的 value 变化,那么问题就来了,如何实现 Field 之间的联动?于是 FormValues 组件就应运而生了。每当 formValues 发生变化,FormValues 组件会就把新的 formValues 通知给子组件。也就是说如果你使用了 FormValues 组件,那么每一次 formValues 的变化都会导致 FormValues 组件以及它的子组件 rerender,因此不建议大范围使用,否则可能带来性能问题。总之,在使用 FormValues 的时候,最好把它放到一个影响范围最小的地方。也就是说,当 formValues 发生变化时,让尽可能少的组件 rerender。在下面的代码中,FieldB 的显示与否需要根据 FieldA 的 value 来判断,那么你只需要将 FormValues 作用于 FIeldA 和 FieldB 就可以了。<FormValues> {({ formValues, updateFormValues }) => ( <> <FieldA name=“A” /> {!!formValues.A && <FieldB name=“B” />} </> )}</FormValues>FormSectionFormSection 主要是用于将一组 Fields group 起来,以便在复用在多个 form 中复用。主要是通过给 name添加前缀来实现的。那么怎样给 Field 和 FieldArray 的 name 添加前缀呢?我首先想到的是通过 React.Children 拿到子组件的 name,再和 FormSection 的 name 拼接起来。但是,FormSection 和 Field 有可能不是父子关系!因为 Field 组件还可以被抽成一个独立的组件。因此,存在跨级组件通信的问题。没错!跨级组件通信我们还是会用到 context。不过这里我们需要先从 FormConsumer 中拿到对应的 context value,再通过 Provider 将 prefix 提供给 Consumer。这时 Field/FieldArray 通过 Consumer 拿到的就是 FormSection 中的 Provider 提供的值,而不再是由 Form 组件的 Provider 所提供。因为 Consumer 会消费离自己最近的那个 Provider 提供的值。<FormConsumer> {(formContextValue) => { return ( <FormProvider value={{ …formContextValue, fieldPrefix: ${formContextValue.fieldPrefix || ""}${name}., }} > {children} </FormProvider> ); }}</FormConsumer>测试Unit Test主要用于工具类方法。Integration Test主要用于 Field,FieldArray 等组件。因为它们不能脱离 Form 独立存在,所以无法对其使用单元测试。Note: 在测试中,无法直接修改 instance 上的某一个属性,以为 React 将 props 上面的节点都设置成了 readonly (通过 Object.defineProperty 方法)。 但是可以通过整体设置 props 绕过。instance.props = { …instance.props, subscribeFormAction: mockSubscribeFormAction, dispatch: mockDispatch,};Auto Fill Form Util如果项目中的表单过多,那么对于 QA 测试来说无疑是一个负担。这个时候我们希望能够有一个自动填表单的工具,来帮助我们提高测试的效率。在写这个工具的时候,我们需要模拟 Input 事件。input.value = ‘v’;const event = new Event(‘input’, {bubbles: true});input.dispatchEvent(event);我们的期望是,通过上面的代码去模拟 DOM 的 input 事件,然后触发 React 的 onChange 事件。但是 React 的 onChange 事件却没有被触发。因此无法给 input 元素设置 value。因为 ReactDOM 在模拟 onChange 事件的时候有一个逻辑:只有当 input 的 value 改变,ReactDOM 才会产生 onChange 事件。React 16+ 会覆写 input value setter,具体可以参考 ReactDOM 的 inputValueTracking。因此我们只需要拿到原始的 value setter,call 调用就行了。const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, “value”).set;nativeInputValueSetter.call(input, “v”);const event = new Event(“input”, { bubbles: true});input.dispatchEvent(event);Debug打印 Log在 Dev 环境中,可以通过 Log 来进行 Debug。目前在 Dev 环境下会自动打印 Log,其他环境则不会打印 Log。Log 的信息主要包括: prevState, action, nextState。Note: 由于 prevState, action, nextState 都是 Object,所以别忘了在打印的时候调用 cloneDeep,否则无法保证最后打印出来的值的正确性,也就是说最后得到的结果可能不是打印的那一时刻的值。最后这篇文章只讲了关于 React Rx Form 的思路以及一些核心技术,大家也可以按照这个思路自己去实现一版。当然,也可以参考一下源码,欢迎来提建议和 issue。Github 地址: https://github.com/reeli/reac… ...

December 28, 2018 · 4 min · jiezi

electron 网页和主进程通讯

前端网页import { RxIpc} from ‘rx-ipc-electron/lib/rx-ipc’;export const IsElectron = window.navigator.userAgent.toLowerCase().indexOf(’electron’) !== -1;export function ajax(config) { if (!IsElectron) return Promise.reject(‘only for electron’); const { ipcRenderer } = eval(require('electron')); const rxIpc = new RxIpc(ipcRenderer); return new Promise((resolve, reject) => { rxIpc.runCommand(‘ajax’, null, config).subscribe(resolve, reject); });}window[’test1’] = async function test1() { const url = ‘https://www.baidu.com/s'; const params = { wd: 1 }; const rsp = await ajax({ url, method: ‘get’, params, }); console.log(rsp);}electron进程import rxIpc from ‘rx-ipc-electron/lib/main’;import { Observable } from ‘rxjs’;import axios from ‘axios’;rxIpc.registerListener(‘ajax’, config => { return Observable.from(axios(config));}); ...

December 19, 2018 · 1 min · jiezi

一起来看 rxjs

更新日志2018-05-26 校正2016-12-03 第一版翻译过去你错过的 Reactive Programming 的简介你好奇于这名为Reactive Programming(反应式编程)的新事物, 更确切地说,你想了解它各种不同的实现(比如 [Rx*], [Bacon.js], RAC 以及其它各种各样的框架或库)学习它比较困难, 因为比较缺好的学习材料(译者注: 原文写就时, RxJs 还在 v4 版本, 彼时社区对 RxJs 的探索还不够完善). 我在开始学习的时候, 试图找过教程, 不过能找到的实践指南屈指可数, 而且这些教程只不过隔靴搔痒, 并不能帮助你做真正了解 RxJs 的基本概念. 如果你想要理解其中一些函数, 往往代码库自带的文档帮不到你. 说白了, 你能一下看懂下面这种文档么:Rx.Observable.prototype.flatMapLatest(selector, [thisArg])按照将元素的索引合并的方法, 把一个 “observable 队列 " 中的作为一个新的队列加入到 “observable 队列的队列” 中, 然后把 “observable 队列的队列” 中的一个 “observable 队列” 转换成一个 “仅从最近的 ‘observable 队列’ 产生的值构成的一个新队列.“这是都是什么鬼?我读了两本书, 一本只是画了个大致的蓝图, 另一本则是一节一节教你 “如何使用 Reactive Libarary” . 最后我以一种艰难的方式来学习 Reactive Programming: 一遍写, 一遍理解. 在我就职于 Futurice 的时候, 我第一次在一个真实的项目中使用它, 我在遇到问题时, 得到了来自同事的支持.学习中最困难的地方是 以 Reactive(反应式) 的方式思考. 这意思就是, 放下你以往熟悉的编程中的命令式和状态化思维习惯, 鼓励自己以一种不同的范式去思考. 至今我还没在网上找到任何这方面的指南, 而我认为世界上应该有一个说明如何以 Reactive(反应式) 的方式思考的教程, 这样你才知道要如何开始使用它. 在阅读完本文后之后. 请继续阅读代码库自带的文档来指引你之后的学习. 我希望, 这篇文档对你有所帮助.“什么是 Reactive Programming(反应式编程)?“在网上可以找到大量对此糟糕的解释和定义. Wikipedia 的 意料之中地泛泛而谈和过于理论化. Stackoverflow 的 圣经般的答案也绝对不适合初学者. Reactive Manifesto 听起来就像是要给你公司的项目经理或者是老板看的东西. 微软的 Rx 术语 “Rx = Observables + LINQ + Schedulers” 也读起来太繁重, 太微软了, 以至于你看完后仍然一脸懵逼. 类似于 “reactive” 和 “propagation” 的术语传达出的含义给人感觉无异于你以前用过的 MV* 框架和趁手的语言已经做到的事情. 我们现有的框架视图当然是会对数据模型做出反应, 任何的变化当然也是要冒泡的. 要不然, 什么东西都不会被渲染出来嘛.所以, 让我们撇开那些无用的说辞, 尝试去了解本质.Reactive programming(反应式编程) 是在以异步数据流来编程当然, 这也不是什么新东西. 事件总线或者是典型的点击事件确实就是异步事件流, 你可以对其进行 observe(观察) 或者做些别的事情. 不过, Reactive 是比之更优秀的思维模型. 你能够创建任何事物的数据流, 而不只是从点击和悬浮事件中. “流” 是普遍存在的, 一切都可能是流: 变量, 用户输入, 属性, 缓存, 数据结构等等. 比如, 想象你的 Twitter 时间线会成为点击事件同样形式的数据流.熟练掌握该思维模型之后, 你还会接触到一个令人惊喜的函数集, 其中包含对任何的数据流进行合并、创建或者从中筛选数据的工具. 它充分展现了 “函数式” 的魅力所在. 一个流可以作为另一个流的输入. 甚至多个流可以作为另一个流的输入. 你可以合并两个流. 你可以筛选出一个仅包含你需要的数据的另一个流. 你可以从一个流映射数据值到另一个流.让我们基于 “流是 Reactive 的中心” 这个设想, 来细致地做看一下整个思维模型, 就从我们熟知的 “点击一个按钮” 事件流开始.每个流是一个按时序不间断的事件序列. 它可能派发出三个东西: (某种类型的)一个数值, 一个错误, 或者一个 “完成” 信号. 说到 “完成” , 举个例子, 当包含了这个按钮的当前窗口/视图关闭时, 也就是 “完成” 信号发生时.我们仅能异步地捕捉到这些事件: 通过定义三种函数, 分别用来捕捉派发出的数值、错误以及 “完成” 信号. 有时候后两者可以被忽略, 你只需定义用来捕捉数值的函数. 我们把对流的 “侦听” 称为订阅(subscribing), 我们定义的这三种函数合起来就是观察者, 流则是被观察的主体(或者叫"被观察者”). 这正是设计模式中的观察者模式.描述这种方式的另一种方式用 ASCII 字符来画个导图, 在本教程的后续的部分也能看到这种图形.–a—b-c—d—X—|->a, b, c, d 代表被派发出的值X 代表错误| 代表"完成"信号—> 则是时间线这些都是是老生常谈了, 为了不让你感到无聊, 现在来点新鲜的东西: 我们将原生的点击事件流进行变换, 来创建新的点击事件流.首先, 我们做一个计数流, 来指明一个按钮被点击了多少次. 在一般的 Reactive 库中, 每个流都附带了许多诸如map, filter, scan 等的方法. 当你调用这些方法之一(比如比如clickStream.map(f))时, 它返回一个基于 clickStream 的新的流. 它没有对原生的点击事件做任何修改. 这种(不对原有流作任何修改的)特性叫做immutability(不可变性), 而它和 Reactive(反应式) 这个概念的契合度之高好比班戟和糖浆(译者注: 班戟就是薄煎饼, 该称呼多见于中国广东地区. 此句意为 immutability 与 Reactive 两个概念高度契合). 这样的流允许我们进行链式调用, 比如clickStream.map(f).scan(g): clickStream: —c—-c–c—-c——c–> vvvvv map(c becomes 1) vvvv —1—-1–1—-1——1–> vvvvvvvvv scan(+) vvvvvvvvvcounterStream: —1—-2–3—-4——5–>map(f) 方法根据你提供的函数f替换每个被派发的元素形成一个新的流. 在上例中, 我们将每次点击都映射为计数 1. scan(g) 方法则在内部运行x = g(accumulated, current), 以某种方式连续聚合处理该流之前所有的值, 在该例子中, g 就是简单的加法. 然后, 一次点击发生时, counterStream 就派发一个点击数的总值.为了展示 Reactive 真正的能力, 我们假设你想要做一个 “双击事件” 的流. 或者更厉害的, 我们假设我们想要得到一个 “三击事件流” , 甚至推广到更普遍的情况, “多击流”. 现在, 深呼吸, 想象一下按照传统的命令式和状态化思维习惯要如何完成这项工作? 我敢说那会烦死你了, 它必须包含各种各样用来保持状态的变量, 以及一些对周期性工作的处理.然而, 以 Reactive 的方式, 它会非常简单. 事实上, 这个逻辑只不过是四行代码. 不过让我们现在忘掉代码.无论你是个初学者还是专家, 借助导图来思考, 才是理解和构建流最好的方法.图中的灰色方框是将一个流转换成另一个流的方法. 首先, 每经过 “250毫秒” 的 “事件静默” (简单地说, 这是在 buffer(stream.throttle(250ms)) 完成的. (现在先)不必担心对这点的细节的理解, 我们主要是演示下 Reactive 的能力.), 我们就得到了一个 “点击动作” 的列表, 即, 转换的结果是一个列表的流, 而从这个流中我们应用 map() 将每个列表映射成对应该队列的长度的整数值. 最后, 我们使用 filter(x >= 2) 方法忽略掉所有的 1. 如上: 这 3 步操作将产生我们期望的流. 我们之后可以订阅(“侦听”)它, 并按我们希望的处理方式处理流中的数据.我希望你感受到了这种方式的美妙. 这个例子只是一次不过揭示了冰山一角: 你可以将相同的操作应用到不同种类的流上, 比如 API 返回的流中. 除此以外, 还有许多有效的函数.“为什么我应该采用反应式编程?“Reactive Programming (反应式编程) 提升了你代码的抽象层次, 你可以更多地关注用于定义业务逻辑的事件之间的互相依赖, 而不必写大量的细节代码来处理事件. RP(反应式编程)的代码会更简洁明了.在现代网页应用和移动应用中, 这种好处是显而易见的, 这些场景下, 与数据事件关联的大量 UI 事件需要被高频地交互. 10 年前, 和 web 页面的交互只是很基础地提交一个长长的表单给后端, 然后执行一次简单的重新渲染. 在这 10 年间, App 逐渐变得更有实时性: 修改表单中的单个字段能够自动触发一次到后端的保存动作, 对某个内容的 “点赞” 需要实时反馈到其他相关的用户……现今的 App 有大量的实时事件, 它们共同作用, 以带给用户良好的体验. 我们要能简洁处理这些事件的工具, 而 Reactive Programming 方式我们想要的.举例说明如何以反应式编程的方式思考现在我们进入到实战. 一个真实的手把手教你如何以 RP(反应式编程) 的方式来思考的例子. 注意这里不是随处抄来的例子, 不是半吊子解释的概念. 到这篇教程结束为止, 我们会在写出真正的功能性代码的同时, 理解我们做的每个动作.我选择了 JavaScript 和 RxJS 作为工具, 原因是, JavaScript 是当下最为人熟知的语言, 而 [Rx*] 支持多数语言和平台 (.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy 等等). , 无论你的工具是什么, 你可以从这篇教程中收益.实现一个"建议关注"盒子在 Twitter 上, 有一个 UI 元素是建议你可以关注的其它账户.我们将着重讲解如何模仿出它的核心特性:在页面启动时, 从 API 中加载账户数据, 并展示三个推荐关注者在点击"刷新"时, 加载另外的三个推荐关注的账户, 形成新三行在点击一个账户的 “x” 按钮时, 清除该账户并展示一个新的每一行显示账户的头像和到他们主页的链接我们可以忽略其它的特性和按钮, 它们都是次要的. 另外, Twitter 最近关闭了非认证请求接口, 作为替代, 我们使用 [Github 的 API] 来构建这个关注别人 UI.(注: 到本稿的最新的校正为止, github 的该接口对非认证用户启用了一段时间内访问频次限制)如果你想尽早看一下完整的代码, 请点击[样例代码].请求和回复你如何用 Rx 处理这个问题?首先, (几乎) 万物皆可为流 .这是 “Rx 口诀”. 让我们从最容易的特性开始: “在页面启动时, 从 API 中加载账户数据”. 这没什么难得, 只需要(1) 发一个请求, (2) 读取回复, (3) 渲染回复的中的数据. 所以我们直接把我们我们的请求当做流. 一开始就用流也许颇有"杀鸡焉用牛刀"的意味, 但为了理解, 我们需要从基本的例子开始.在应用启动的时候, 我们只需要一个请求, 因此如果我们将它作为一个数据流, 它将会只有一个派发的值. 我们知道之后我们将有更多的请求, 但刚开始时只有一个.–a——|->其中 a 是字符串 ‘https://api.github.com/users'这是一个将请求的 URL 的流. 无论请求何时发生, 它会告诉我们两件事: 请求发生的时刻和内容. 请求执行之时就是事件派发之时, 请求的内容就是被派发的值: 一个 URL 字符串.创建这样一个单值流对 [Rx*] 来说非常简单, 官方对于流的术语, 是 “Observable”(可被观察者), 顾名思义它是可被观察的, 但我觉得这名字有点傻, 所以我称呼它为 流.var requestStream = Rx.Observable.just(‘https://api.github.com/users');但现在, 这只是一个字符串流, 不包含其他操作, 所以我们需要要在值被派发的时候做一些事情. 这依靠对流的订阅.requestStream.subscribe(function(requestUrl) { // 执行该请求 jQuery.getJSON(requestUrl, function(responseData) { // … });}注意我们使用了 jQuery Ajax 回调(我们假定你应已对此有了解)来处理请求操作的异步性. 但稍等, Rx 就是处理 异步 数据流的. 难道这个请求的回复不就是一个在未来某一刻会带回返回数据的流么? 从概念上讲, 它看起来就是的, 我们来尝试写一下.requestStream.subscribe(function(requestUrl) { // 执行该请求 var responseStream = Rx.Observable.create(function (observer) { jQuery.getJSON(requestUrl) .done(function(response) { observer.onNext(response); }) .fail(function(jqXHR, status, error) { observer.onError(error); }) .always(function() { observer.onCompleted(); }); }); responseStream.subscribe(function(response) { // 对回复做一些处理 });}Rx.Observable.create() 所做的是自定义一个流, 这个流会通知其每个观察者(或者说其"订阅者” )有数据产生 (onNext()) 或发生了错误 (onError()). 我们需要做的仅仅是包装 jQuery Ajax Promise. 稍等, 这难道是说 Promise 也是一个 Observable?是的. Observable 就是一个 Promise++ 对象. 在 Rx 中, 通过运行 var stream = Rx.Observable.fromPromise(promise) 你就可以把一个 Promise 转换成一个 Observable. 仅有的区别在于 Observables 不符合 Promises/A+ 标准, 但他们在概念上是不冲突的. 一个 Promise 就是一个仅派发一个值的 Observable. Rx 流就是允许多次返回值的 Promise.这个例子很可以的, 它展示了 Observable 是如何至少有 Promise 的能力. 因此如果你喜欢 Promise, 请注意 Rx Observable 也可以做到同样的事.现在回到我们的例子上, 也许你已经注意到了, 我们在一个中 subscribe() 调用了另一个 subscribe(), 这有点像回调地狱. 另外, responseStream 的创建也依赖于 requestStream. 但正如前文所述, 在 Rx 中有简单的机制来最流作变换并支持从其他流创建一个新的流, 接下来我们来做这件事.到目前为止, 你应该知道的对流进行变换的一个基础方法是 map(f), 将 “流 A” 中的每一个元素作 f()处理, 然后在 “流 B” 中生成一一对应的值. 如果我们这样处理我们的请求和回复流, 我们可以把请求 URL 映射到回复的 Promise (被当做是流) 中.var responseMetastream = requestStream .map(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });这下我们创建了一个叫做 元流 (流的流) 的奇怪的东西. 不必对此感到疑惑, 元流, 就是其中派发值是流的流. 你可以把它想象成 指针): 每个被派发的值都是对其它另一个流的 指针 . 在我们的例子中, 每个请求的 URL 都被映射为一个指针, 指向一个个包含 URL 对应的返回数据的 promise 流.这个元流看上去有点让人迷惑, 而且对我们根本没什么用. 我们只是想要一个简单的回复流, 其中每个派发的值都应是一个 JSON 对象, 而不是一个包含 JSON 对象的 Promise. 现在来认识 Flatmap: 它类似于 map(), 但它是把 “分支” 流中派发出的的每一项值在 “主干” 流中派发出来, 如此, 它就可以对元流进行扁平化处理.(译者注: 这里, “分支” 流指的是元流中每个被派发的值, “主干” 流是指这些值有序构成的流, 由于元流中的每个值都是流, 作者不得不用 “主干” 和 “分支” 这样的比喻来描述元流与其值的关系). 在此, Flatmap 并不是起到了"修正"的作用, 元流也并不是一个 bug, 相反, 它们正是 Rx 中处理异步回复流的工具.var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });漂亮. 因为回复流是依据请求流定义的, 设想之后有更多的发生在请求流中的事件, 不难想象, 就会有对应的发生在回复流中的的回复事件:requestStream: –a—–b–c————|->responseStream: —–A——–B—–C—|->(小写的是一个请求, 大写的是一个回复)现在我们终于得到了回复流, 我们就可以渲染接收到的数据responseStream.subscribe(function(response) { // 按你设想的方式渲染 response 为 DOM});整理一下到目前为止的代码, 如下:var requestStream = Rx.Observable.just(‘https://api.github.com/users');var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });responseStream.subscribe(function(response) { // 按你设想的方式渲染 response 为 DOM});刷新按钮现在我们注意到, 回复中的 JSON 是一个包含 100 个用户的列表. [Github 的 API] 只允许我们指定一页的偏移量, 而不能指定读取的一页中的项目数量, 所以我们只用到 3 个数据对象, 剩下的 97 个只能浪费掉. 我们暂时忽略这个问题, 之后我们会看到通过缓存回复来处理它.每次刷新按钮被点击的时候, 请求流应该派发一个新的 URL, 因此我们会得到一个新的回复. 我们需要两样东西: 一个刷新按钮的点击事件流(口诀: 万物皆可成流), 并且我们需要改变请求流以依赖刷新点击流. 好在, RxJs 拥有从事件监听器产生 Observable 的工具.var refreshButton = document.querySelector(’.refresh’);var refreshClickStream = Rx.Observable.fromEvent(refreshButton, ‘click’);既然刷新点击事件自身不带任何 API URL, 我们需要映射每次点击为一个实际的 URL. 现在我们将请求流改成刷新点击流, 这个流被映射为每次带有随机的偏移参数的、到 API 的请求.var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });如果我直接这样写, 也不做自动化测试, 那这段代码其实有个特性没实现. 即请求不会在页面加载完时发生, 只有当刷新按钮被点击的时候才会. 但其实, 两种行为我们都需要: 刷新按钮被点击的时候的请求, 或者是页面刚打开时的请求.两种场景下需要不同的流:var requestOnRefreshStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });var startupRequestStream = Rx.Observable.just(‘https://api.github.com/users');但我们如何才能"合并"这两者为同一个呢? 有一个 merge() 方法. 用导图来解释的话, 它看起来像是这样的.stream A: —a——–e—–o—–>stream B: —–B—C—–D——–> vvvvvvvvv merge vvvvvvvvv —a-B—C–e–D–o—–>那我们要做的事就变得很容易了:var requestOnRefreshStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });var startupRequestStream = Rx.Observable.just(‘https://api.github.com/users');var requestStream = Rx.Observable.merge( requestOnRefreshStream, startupRequestStream);也有另外一种更干净的、不需要中间流的写法:var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; }) .merge(Rx.Observable.just(‘https://api.github.com/users'));甚至再短、再有可读性一点:var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; }) .startWith(‘https://api.github.com/users');startWith() 会照你猜的那样去工作: 给流一个起点. 无论你的输入流是怎样的, 带 startWith(x) 的输出流总会以 x 作为起点. 但我这样做还不够 [DRY], 我把 API 字符串写了两次. 一种修正的做法是把 startWith() 用在 refreshClickStream 上, 这样可以从"模拟"在页面加载时一次刷新点击事件.var requestStream = refreshClickStream.startWith(‘startup click’) .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });漂亮. 如果你现在回头去看我说 “有个特性没实现” 的那一段, 你应该能看出那里的代码和这里的代码的区别仅仅是多了一个 startWith().使用流来建立"3个推荐关注者"的模型到现在为止, 我们只是写完了一个发生在回复流的 subscribe() 中的 推荐关注者 的 UI. 对于刷新按钮, 我们要解决一个问题: 一旦你点击了"刷新”, 现在的三个推荐关注者仍然没有被清理. 新的推荐关注者只在请求内回复后才能拿到, 不过为了让 UI 看上去令人舒适, 我们需要在刷新按钮被点击的时候就清理当前的推荐关注者.refreshClickStream.subscribe(function() { // 清理 3 个推荐关注者的 DOM 元素});稍等一下. 这样做不太好, 因为这样我们就有两个会影响到推荐关注者的 DOM 元素的 subscriber (另一个是 responseStream.subscribe()), 这听起来不符合 Separation of concerns. 还记得 Reactive 口诀吗?在 “万物皆可为流” 的指导下, 我们把推荐关注者构建为一个流, 其中每个派发出来的值都是一个包含了推荐关注人数据的 JSON 对象. 我们会对三个推荐关注者的数据分别做这件事. 像这样来写:var suggestion1Stream = responseStream .map(function(listUsers) { // 从列表中随机获取一个用户 return listUsers[Math.floor(Math.random()*listUsers.length)]; });至于获取另外两个用户的流, 即 suggestion2Stream 和 suggestion3Stream, 只需要把 suggestion1Stream 复制一遍就行了. 这不够 [DRY], 不过对我们的教程而言, 这样能让我们的示例简单些, 同时我认为, 思考如何在这个场景下避免重复编写 suggestion[N]Stream 也是个好的思维练习, 就留给读者去考虑吧.我们让渲染的过程发生在回复流的 subscribe() 中, 而是这样做:suggestion1Stream.subscribe(function(suggestion) { // 渲染第 1 个推荐关注者});回想之前我们说的 “刷新的时候, 清理推荐关注者”, 我们可以简单地将刷新单击事件映射为 “null” 数据(它代表当前的推荐关注者为空), 并且在 suggestion1Stream 做这项工作, 如下:var suggestion1Stream = responseStream .map(function(listUsers) { // 从列表中随机获取一个用户 return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) );在渲染的时候, 我们把 null 解释为 “没有数据”, 隐藏它的 UI 元素.suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // 隐藏第 1 个推荐关注者元素 } else { // 显示第 1 个推荐关注者元素并渲染数据 }});整个情景是这样的:refreshClickStream: ———-o——–o—-> requestStream: -r——–r——–r—-> responseStream: —-R———R——R–> suggestion1Stream: —-s—–N—s—-N-s–> suggestion2Stream: —-q—–N—q—-N-q–> suggestion3Stream: —-t—–N—t—-N-t–>其中 N 表示 null(译者注: 注意, 当 refreshClickStream 产生新值, 即用户进行点击时, null 的产生总是立刻发生在 refreshClickStream 之后; 而 refreshClickStream => requestStream => responseStream, responseStream 中的值, 是发给 API 接口的异步请求的结果, 这个结果的产生往往会需要花一点时间, 必然在 null 之后, 因此可以达到 “为了让 UI 看上去令人舒适, 我们需要在刷新按钮被点击的时候就清理当前的推荐关注者” 的效果).稍微完善一下, 我们会在页面启动的时候也会渲染 “空” 推荐关注人. 为此可以 startWith(null) 放在推荐关注人的流里:var suggestion1Stream = responseStream .map(function(listUsers) { // 从列表中随机获取一个用户 return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);最后我们得到的流:refreshClickStream: ———-o———o—-> requestStream: -r——–r———r—-> responseStream: —-R———-R——R–> suggestion1Stream: -N–s—–N—-s—-N-s–> suggestion2Stream: -N–q—–N—-q—-N-q–> suggestion3Stream: -N–t—–N—-t—-N-t–>关闭推荐关注人, 并利用已缓存的回复数据目前还有一个特性没有实现. 每个推荐关注人格子应该有它自己的 ‘x’ 按钮来关闭它, 然后加载另一个数据来代替. 也许你的第一反应是, 用一种简单方法: 在点击关闭按钮的时候, 发起一个请求, 然后更新这个推荐人:var close1Button = document.querySelector(’.close1’);var close1ClickStream = Rx.Observable.fromEvent(close1Button, ‘click’);// close2Button 和 close3Button 重复此过程var requestStream = refreshClickStream.startWith(‘startup click’) .merge(close1ClickStream) // 把关闭按钮加在这里 .map(function() { var randomOffset = Math.floor(Math.random()500); return ‘https://api.github.com/users?since=' + randomOffset; });然而这没不对. (由于 refreshClickStream 影响了所有的推荐人流, 所以)该过程会关闭并且重新加载_所有的_推荐关注人, 而不是仅更新我们想关掉的那一个. 这里有很多方式来解决这个问题, 为了玩点炫酷的, 我们会重用之前的回复数据中别的推荐人. API 返回的数据每页包含 100 个用户, 但我们每次只用到其中的 3 个, 所以我们有很多有效的刷新数据可以用, 没必要再请求新的.再一次的, 让我们用流的思维来思考. 当一个 ‘close1’点击事件发生的时候, 我们使用 responseStream中 最近被派发的 回复来从回复的用户列表中随机获取一个用户. 如下: requestStream: –r—————> responseStream: ——R———–>close1ClickStream: ————c—–>suggestion1Stream: ——s—–s—–>在 [Rx] 中, 有一个合成器方法叫做 combineLatest, 似乎可以完成我们想做的事情. 它把两个流 A 和 B 作为其输入, 而当其中任何一个派发值的时候, combineLatest 会把两者最近派发的值 a 和 b 按照 c = f(x,y) 的方法合并处理再输出, 其中 f 是你可以定义的方法. 用图来解释也许更清楚:stream A: –a———–e——–i——–>stream B: —–b—-c——–d——-q—-> vvvvvvvv combineLatest(f) vvvvvvv —-AB—AC–EC—ED–ID–IQ—->在该例中, f 是一个转换为全大写的函数我们可以把 combineLatest() 用在 close1ClickStream 和 responseStream 上, 因此一旦 “关闭按钮1” 被点击(导致 close1ClickStream 产生新值), 我们都能得到最新的返回数据, 并在 suggestion1Stream中产生一个新的值. 由于 combineLatest() 的对称性的, 任何时候, 只要 responseStream 派发了一个新的回复, 它也将合并最新的一次 ‘关闭按钮1被点击’ 事件来产生一个新的推荐关注人. 这个特性非常有趣, 因为它允许我们简化我们之前的 suggestion1Stream , 如下:var suggestion1Stream = close1ClickStream .combineLatest(responseStream, function(click, listUsers) { return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);在上述思考中, 还有一点东西被遗漏. combineLatest() 使用了两个数据源中最近的数据, 但是如果这些源中的某个从未派发过任何东西, combineLatest() 就不能产生一个数据事件到输出流. 如果你再细看上面的 ASCII 图, 你会发现当第一个流派发 a 的时候, 不会有任何输出. 只有当第二个流派发 b 的时候才能产生一个输出值.有几种方式来解决该问题, 我们仍然采取最简单的一种, 就是在页面启动的时候模拟一次对 ‘关闭按钮1’ 按钮的点击:var suggestion1Stream = close1ClickStream.startWith(‘startup click’) // 把对"关闭按钮1"的点击的模拟加在这里 .combineLatest(responseStream, function(click, listUsers) {l return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);总结整理现在我们的工作完成了. 完整的代码如下所示:var refreshButton = document.querySelector(’.refresh’);var refreshClickStream = Rx.Observable.fromEvent(refreshButton, ‘click’);var closeButton1 = document.querySelector(’.close1’);var close1ClickStream = Rx.Observable.fromEvent(closeButton1, ‘click’);// close2 和 close3 是同样的逻辑var requestStream = refreshClickStream.startWith(‘startup click’) .map(function() { var randomOffset = Math.floor(Math.random()*500); return ‘https://api.github.com/users?since=' + randomOffset; });var responseStream = requestStream .flatMap(function (requestUrl) { return Rx.Observable.fromPromise($.ajax({url: requestUrl})); });var suggestion1Stream = close1ClickStream.startWith(‘startup click’) .combineLatest(responseStream, function(click, listUsers) { return listUsers[Math.floor(Math.random()listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);// suggestion2Stream 和 suggestion3Stream 是同样的逻辑suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // 隐藏第 1 个推荐关注者元素 } else { // 显示第 1 个推荐关注者元素并渲染数据 }});你可以在这里查看完整的[样例代码]很惭愧, 这只是一个微小的代码示例, 但它的信息量很大: 它着重表现了, 如何对关注点进行适当的隔离, 从而对不同流进行管理, 甚至充分利用了返回数据的流. 这样的函数式风格使得代码像声明式多于像命令式: 我们并不用给出一个要执行的的结构化序列, 我们只是通过定义流之间的关系来表达系统中每件事物是什么. 举例来说, 通过 Rx, 我们告诉计算机 suggestion1Stream 就是 点击关闭按钮1 的流, 与最近一个API返回的(用户中随机选择的一个)的用户的流, 刷新时产生 null 的流, 和应用启动时产生 null 的流的合并流.回想一下那些你熟稔的流程控制的语句(比如 if, for, while), 以及 Javascript 应用中随处可见的基于回调的控制流. (只要你愿意, )你甚至可以在上文的 subscribe() 中不写 if 和 else, 而是(在 observable 上)使用 filter()(这一块我就不写实现细节了, 留给你作为练习). 在 Rx 中, 有很多流处理方法, 比如 map, filter, scan, merge, combineLatest, startWith, 以及非常多用于控制一个事件驱动的程序的流的方法. 这个工具集让你用更少的代码而写出更强大的效果.接下来还有什么?如果你愿意用 [Rx] 来做反应式编程, 请花一些时间来熟悉这个 函数列表, 其中涉及如何变换, 合并和创建 Observables (被观察者). 如果你想以图形的方式理解这些方法, 可以看一下 弹珠图解 RxJava. 一旦你对理解某物有困难的时候, 试着画一画图, 基于图来思考, 看一下函数列表, 再继续思考. 以我的经验, 这样的学习流程非常有用.一旦你熟悉了如何使用 [Rx] 进行变成, 理解冷热酸甜, 想吃就吃…哦不, 冷热 Observables 就很有必要了. 反正就算你跳过了这一节, 你也会回来重新看的, 勿谓言之不预也. 建议通过学习真正的函数式编程来磨练你的技巧, 并且熟悉影响各种议题, 比如"影响 [Rx] 的副作用"什么的.不过, 实现了反应式编程的库并非并非只有 [Rx]. [Bacon.js] 的运行机制就很直观, 理解它不像理解 [Rx] 那么难; [Elm Language] 在特定的应用场景有很强的生命里: 它是一种会编译到 Javascript + HTML + CSS 的反应式编程语言, 它的特色在于 [time travelling debugger]. 这些都很不错.Rx 在严重依赖事件的前端应用中表现优秀. 但它不只是只为客户端应用服务的, 在接近数据库的后端场景中也大有可为. 实际上, [RxJava 正是激活 Netflex 服务端并发能力的关键]. Rx 不是一个严格限于某种特定类型应用的框架或者是语言. 它其实是一种范式, 你可以在任何事件驱动的软件中实践它.本文作者:richardo2016阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 6, 2018 · 8 min · jiezi

前端数据模型Model;适用于多人团队协作的开发模式

前言本文讲述的数据模型并不是一个库,也不是需要npm的包,仅仅只是一种在多人团队协作开发的时候拟定的规则。至少目前为止,我们的开发团队再也没用过mock(虽然一开始也没用),也不用担心后台数据的字段或结构发生变动,真正实现前后台并行开发的愉快模式。本文技术栈有 Typescript、Rxjs、AngularX定义Model类比于java里的类,我们的Model也是一个类,是TS的类,我们根据需求和设计图或原型图规划好某一个具体的模块的基类Model,并自行定义一些字段和枚举类型,方法属性等,并不需要强行和后台的字段一致,要保证百分百纯的前后端分离,举个例子比如开发某一个后台管理项目,里边有产品(Product)模块、用户(User)模块等那么我们会在model文件夹里定义BaseProduct的基类export class BaseProductModel { constructor() {} // 必有id 和 name public id: number = null; public name: string = ‘’; /…more…/}基类的定义是必要的,可以节省很多不必要的代码,并不需要写一个页面或组件就重新定义新的model,如果某一个组件里面需要对这个产品的内容进行拓展的大可直接继承,并不会影响其他有了这个基类的文件我们推崇一切基类都必须继承,不可直接构造真实的项目中产品的字段和属性肯定不止只有id和name,可能还包含版本、缩略图地址、唯一标识、产品、对应规格的价格、状态、创建时间等等;这些属性完全可以放在基类里,因为所有产品都有这些属性,说到类型和状态的定义,请注意绝对不能将可枚举性质的属性直接使用后台或第三方返回的对应属性比如,产品模块里最基础的状态(status)属性,假设后台定义的对应状态有0: 禁用1: 启用2: 隐藏3: 不可购买这四种,倘若我们在项目当中直接使用这些对应状态的数字去判断或进行逻辑处理,分不分的清另谈,如果中途或以后状态的数字变了,GG。可能大家觉得这样的情况很少,但也不是没有,一旦出现改起来BUG就一堆。所以对于这种可枚举性质的属性我们会定义一个枚举类(Enum)export enum EStatus { BAN = 0, OPEN = 1, HIDE = 2, NOTBUY = 3}然后在model里这样export class BaseProductModel { // …… public status: string = EStatus[1] // 默认启用}美滋滋,而且在进行逻辑判断的时候我们也不用去关心每个状态对应的数字是什么,我们只关心它是BAN还是OPEN,简洁明了不含糊而且我们还可以给model增加一个只读属性,用来返回这个状态对应的中文提示(这种需求很常见)public get conversionStatusHint() : string { const _ = { BAN: ‘禁用’, OPEN: ‘启用’, HIDE: ‘隐藏’, NOTBUY: ‘买不得呀’ } return _[this.status] ? _[this.status] : ‘’}这样就不用在每一个组件里面写一个方法来传参数返回中文名称了到了这里,我们的BaseProductModel已经算是定义好了,下面我们就需要给这个model定义一个方法目的是把后台返回的字段和数据结构转化为我们自己定义的字段和数据结构转化后台数据可能到了这里很多人会觉得这是多此一举,后台都直接返回数据了还转化什么,返回什么用什么就得了。但在大型的团队开发项目当中,谁也不能保证一个字段也不修改,一个字段也不删除或增加或缺失,牵一发动全身。人生苦短。而且还有一种情况就是,可能这个项目是前端先进行,后台还未介入,需要前端这边先把整体的功能和样式都先根据设计图规划开发。export class BaseProductModel { // …… // 转化后台数据 public setData( data: BaseProductModel ): void { if (data) { for (let e in this) { if ((<Object>data).hasOwnProperty(e)) { if( e == ‘status’ ) { this.status = EStatus[(<any>data)[e]] } else { this[e] = (<any>data)[e]; } } } } }}然后在调用的时候/** 假设ProductModel类继承了BaseProductModel类 /public productModel: ProductModel = new ProductModel();/…more…/this.productModel.setData(<BaseProductModel>{ // 假设后台定义的创建时间字段是create_at,model里定的创建时间是createTime createTime: data.create_at});// 即使数据结构不一致也可在这里进行统一转化做好了转化这一步,所有的数据变动和数据结构的变化都在这同一个地方修改即搞定,这个时候随便后台怎么改,欢乐改,都不影响我们后续的逻辑处理和字段的变动。同理,在post数据给后台的时候转化就显得容易多了,后台需要什么数据和字段再转化一次不就得了。以上的数据模型可以很好的降低前后台掐架的概率,mock?不需要下面是一个我们抽离出来的常用的表格数据模型基类import { BehaviorSubject } from ‘rxjs’//分页配置export interface PaginationConfig { // 当前的页码 pageIndex: number; // 总数 total: number; // 当前选中的一页显示多少个的数量 rows: number; // 可选择的每页显示多少个数量 rowsOptions?: Array<number>;}//分页配置初始数据export let PaginationInitConfig: PaginationConfig = { pageIndex: 1, total: 0, rows: 10, rowsOptions: [10, 20, 50]}//表格配置export interface TableConfig extends PaginationConfig { // 是否显示loading效果 isLoading?: boolean; // 是否处于半选状态 isCheckIndeterminate?: boolean; // 是否全选状态 isCheckAll?: boolean; // 是否禁用选中 isCheckDisable?: boolean; //没有数据的提示 noResult?: string;}//表头export interface TableHead { titles: string[]; widths?: string[]; //样式类 src/styles/ 中有公用的表格样式类 classes?: string[]; sorts?: (boolean | string)[];}//分页参数export interface PageParam { page: number; rows: number;}//排序类型export type orderType = ‘desc’ | ‘asc’ | null | ‘’//排序参数export interface SortParam { orderBy?: string; order?: orderType}// 所有表格的基类export class BaseTableModel<T> { //表格配置 tableConfig: TableConfig //表格头部配置 tableHead: TableHead //表格数据流 tableData$: BehaviorSubject<T[]> //排序类型 orderType: orderType //当前排序的标示 currentSortBy: string constructor( //选中的 key private checkKey: string = ‘isChecked’, //禁用的 key private disabledKey: string = ‘isDisabled’ ) { this.initData() } // 重置数据 public initData(): void { this.tableHead = { titles: [] } this.tableConfig = { pageIndex: 1, total: 0, rows: 10, rowsOptions: [10, 20, 50], isLoading: false, isCheckIndeterminate: false, isCheckAll: false, isCheckDisable: false, noResult: ‘暂无数据’ } this.tableData$ = new BehaviorSubject([]) } /* * 设置表格配置 * @author GR-05 * @param conf / setConfig(conf: TableConfig): void { this.tableConfig = Object.assign(this.tableConfig, conf) } /* * 设置表格头部标题 * @author GR-05 * @param titles / setHeadTitles(titles: string[]): void { this.tableHead.titles = titles } /* * 设置表格头部宽度 * @author GR-05 * @param widths / setHeadWidths(widths: string[]): void { this.tableHead.widths = widths } /* * 设置表格头部样式类 * @author GR-05 * @param classes / setHeadClasses(classes: string[]): void { this.tableHead.classes = classes } /* * 设置表格排序功能 * @author GR-05 * @param sorts / setHeadSorts(sorts: (boolean | string)[]): void { this.tableHead.sorts = sorts } /* * 设置当前排序类型 * @param ot / setSortType(ot: orderType) { this.orderType = ot } /* * 设置当前排序标识 * @param orderBy / setSortBy(orderBy: string) { this.currentSortBy = orderBy } /* * 设置当前被点击的排序标示 * @param i 排序数组索引 / sortByClick(i: number) { if (this.tableHead.sorts && this.tableHead.sorts[i]) { if (!this.orderType) { this.orderType = ‘desc’ } else { this.orderType == ‘desc’ ? this.orderType = ‘asc’ : this.orderType = ‘desc’ } this.currentSortBy = this.tableHead.sorts[i] as string } } /* * 获取当前的排序参数 / getCurrentSort(): SortParam { return { order: this.orderType, orderBy: this.currentSortBy } } /* * 设置表格loading * @author GR-05 * @param flag / setLoading(flag: boolean = true): void { this.tableConfig.isLoading = flag } /* * 设置当前表格数据总数 * @author GR-05 * @param total / setTotal(total: number): void { this.tableConfig.total = total } setPageAndRows(pageIndex: number, rows: number = 10) { this.tableConfig.pageIndex = pageIndex this.tableConfig.rows = rows } /* * 更新表格数据(新数据、单选、多选) * @author GR-05 * @param dataList / setDataList(dataList: T[]): void { this.tableConfig.isCheckAll = false this.tableConfig.isCheckIndeterminate = dataList.filter(item => !item[this.disabledKey]).some(item => item[this.checkKey] == true) this.tableConfig.isCheckAll = dataList.filter(item => !item[this.disabledKey]).every(item => item[this.checkKey] == true) this.tableConfig.isCheckAll ? this.tableConfig.isCheckIndeterminate = false : {} this.tableData$.next(dataList); if (dataList.length == 0) { this.tableConfig.isCheckAll = false } } /* * 获取已选的项 * @author GR-05 */ getCheckItem(): T[] { return this.tableData$.value.filter(item => item[this.checkKey] == true && !item[this.disabledKey]) }}我们为什么没有抽离成组件而是数据模型这么一个类上,主要是因为,组件的样式我们是不确定唯一性的,但数据和处理逻辑确是类似的,哪里地方要用到,就在哪个组件里new一个就好了;其中BaseTableModel后面的T可以是所有你想在表格上渲染的任何一个model类,比如之前的ProductModel,页面需求需要展示产品的表格列表,则export class TableModel extends BaseTableModel<ProductModel> { constructor() { super(); }}那么最后你只需要将BaseTableModel里的tableData$数据next成处理好的ProdcuModel数组就好了。 ...

October 29, 2018 · 3 min · jiezi

富交互Web应用中的撤销和前进

在web应用中,用户在进行一些富交互行为的操作时难免会出现误操作,比如在富文本编辑器设置错了字体颜色就需要撤回,做H5活动页面的时候不小心删了一个图片也需要撤回,更比如在线设计原型图应用的时候不小心删了一个页面等,总之在交互场景非常复杂的情况下,用户操作失误的可能性非常大,这时候‘撤销’和‘前进’这两个操作就很有必要了,而且用户体验也很好思路不管是任何场景下的web应用,用户的每一次操作我们都可以看成是对某个组件或某个对象的状态和属性进行改变,一旦连续的动作操作完成正准备进行下一个动作之前,此刻的状态就是一个全新的状态A —— B —— C用户未操作的时候全局状态是A用户操作某个组件使其移动到位置X,松开鼠标之后全局状态是B用户操作另一个组件使其删除,完成后全局状态是C所以,撤销的操作就是在用户操作状态到C的时候让全局的状态回到B,回到上一次操作完的时候。那么就需要可以存放这种大量状态的列表或索引来记录每一次操作的动作但如果我用某一个数组变量来存储如此庞大的数据是不是略显不妥?数据量越大内存应该会爆吧?所以这里我推荐大家使用IndexedDB下面是利用Angular、Rxjs和IndexedDB封装好的一个服务类import { Inject } from “@angular/core”;import { IndexedDBAngular } from “indexeddb-angular”;import { Subject, Observer, Observable } from “rxjs”;export interface IDBData { widgetList: string}// 前进和后退的服务@Inject({ providedIn: ‘root’})export class PanelExtendMoveBackService { /** * 发射DB集合存储的数据,可订阅 / public launchDBDataValue$: Subject<IDBData> = new Subject<IDBData>() /* * 创建一个叫panelDataDB的本地数据库,版本号为1 / public db = new IndexedDBAngular(‘panelDataDB’, 1) /* * 记录前进和后退的存储集合项的下标key * 默认为0 / public dbCurrentIndex: number = 0 /* * 自增的DBkey / public dbKey: number = -1 // 是否允许前进 public get isMove() : boolean { return this.dbCurrentIndex < this.dbKey } // 是否允许后退 public get isBack() : boolean { return this.dbCurrentIndex > 0 } constructor() {} /* * 创建DB集合 / public createCollections(): Observable<boolean> { const _sub: Subject<boolean> = new Subject<boolean>() this.dbKey = -1 this.db.createStore(1, (db: any) => { db.currentTarget.result.createObjectStore(‘panelItem’) }).then(()=>{ this.dbClear() _sub.next(true) }) return _sub.asObservable() } /* * 往集合里添加数据 * 同时把新添加的key赋值给dbCurrentIndex, / public dbAdd(): void { this.handleDbCurrentRefreshDB(); this.dbKey += 1; // 此处存储你要保存的数据 const _widget_list = [] this.db.add(‘panelItem’, { widgetList: JSON.stringify(_widget_list) }, this.dbKey).then( _e => { if ((<Object>_e).hasOwnProperty(‘key’)) { this.dbCurrentIndex = _e.key }; }, () => { this.dbKey -= 1 throw new Error(‘添加panelItem集合失败’) } ) } /* * 在执行添加数据集操作的时候判断dbCurrentIndex当前指引的下标是否低于dbKey * 如果是说明执行了后退操作之后后续动作执行了dbAdd的操作,则清空dbCurrentIndex索引之后的数据重新添加 / public handleDbCurrentRefreshDB(): void { if (this.dbCurrentIndex < this.dbKey) { for (let i = this.dbCurrentIndex + 1; i <= this.dbKey; i++) { this.db.delete(‘panelItem’, i).then(() => {}) } this.dbKey = this.dbCurrentIndex } } /* * 执行后退操作发射DB数据集 / public acquireBackDBData(): void { if( this.isBack ) { this.dbCurrentIndex -= 1 this.db.getByKey(‘panelItem’, this.dbCurrentIndex).then(res=>{ this.launchDBDataValue$.next(res) },()=>{ }) } } /* * 执行前进操作发射DB数据集 / public acquireMoveDBData(): void { if( this.isMove ) { this.dbCurrentIndex += 1 this.db.getByKey(‘panelItem’, this.dbCurrentIndex).then(res => { this.launchDBDataValue$.next(res) }, () => { }) } } /* * 清除DB集合panelItem */ public dbClear(): void { this.db.clear(‘panelItem’).then(_e => {}) }}这里我偷懒了一下,直接采用自增的id作为key了,也方便查找每一次操作所存储的数据如下最后可以看一下我实现好了的撤销和前进操作的场景 ...

October 19, 2018 · 2 min · jiezi

浅淡 RxJS WebSocket

引言中后台仪表盘是一个非常复杂,特别是当需要全面屏运用时,数据的实时性需求非常高。WebSocket 不管在什么环境中使用其实都是非常简单,各现代浏览器实现标准都很统一,而且接口也足够简单。即便是在 Angular 也是如此,只需要简单几行代码就能使用 WebSocket。const ws = new WebSocket(‘wss://echo.websocket.org’);ws.onmessage = (e) => { console.log(‘message’, e);}若需要向服务端发送消息,则:ws.send(content);在 Angular 里绝大多数的人都会根据上述代码进一步拓展,比如统一消息解析、错误处理、多路复用等,并最终将其封装成一个服务类。事实上,RxJS 也包裹了一个 WebSocket Subject,位于 rxjs/websocket。如何使用假如将上面的示例使用 RxJS 来写,则:import { webSocket, WebSocketSubject } from ‘rxjs/webSocket’;const ws = webSocket(‘wss://echo.websocket.org’);ws.subscribe(res => { console.log(‘message’, res);});ws.next(content);webSocket 是一个工厂函数,所生产出来的 WebSocketSubject 对象可被多次订阅,若未订阅或取消最后一个订阅时都会导致 WebSocket 连接中断,当再一次订阅时会重新自动连接。WebSocketSubjectConfigwebSocket 除了接收字符串(WebSocket服务远程地址)外,还允许指定更复杂的配置项。默认情况下,消息是使用 JSON.parse 和 JSON.stringify 对消息格式序列化和反序列化操作,所以不管消息发送或接收都以 JSON 为准,可通过 serializer、deserializer 属性来改变。若需要关心 WebSocket 什么时候开始或结束(closeObserver),则:const open$ = new Subject();const ws = webSocket({ url: ‘wss://echo.websocket.org’, openObserver: open$});// 订阅打开事件open$.subscribe(() => {});消息WebSocketSubject 也是 Subject 的变体之一,因此订阅它表示接收消息,反之则利用 next、complete、error 来维护消息的推送。使用 next 来发送消息使用 complete 会尝试检测是否最后一个订阅,若是将会关闭连接使用 error 相当于原始 close 方法且必须提供 { code: number, reason?: string} 参数,注意 code 务必遵守取值范围可被重放调用 next 发送消息时若 WebSocket 连接中断(例如:没人订阅时),消息会被缓存当下一次重新连接以后会按顺序发送。这对于异步世界里非常方便,我们只需要确保 Angular 启动前初始化好 WebSocket 不管什么时候订阅接收消息,都可以随时发送也无须等待。事实上这一点是 RxJS WebSocket 默认情况下是通过 webSocket 所生产的 WebSocketSubject 其本质上是 ReplaySubject 的“重放”能力。当然你可以通过 webSocket 的第二个参数改变这种行为。多路复用一般来说我们不太可能只会一个 Web Socket 服务完成所有的事,然而也不太可能针对每一个业务实例创建一个 webSocket。往往我们会增加一层网关并将这些业务 WebSocket 进行汇总,对于前端始终只需要一个连接,这就是多路复用存在的意义。而核心是必须要让后端知道,什么时候发送什么消息给什么样的服务。首先必须先使用 multiplex 方法来创建 Observable 以便订阅某一路消息,它有三个参数来帮助我们区分消息:subMsg 告知正在订阅哪一路消息unsubMsg 告知取消订阅哪一路消息messageFilter 过滤消息,使订阅者只接收哪一路消息const ws = webSocket(‘wss://echo.websocket.org’);const user$ = this.ws.multiplex( () => ({ type: ‘subscribe’, tag: ‘user’ }), () => ({ type: ‘unsubscribe’, tag: ‘user’ }), message => message.type === ‘user’);user$.subscribe(message => console.log(message));const todo$ = this.ws.multiplex( () => ({ type: ‘subscribe’, tag: ’todo’ }), () => ({ type: ‘unsubscribe’, tag: ’todo’ }), message => message.type === ’todo’);todo$.subscribe(message => console.log(message));user$ 流和 todo$ 流他们共用一个 WebSocket 连接,这便是多路复用。虽然订阅是通过 multiplex 创建的,然后消息的推送依然还是需要使用 ws.next()。总结这原本是对内部一个简单培训,然而我发现竟然极少人会讨论 RxJS 里面 Web Socket 的实现。其实一直有想着要给 ng-alain 内置 WebSocket,只是就封装角度来讲完全没有价值,因为已经足够优雅。 ...

September 24, 2018 · 1 min · jiezi