利用Angular2的Observables实现交互控制的方法


Posted in Javascript onDecember 27, 2018

在Angular1.x中,我们使用Promise来处理各种异步。但是在angular2中,使用的是Reactive Extensions (Rx)的Observable。对于Promise和Observable的区别,网上有很多文章,推荐egghead.io上的这个7分钟的视频(作者 Ben Lesh)。在这个视频的介绍中,主要说的,使用Observable创建的异步任务,可以被处理,而且是延时加载的。这篇文章里,我们主要针对一些在跟服务器端交互的时候遇到的问题,来看看Observable给我们带来的特性。

实例场景

首先,我们来定义一下问题的场景。假设我们要实现一个搜索功能,有一个简单的输入框,当用户输入文字的时候,实时的利用输入的文字进行查询,并显示查询的结果。

问题

在这个简单的场景当中,一般需要考虑3个问题:

不能在用户输入每个字符的时候就触发搜索。

如果用户输入每个字符就触发搜索,一来浪费服务器资源,二来客户端频繁触发搜索,以及更新搜索结果,也会影响客户端的响应。一般这个问题,都是通过加一些延时来避免。

如果用户输入的文本没有变化,就不应该重新搜索。

假设用户输入了'foo'以后,停顿了一会,触发了搜索,再敲了一个字符'o',结果发现打错了,又删掉了这个字符。如果这个时候用户又停顿一会,导致触发了搜索,这次的文本'foo'跟之前搜索的时候的文本是一样的,所以不应该再次搜索。

要考虑服务器的异步返回的问题。

当我们使用异步的方式往服务器端发送多个请求的时候,我们需要注意接受返回的顺序是无法保证的。比如我们先后搜索了2个单词'computer', ‘car', 虽然'car'这个词是后来搜的,但是有可能服务器处理这个搜索比较快,就先返回结果。这样页面就会先显示'car'的搜索结果,然后等收到'computer'的搜索结果的时候,再显示'computer'的结果。但是,这时候在用户看来明明搜索的是'car',却显示的是另外的结果。

迎接挑战

在这个实例中,我们使用wikipedia的api接口来开发一个简单的实例,实现简单的搜索功能。

实现搜索

由于只是演示,我们的app里面只包含2个文件: app.ts 和 wikipedia-service.ts,最终版本的源文件,请参考原文提供的demo链接。

我们直接来看最初版本的WikipediaService是如何实现的:

import { Injectable } from '@angular/core';
import { URLSearchParams, Jsonp } from '@angular/http';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
search (term: string) {
var search = new URLSearchParams()
search.set('action', 'opensearch');
search.set('search', term);
search.set('format', 'json');
return this.jsonp
.get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search })
.toPromise()
.then((response) => response.json()[1]);
}
}

在这里版本中,使用Jsonp模块来请求api结果,它的结果应该是一个类型为Observable<Response>的对象,我们把返回的结果从Observable<Response> 转换成 Promise<Response>对象,然后使用它的then方法把结果转成json。这样,这个search方法的返回类型为Promise<Array<string>>。

注意上面,我们使用response.json()[1]方式,从原先的结果中,得到我们需要的查询结果的列表,列表里面都是string。

这个看起来很简单,在angular1.x里面,也基本都是使用$http或$resource,来返回一个Promise类型的结果。

下面就是app.ts的部分内容(因为这只是演示,所以直接在app.ts里面直接定义module和component,并且调用service,在真实的app中,应该创建相应的组件来实现):

// check the plnkr for the full list of imports
import {...} from '...';
@Component({
selector: 'my-app',
template: `
<div>
<h2>Wikipedia Search</h2>
<input #term type="text" (keyup)="search(term.value)">
<ul>
<li *ngFor="let item of items">{{item}}</li>
</ul>
</div>
`
})
export class AppComponent {
items: Array<string>;
constructor(private wikipediaService: WikipediaService) {}
search(term) {
this.wikipediaService.search(term).then(items => this.items = items);
}
}

