Angular18 RXJS
1 RX
全称是 Reactive Extensions,它是微软开发并维护的基于 Reactive Programming 范式实现的一套工具库集合;RX结合了观察者模式、迭代器模式、函数式编程来管理事件序列
RX官方文档:点击前往
2 RXJS
RXJS就是RX在JavaScript层面上的实现;RxJS 是一个库,它通过使用 observable 序列来编写异步和基于事件的程序。它提供了一个核心类型 Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 [Array#extras] 启发的操作符 (map、filter、reduce、every, 等等),这些数组操作符可以把异步事件作为集合来处理。
RXJS官方文档:点击前往
3 RXJS中解决异步事件管理的一些基本概念
3.1 Observable
可观察对象:表示一个可调用的未来值或者事件的集合
官方文档:点击前往
3.2 Observer
观察者对象:一个回调函数集合,它知道怎么去监听被可观察对象Observable发送的值
官方文档:点击前往
3.3 Subscription
订阅:表示一个可观察对象Observable的执行,主要用于取消可观察对象Observable执行
官方文档:点击前往
3.4 Operators
操作符:就是一些义函数式编程来处理可观察对象Observable,使用像 map
、filter
、concat
、flatMap
等这样的操作符来处理集合。
官方文档:点击前往
3.5 Subject
相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
官方文档:点击前往
3.6 Schedulers
用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeout
或 requestAnimationFrame
或其他。
官方文档:点击前往
4 简单实例
技巧01:利用 jsbin 这个在线的JS编辑器作为测试编辑器,连接地址 -> 点击前往
技巧02:本博文使用JS版本都是ES6
4.1 单击按钮实例
4.1.1 在HTML中通过script标签引入RXJS库
<script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script>
4.1.2 在HTML中添加一个button标签
<button id="btn" >单击查看效果</button>
4.1.2 通过JS实现RXJS编程
const btn = document.getElementById('btn');
const btn$ = Rx
.Observable
.fromEvent(btn, 'click');
btn$.subscribe(value => console.log('你点击了按钮哟'));
代码解释:
第一行:获取ID为“btn”的DOM节点
第二行:将获取到的DOM节点的单机事件添加到一个可观察对象Observable的序列中
第三行:订阅可观察对象Observable,当对应DOM节点的单机事件被触发时就会执行相应的操作(本实例是打印出一些信息)
4.1.3 效果展示
4.1.4 代码汇总
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body> <button id="btn" >单击查看效果</button> <script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script>
</body>
</html>
HTML
const btn = document.getElementById('btn');
const btn$ = Rx
.Observable
.fromEvent(btn, 'click');
btn$.subscribe(value => console.log('你点击了按钮哟'));
ES6
4.2 文本框输入实例
4.2.1 在HTML中通过script标签引入RXJS库
<script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script>
4.2.2 在HTML中添加一个input标签
<input id="test" type="text" placeholder="请输入一些信息" />
4.2.3 通过JS实现RXJS编程
const test = document
.getElementById('test');
const test$ = Rx
.Observable
.fromEvent(test, 'keyup')
.debounceTime(500)
.pluck('target', 'value');
test$
.subscribe(value => console.log(value));
代码解释:
fromEvent -> 将DOM节点的keyup事件添加到可观察对象Observable中
debounceTime -> DOM节点的keyup事件触发后0.5秒后才推送
pluck -> 获取触发事件DOM节点的value值
4.2.4 效果展示
4.2.5 代码汇总
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body> <input id="test" type="text" placeholder="请输入一些信息" /> <script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script>
</body>
</html>
HTML
const test = document
.getElementById('test');
const test$ = Rx
.Observable
.fromEvent(test, 'keyup')
.debounceTime(500)
.pluck('target', 'value');
test$
.subscribe(value => console.log(value));
ES6
5 弹珠图
要解释操作符是如何工作的,文字描述通常是不足以描述清楚的。许多操作符都是跟时间相关的,它们可能会以不同的方式延迟(delay)、取样(sample)、节流(throttle)或去抖动值(debonce)。图表通常是更适合的工具。弹珠图是操作符运行方式的视觉表示,其中包含输入 Obserable(s) (输入可能是多个 Observable )、操作符及其参数和输出 Observable 。
6 可观察对象Observable
技巧01:可观察对象Observable是数据的产生者,它会将数据以流的方式推送给观察者Observer;可以将可观察对象Observable看作是一个可以同步、异步返回多个值的函数
const test$ = Rx.Observable
.create(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
setTimeout(() => observer.next('一秒钟后异步推送的数据'), 1000);
setTimeout(() => {
observer.next('2秒钟后异步推送的数据');
observer.complete();
}, 2000);
});
代码解释:上面的代码中利用create操作符创建了一个Observable,该Observable一旦被订阅就会向Observer推送1、2、3,然后1秒钟后推送“一秒钟后异步推送的数据”,再过1秒钟后推送“2秒钟后异步推送的数据”,紧接着是完成数据流的推送
技巧02:订阅Observable是Observable执行和发送值/事件给Observer最简单的方式;订阅Observable就类似于调用一个拥有多个返回值的函数
const test$Sub = test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('数据推送完成')
);
代码解释:上面的代码是订阅Observable的一个实例test$,订阅方法subscribe中的参数是有顺序的,其中第一个参数是位置参数用于处理数据,第二三个参数是可选参数,其中第二个参数用来处理错误,第三个参数用来处理数据推送完成后的一些操作;订阅后会返回一个Subscription对象,可以通过该对象来关闭正在执行的Observable
技巧03:利用Subscription对象来关闭正在执行的Observable
const test$ = Rx.Observable
.create(observer => {
// const t = setInterval(() => observer.next('hello'), 1000);
observer.next(1);
observer.next(2);
observer.next(3);
const st01 = setTimeout(() => observer.next('一秒钟后异步推送的数据'), 1000);
const st02 = setTimeout(() => {
observer.next('2秒钟后异步推送的数据');
observer.complete();
}, 2000); return function unsubscribe() {
clearTimeout(st01);
clearTimeout(st02);
// clearInterval(t);
}
});
const test$Sub = test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('数据推送完成')
);
setInterval(() => test$Sub.unsubscribe(), 1000);
// test$Sub.unsubscribe();
代码解释:使用create创建Observable的时候回返回一个函数unsubscribe,该函数用来定义如何清理执行的资源;订阅该Observable后就会返回一个Subscription对象,通过该Subscription对象来调用unsubscribe方法来取消正在执行的Observable
7 观察者Observer
Observer是Observable推送的值的消费者;Observer是一组回调函数的集合,每个回调函数对应一种 Observable 发送的通知类型:next
、error
和 complete。下面的示例是一个典型的观察者对象:
var observer = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
技巧01:使用Observer就必须将他们提供给Observable的subscirbe方法
坑01:虽然所Observer是一个对象,但是在将Observer传给Observable的subscribe方法时只需要将对象中的内容传进去就可以啦,例如下面第一种方式就是错误的,第二种方式才是正确的
test$.subscribe(
{
value => console.log(value),
error => console.log(error),
() => console.log('数据推送完成')
}
);
错误的使用方法
test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('数据推送完成')
);
8 操作符
操作符是 Observable 类型上的方法,比如 .map(...)
、.filter(...)
、.merge(...)
,等等。当操作符被调用时,它们不会改变已经存在的 Observable 实例。相反,它们返回一个新的 Observable ,它的 subscription 逻辑基于第一个 Observable。(操作符是函数,它基于当前的 Observable 创建一个新的 Observable。这是一个无副作用的操作:前面的 Observable 保持不变)
操作符本质上是一个纯函数 (pure function),它接收一个 Observable 作为输入,并生成一个新的 Observable 作为输出。订阅输出 Observalbe 同样会订阅输入 Observable 。
8.1 小案例之mapTo
每次源 Observble 发出值时,都在输出 Observable 上发出给定的常量值;接收常量 value
作为参数,并每当源 Observable 发出值时都发出这个值。换句话说, 就是忽略实际的源值,然后简单地使用这个发送时间点以知道何时发出给定的 value
。
8.1.1 效果展示
8.1.2 代码汇总
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button id="btn" type="button">点击测试</button>
<script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script>
</body>
</html>
HTML
// 根据ID获取DOM对象
const btn = document.getElementById("btn"); // 将对应DOM对象的单击事件变成事件流
const btn$ = Rx.Observable
.fromEvent(btn, "click")
.mapTo('ello'); // 订阅事件流
btn$.subscribe(value => console.log(value));
ES6
8.2 创建类操作符
8.2.1 interval
public static interval(period: number, scheduler: Scheduler): Observable
创建一个 Observable ,该 Observable 使用指定的 IScheduler ,并以指定时间间隔发出连续的数字。
坑01:interval产生的第一个数并不是立即就产生的,而是当第一个时间间隔过后才会推送第一个数字,而且推送的数字是从0开始的自增数字
const test$ = Rx.Observable
.interval(1000);
test$.subscribe(value => console.log(value));
技巧01:官方文档 => 点击前往
8.2.2 timer
public static timer(initialDelay: number | Date, period: number, scheduler: Scheduler): Observable
根据给定的时间间隔自增的产生数字
参数解释:
参数01:产生第一个数字的间隔时间
参数02:产生下一个数字的时间间隔(可选)
参数03:调度器,用来调度值的发送, 提供“时间”的概念 (可选,默认值: async)
坑01:如果只给定第一个参数,那么会在时间间隔到达后推送数字0,然后就不在继续推送数据啦
技巧01:其实timer的用法和interval非常想,都是自动产生自动的数字
const test$ = Rx.Observable
.timer(1000, 1000)
.take(3)
.map(value => value * 10);
test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.3 合并类操作符
8.3.1 combineLatest
组合多个 Observables 来创建一个 Observable ,该 Observable 的值根据每个输入 Observable 的最新值计算得出的;组合多个 Observables 来创建一个 Observable ,该 Observable 的值根据每个输入 Observable 的最新值计算得出的。
技巧01:每个Observables都必须有值,而且他们其中必须有一个的值更新后才会执行combineLatest
》效果展示
》代码汇总
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<input type="number" id="weight" placeholder="请输入体重" />
<span>Kg</span>
<br />
<input type="number" id="height" placeholder="请输入身高" />
<span>cm</span> <script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js"></script>
</body>
</html>
HTML
const weight = document.getElementById('weight');
const height = document.getElementById('height'); const weight$ = Rx.Observable
.fromEvent(weight, 'keyup')
.debounceTime(500)
.pluck('target', 'value');
const height$ = Rx.Observable
.fromEvent(height, 'keyup')
.debounceTime(500)
.pluck('target', 'value'); const bmi$ = Rx.Observable
.combineLatest(
weight$,
height$,
(w, h) => {
const str = '成人BMI数值为:';
const bmi = w / (h * h / 100 / 100);
if (bmi <.5) {
return str + bmi + ' -> 过轻';
} else {
if (bmi <.9) {
return str + bmi + ' -> 正常';
} else {
if (bmi <) {
return str + bmi + ' -> 过重';
} else {
if (bmi <) {
return str + bmi + ' -> 肥胖';
} else {
return str + bmi + ' -> 非常肥胖';
}
}
}
} }
); // 过轻:低于18.5
// 正常:18.5-23.9
// 过重:24-27
// 肥胖:28-32
// 非常肥胖, 高于32
// weight$.subscribe(value => console.log(value));
// height$.subscribe(value => console.log(value));
bmi$.subscribe(value => console.log(value));
ES6
8.3.2 merge
public merge(other: ObservableInput, concurrent: number, scheduler: Scheduler): Observable
将两个数据源的数据合并到一起
const test01$ = Rx.Observable
.create(observer => {
observer.next(1);
setTimeout(() => {
observer.next(2);
}, 1000);
observer.next(3);
});
const test02$ = Rx.Observable
.create(observer => {
observer.next(4);
observer.next(5);
observer.next(6);
});
const test$ = Rx.Observable
.merge(test01$, test02$); test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
8.3.3 startWith
public startWith(values: ...T, scheduler: Scheduler): Observable
向原数据源添加一个数据作为第一个推送的数据
const test01$ = Rx.Observable
.create(observer => {
observer.next(1);
setTimeout(() => {
observer.next(2);
}, 1000);
observer.next(3);
})
.startWith('HELLO'); test01$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
8.3.4 zip
根据顺序将两个数据源中的数据进行组合
技巧01:如果一个数据源中有3个数据,另外一个数据源中有4个数据,那么只会将这两个数据源中前面推送的3个数据进行组合
const test01$ = Rx.Observable
.create(observer => {
observer.next(1);
setTimeout(() => {
observer.next(2);
}, 1000);
observer.next(3);
});
const test02$ = Rx.Observable
.create(observer => {
observer.next('a');
observer.next('b');
observer.next('c');
observer.next('d');
}); const test$ = Rx.Observable
.zip(test01$, test02$); test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4 其他操作符
8.4.1 take
public take(count: number): Observable<T>
只会推送原数据流中前面的count个数据
const test$ = Rx.Observable
.interval(1000)
.take(3);
test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
8.4.2 map
public map(project: function(value: T, index: number): R, thisArg: any): Observable<R>
对原数据源的每个值做同样的处理
const test$ = Rx.Observable
.interval(1000)
.take(3)
.map(value => value * 10);
test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.3 filter
public filter(predicate: function(value: T, index: number): boolean, thisArg: any): Observable
对原数据源的数据进行过滤
参数解释:
参数01:过滤操作函数,操作函数中第一个参数代表正在过滤的数据,第二个参数代表原数据源序列的索引(注意:索引是从0开始的,用处还问弄清楚)???
参数02:可选参数,用来决定 predicate
函数中的 this
的值
const test$ = Rx.Observable
.timer(1000, 1000)
.take(5)
.map(value => value * 10)
.filter((value) => value > 10);
test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.4 first
public first(predicate: function(value: T, index: number, source: Observable<T>): boolean, resultSelector: function(value: T, index: number): R, defaultValue: R): Observable<T | R>
只推送原数据源中的第一个值或者第一个满足条件的值
技巧01:如果first不携带任何参数,那么将会推送原数据源中的第一个值(和take(1)的效果相同);如果携带一个返回boolean的方法,那么将返回满足该方法的第一个数据
const test$ = Rx.Observable
.timer(1000, 1000)
.take(5)
.map(value => value * 10)
// .first();
.first((value) => value > 10);
test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.5 last
public last(predicate: function): Observable
只推送原数据源中的最后一个数据
技巧01:如果last不指定参数时,就会默认只推送最后一个数据;如果指定了一个返回boolean类型的方法作为参数,那么就会返回最后一个满足条件的数据
const test$ = Rx.Observable
.interval(1000)
.take(5)
.last();
// .last(value => value < 3 ); test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.6 skip
public skip(count: Number): Observable
在推送数据时,跳过前面count个数据
const test$ = Rx.Observable
.interval(1000)
.take(5)
.skip(2); test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.7 scan
public scan(accumulator: function(acc: R, value: T, index: number): R, seed: T | R): Observable<R>
对原数据源序列进行依次累加,每累加玩完一次就会推送一个结果
参数解释:
参数01:一个累加方法,该方法的第一个参数代表累加结果,第二个参数代表当前累加值,该累加值随便用一个变量表示即可
参数02:可选参数,累加器的初始值,如果不填就会默认将原数据源的第一个数据作为初始累加值
const b = 100;
const test01$ = Rx.Observable
.interval(1000)
.take(5)
.scan((acc, a) => acc + a, b); test01$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.8 reduce
public reduce(accumulator: function(acc: R, value: T, index: number): R, seed: R): Observable<R>
对原数据源中的数据进行累加,最后只将累加的结果推送
参数解释:
参数01:第一个参数是一个累加方法,该累加方法接受两个参数,第一个参数代表累加结果,第二个参数代表当前累加值
参数02:可选参数,该参数如果指定了就会被用作累加的初始值
const b = 100;
const test01$ = Rx.Observable
.interval(1000)
.take(5)
.reduce((acc, a) => acc + a, b); test01$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.9 debounceTime
public debounceTime(dueTime: number, scheduler: Scheduler): Observable
当没有新的数据添加到原数据源时,延迟发送原数据源中的数据
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body> <input type='text' id="test" placeholder="测试文本框" /> <script src="https://unpkg.com/@reactivex/rxjs@5.0.3/dist/global/Rx.js">
</script>
</body>
</html>
HTML
const test = document.getElementById('test');
const test$ = Rx.Observable
.fromEvent(test, 'keyup')
.debounceTime(500)
.pluck('target', 'value'); test$.subscribe(
value => console.log(value),
error => console.log(error),
() => console.log('完成')
);
ES6
8.4.10 distinct
public distinct(keySelector: function, flushes: Observable): Observable
去除掉原数据源中重复的数据
技巧01:如果distinct方法提供了keySelector方法,就会先利用keySelector方法对原数据源你的数据进行处理后在进行去重操作
官方文档 => 点击前往
const test01$ = Rx.Observable
.create(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.next(1);
observer.next(2);
observer.next(3);
})
.distinct();
test01$.subscribe(value => console.log(value));
ES6
8.4.11 distinctUntilChanged
public distinctUntilChanged(compare: function): Observable
去除掉原数据源相邻数据的重复数据
const test01$ = Rx.Observable
.create(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.next(3);
observer.next(3);
observer.next(1);
observer.next(2);
observer.next(3);
})
.distinctUntilChanged();
test01$.subscribe(value => console.log(value));
ES6
Angular18 RXJS的更多相关文章
- RxJS + Redux + React = Amazing!(译一)
今天,我将Youtube上的<RxJS + Redux + React = Amazing!>翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: https:/ ...
- RxJS + Redux + React = Amazing!(译二)
今天,我将Youtube上的<RxJS + Redux + React = Amazing!>的后半部分翻译(+机译)了下来,以供国内的同学学习,英文听力好的同学可以直接看原版视频: ht ...
- [译]RxJS 5.X基础篇
欢迎指错与讨论 : ) 当前RxJS版本:5.0.0-beta.10.更详细的内容尽在RxJS官网http://reactivex.io/rxjs/manual/overview.html.文章比较长 ...
- 学习RxJS: 导入
原文地址:http://www.moye.me/2016/05/31/learning_rxjs_part_one_preliminary/ 引子 新手们在异步编程里跌倒时,永远会有这么一个经典问题: ...
- [Angular2 Form] Use RxJS Streams with Angular 2 Forms
Angular 2 forms provide RxJS streams for you to work with the data and validity as it flows out of t ...
- [RxJS] Introduction to RxJS Marble Testing
Marble testing is an expressive way to test observables by utilizing marble diagrams. This lesson wi ...
- [rxjs] Throttled Buffering in RxJS (debounce)
Capturing every event can get chatty. Batching events with a throttled buffer in RxJS lets you captu ...
- [Javascript + rxjs] Simple drag and drop with Observables
Armed with the map and concatAll functions, we can create fairly complex interactions in a simple wa ...
- [Javascript + rxjs] Introducing the Observable
In this lesson we will get introduced to the Observable type. An Observable is a collection that arr ...
随机推荐
- MySQL 基础知识(基本架构、存储引擎差异)
前言: // MySQL 并发.异步IO.进程劫持 最近在看高性能 MySQL,记录写学习笔记: 高性能 MySQL 学习笔记(一) 架构与历史 笔记核心内容:MySQL 服务器基础架构.各种存储引擎 ...
- HTTP通道
通过httptunnel技术进行入侵示例 httptunnel是通过HTTP通道来传输其他协议数据的工具软件,下载地址为:www.http-tunnel. com,目前最新版本3.0.5 工具/原料 ...
- nyoj-3-多边形重心问题(求多边形面积和中心)
题目链接 /* Name:nyoj-3-多边形重心问题 Copyright: Author: Date: 2018/4/26 21:25:41 Description: ACM国际大学生程序设计竞赛 ...
- Gym - 100623J Just Too Lucky (数位dp)
给定n∈[1,1e12],求1到n的所有整数中,各位数字之和能整除它本身的数的个数. 这道题与UVA-11361类似,假如设dp[u][lim][m1][m2]为枚举到第u位(从低到高数),是否受限, ...
- [SPOJ10707]Count on a tree II
luogu 题意 给定一个n个节点的树,每个节点表示一个整数,问u到v的路径上有多少个不同的整数. sol 也就是路径数颜色.树上莫队板子题. 我这种分块的姿势貌似是假的. 所以跑的是最慢的QAQ. ...
- 【LeetCode】673. Number of Longest Increasing Subsequence
题目: Given an unsorted array of integers, find the number of longest increasing subsequence. Example ...
- CentOS7.2 Jenkins部署
1.安装配置java环境 直接下载二进制安装包: # tar xvf jdk-8u111-linux-x64.tar.gz -C /usr/local/src/ # ln -sv /usr/local ...
- MySQL 使用pt-table-checksum 检查主从数据一致性 (实例转)
1.基本环境: Mysql版本:5.6.12-log Percona-toolkit:2.2.18 Linux:centos6.5 2.安装 源码安装: # 一些依赖包 yum install per ...
- jQuery.extend()方法
定义和用法 jQuery.extend()函数用于将一个或多个对象的内容合并到目标对象. 注意: 1. 如果只为$.extend()指定了一个参数,则意味着参数target被省略.此时,target就 ...
- 用phpinfo( )打印出来的php版本和在服务器上用php -v打印出来的版本不同的原因
php -v 是linux系统的php版本,而phpinfo里显示的是WEB Server中配置的版本.说简单点,你的系统中有两个php版本. 如果您阅读过此文章有所收获,请为我顶一个,如果文章中有错 ...