从上面的代码也能看出,AppComponent有一个search()方法,它调用wikipediaService.search()方法,因为这个方法返回一个Promise<Array<string>>类型的结果,所以使用then(),把结果列表赋值给model对象items。上面的template里面的模板内容就是用来以列表显示查询的结果。

虽然这个实现满足了基本的查询功能,但是对于上面提到的3个问题,都没有能够解决。下面就来修改这个实现来解决上面的问题。

控制用户输入延时

我们先解决第一个问题:当用户输入的时候,不要每次输入一个字符就触发一次搜索,而是设置一个时间延时,当用户停止输入的时间超过400毫秒,就触发搜索。如果用户一直不停的输入,输入的时间间隔小于400ms就不触发。这正是'Observables'能做的事情。

为此,我们需要一个Observable<string>对象来保存用户的输入,然后就可以用这个对象提供的方法来实现延时触发的功能。我们可以利用Angular2的指令(directive)formControl。要用这个指令,需要引入ReactiveFormsModule模块。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { JsonpModule } from '@angular/http';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, JsonpModule, ReactiveFormsModule]
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}

引入以后,我们就可以在模板里面使用FormControl来创建表单输入,并给他设置一个变量名term。

<input type="text" [formControl]="term"/>

这样,这个input组件所绑定的变量term就是FormControl的一个实例,它有一个属性valueChanges,这个属性是一个Observable<string>类型的对象。我们就可以使用Observable<string>的debounceTime方法来设置触发延时。

export class AppComponent {
items: Array<string>;
term = new FormControl();
constructor(private wikipediaService: WikipediaService) {
this.term.valueChanges
.debounceTime(400)
.subscribe(term => this.wikipediaService.search(term).then(items => this.items = items));
}
}

我们看到this.term.valueChanges是一个Observable<string>对象,通过debounceTime(400)我们设置它的事件触发延时是400毫秒。这个方法还是返回一个Observable<string>对象。然后我们就给这个对象添加一个订阅事件:

term => this.wikipediaService.search(term).then(items => this.items = items)

这是用lambda表达式写的一个方法。参数term就是Observable<string>对象经过400ms的延时设置,产生的一个用户输入的字符串。方法体就是用这个参数进行搜索,跟之前版本的处理方式一致。

在这个修改版中,我们把之前的search()方法去掉,直接在构造函数constructor(...)里面添加的,这相当于,用户在输入框的输入,是一个消息源,会经过debounceTime(400)的处理,然后产生一个消息,这个消息会发送给订阅的事件处理函数来处理,也就是搜索。所以,我们不需要一个search()方法来控制什么时候触发,而是通过类型订阅的机制来处理用户输入。

防止触发两次

现在我们再来解决第二个问题,就是经过400ms的延时以后,用户输入的搜索条件一样的情况。有了上面的Observable,这个就很简单了,Observable有一个distinctUntilChanged的方法,他会判断从消息源过来的新数据跟上次的数据是否一致,只有不一致才会触发订阅的方法。

this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.subscribe(term => this.wikipediaService.search(term).then(items => this.items = items));

处理返回顺序

上面描述了服务器端异步返回数据的时候,返回顺序不一致出现的问题。对于这个问题,我们的解决办法就比较直接,也就是对于之前的请求返回的结果,直接忽略,只处理在页面上用户最后一次发起的请求的结果。说道忽略之前的请求,如果你们看了上面的视频,或者知道Promise和Observable的区别的话,就应该想到我们可以利用Observable的dispose()方法来解决。实际上,我们是利用这种'disposable'特性来解决,而不是直接调用dispose()方法。(实在不知道该怎么翻译'disposable',它的意思是我可以中止在Observable对象上的消息处理,字面的意思是可被丢弃的、一次性的。)

上面我们讲到,在service的search()方法里,我们把Jsonp返回的结果从Observable<Response> 转换成 Promise<Response>对象。为了利用Observable的特性去丢弃上一个未及时返回的结果,我们让这个方法还是返回Observable类型的结果。下面就是修改后的WikipediaService里面的search()方法。

search (term: string) {
var search = new URLSearchParams()
search.set('action', 'opensearch');
search.set('search', term);
search.set('format', 'json');
return this.jsonp
.get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search })
.map((response) => response.json()[1]);
}

注意这个方法最后用.map((response) => response.json()[1]),意思是对于原先的Response类型的结果,转换成实际的搜索结果的列表。

map()以及后面要说到的flatMap()之类的方法,是函数式编程里面常用到的方法,意思就是将原先的数据集里面的每一条数据,经过一定的处理再返回一个新的结果,也就是把一个数据集转换成另一个数据集。

现在,我们的WikipediaSerice的返回结果就不是Promise了,所以我们就需要修改app.ts,我们不能再使用then()方法来处理结果,而是使用subscribe()添加一个消息订阅方法。

this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.subscribe(
term => this.wikipediaService.search(term).subscribe(
items => this.items = items
)
);

其中,第一个subscribe():

this.term.valueChanges...subscribe(term => ....)

这个是对输入框产生的查询字符串,注册一个订阅方法,来处理用户的输入。

第二个subscribe():

this.wikipediaService.search(term).subscribe(items => this.items = items));

是对从服务器端返回的数据查询结果,注册一个订阅方法,来将这个数据赋值到model上。

我们也可以用下面的方式,来避免这样使用多个subscribe:

this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.flatMap(term => this.wikipediaService.search(term))
.subscribe(items => this.items = items);

我们在用户输入的字符串的Observable<string>上调用flatMap(...)方法,相当于,对用户输入的每个有效的查询条件,调用wikipediaService.search()方法。然后对这个查询返回的数据,再注册一个订阅方法。

费了这么大的篇幅,希望你明白了Observable的flatMap和subscribe用法,对于没有接触过函数式编程的人来说,这确实不好理解,但是在Angular2里面,我们将会大量使用各种函数式编程的方法。所以还是需要你花时间慢慢理解。

费了这么大功夫,上面说的似乎跟'忽略之前未及时返回的消息'好像没什么关系,那么上面的修改到底有没有解决那个问题呢。没有!确实是没有。因为我们使用flatMap,对用户输入的每个有效的查询字符串,都会调用订阅的那个处理函数,然后更新model。所以我们的问题还是没有解决。

但是到了这一步以后,解决办法就很容易了,我们只需要用switchMap代理flatMap就可以。就这么简单!这是因为,switchMap会在处理每一个新的消息的时候,就直接把上一个消息注册的订阅方法直接取消掉。

最后,再优化一下代码:

@Component({
selector: 'my-app',
template: `
<div>
<h2>Wikipedia Search</h2>
<input type="text" [formControl]="term"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>
</div>
`
})
export class AppComponent {
items: Observable<Array<string>>;
term = new FormControl();
constructor(private wikipediaService: WikipediaService) {
this.items = this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.switchMap(term => this.wikipediaService.search(term));
}
}

我们直接把switchMap()的结果,赋给model对象this.items,也就是一个Observable<Array<string>>类型的数据。这样,在模板里面使用items的地方也需要修改,使用AsyncPipe就可以:

<li *ngFor="let item of items | async">{{item}}</li>

这样,模板在解析items这个model的时候,就会自动解析这个Observable的结果,再渲染页面。

Demo地址: Smart Wikipedia search using Angular 2

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript 面向对象封装与继承
Nov 27 Javascript
JavaScript中的eval()函数使用介绍
Dec 31 Javascript
jquery中JSON的解析方式
Mar 16 Javascript
javascript模拟php函数in_array
Apr 27 Javascript
jQuery的Scrollify插件实现滑动到页面下一节点
Jul 05 Javascript
解析JavaScript的ES6版本中的解构赋值
Jul 28 Javascript
Jquery 垂直多级手风琴菜单附源码下载
Nov 17 Javascript
jquery radio的取值_radio的选中_radio的重置方法
Sep 20 Javascript
javascript实现Emrips反质数枚举的示例代码
Dec 06 Javascript
基于element-ui的rules中正则表达式
Sep 04 Javascript
vue+iview/elementUi实现城市多选
Mar 28 Javascript
Vue.js页面中有多个input搜索框如何实现防抖操作
Nov 04 Javascript
angular 用Observable实现异步调用的方法
Dec 27 #Javascript
详解CommonJS和ES6模块循环加载处理的区别
Dec 26 #Javascript
vue-router beforeEach跳转路由验证用户登录状态
Dec 26 #Javascript
Vuerouter的beforeEach与afterEach钩子函数的区别
Dec 26 #Javascript
使用Sonarqube扫描Javascript代码的示例
Dec 26 #Javascript
angular6的table组件开发的实现示例
Dec 26 #Javascript
详解VUE里子组件如何获取父组件动态变化的值
Dec 26 #Javascript
You might like
用文本文件制作留言板提示(上)
2006/10/09 PHP
启用OPCache提高PHP程序性能的方法
2019/03/21 PHP
PHP数组对象与Json转换操作实例分析
2019/10/22 PHP
PHP实现计算器小功能
2020/08/28 PHP
Some tips of wmi scripting in jscript (1)
2007/04/03 Javascript
使用Grunt.js管理你项目的应用说明
2013/04/24 Javascript
利用window.name实现windowStorage代码分享
2014/01/02 Javascript
javascript原生和jquery库实现iframe自适应高度和宽度
2014/07/18 Javascript
jQuery操作DOM之获取表单控件的值
2015/01/23 Javascript
javascript实现漂亮的拖动层,窗口拖拽特效
2015/04/24 Javascript
使用Angular.js开发的注意事项
2016/10/19 Javascript
Vuejs 组件——props数据传递的实例代码
2017/03/07 Javascript
Javascript前端经典的面试题及答案
2017/03/14 Javascript
详谈javascript精度问题与调整
2017/07/08 Javascript
JS常见构造模式实例对比分析
2018/08/27 Javascript
图片文字识别(OCR)插件Ocrad.js教程
2018/11/26 Javascript
微信小程序实现获取小程序码和二维码java接口开发
2019/03/29 Javascript
vue实现直播间点赞飘心效果的示例代码
2019/09/20 Javascript
jQuery实现数字华容道小游戏(实例代码)
2020/01/16 jQuery
VUE+elementui组件在table-cell单元格中绘制微型echarts图
2020/04/20 Javascript
[13:18]《一刀刀一天》之DOTA全时刻21:详解TI新赛制 A队再露獠牙
2014/06/24 DOTA
[43:41]VP vs RNG 2019国际邀请赛淘汰赛 败者组 BO3 第二场 8.21.mp4
2020/07/19 DOTA
新手该如何学python怎么学好python?
2008/10/07 Python
pycharm 使用心得(九)解决No Python interpreter selected的问题
2014/06/06 Python
Python进阶学习之特殊方法实例详析
2017/12/01 Python
python list格式数据excel导出方法
2018/10/31 Python
PyQT实现菜单中的复制,全选和清空的功能的方法
2019/06/17 Python
html5 Canvas画图教程(11)—使用lineTo/arc/bezierCurveTo画椭圆形
2013/01/09 HTML / CSS
什么是"引用"?申明和使用"引用"要注意哪些问题?
2016/03/03 面试题
工厂厂长的职责
2013/12/12 职场文书
小学语文教学经验交流材料
2014/06/02 职场文书
啦啦队口号大全
2014/06/16 职场文书
乡镇创先争优活动总结
2014/08/28 职场文书
党的群众路线教育实践活动对照检查材料(个人)
2014/09/24 职场文书
导游词之宁夏贺兰山岩画
2019/11/08 职场文书
springboot + mongodb 通过经纬度坐标匹配平面区域的方法
2021/11/01 MongoDB