iOS开发-ReactiveCocoa(RAC)框架

前言

本文是根据DeveloperLx在斗鱼直播ReactiveCocoa时整理的文章,同时加上本人的见解,若有错误希望指出。
感谢 DeveloperLx 为大家做出的贡献,虽然直播声音小了点,但满满的都是干货 ^v^

简介

神马是RAC?ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾。借用RayWenderlich上面的话:

As an iOS developer, nearly every line of code you write is in reaction to some event; a button tap, a received network message, a property change (via Key Value Observing) or a change in user’s location via CoreLocation are all good examples. However, these events are all encoded in different ways; as actions, delegates, KVO, callbacks and others. ReactiveCocoa defines a standard interface for events, so they can be more easily chained, filtered and composed using a basic set of tools.

翻译过来就是:

作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。

RAC是由 Mattt Thompson 大神开发的,很多开发者对其的评价是开启一个新Objective-C纪元,可见对其评价有多高。
以下是RAC的Github主页:ReactiveCocoa
以及官方给出的用法链接

安装

ReactiveCocoa安装教程我就不说了,用pod安装即可。
本文的Demo可在文章最后下载,在阅读本文的时候,强烈推荐边看Demo边看博文。
项目中加入了ReactiveCocoa 和 DeveloperLx 大神的打印插件 LxDBAnything
同时加入了键盘相应的第三方IQKeyboardManager,然而没有怎么用到。
以及Masonry,使用方法可以看这篇文章,iOS - Masonry自动布局(Autolayout)

撸代码

第一部分 简单使用

文本框事件

原来我们在使用textFiled的时候我们需要写到

1
[textField addTarget:self action:@selector(textChanged:) forControlEvents:UIControlEventEditingChanged];

然后实现textChanged:方法,在RAC中,对于文本框的监听,是非常简单的一件事情,看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
UITextField * textField = ({
UITextField * textField = [[UITextField alloc]init];
textField.backgroundColor = [UIColor cyanColor];

textField;
});
[self.view addSubview:textField];

@weakify(self); // __weak __typeof__(self) self_weak_ = self;

[textField mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self); // __strong __typeof__(self) self = self_weak_;
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];

[[textField rac_signalForControlEvents:UIControlEventEditingChanged]
subscribeNext:^(id x) {

LxDBAnyVar(x);
}];
[textField.rac_textSignal subscribeNext:^(NSString *x) {

LxDBAnyVar(x);
}];

打印结果:

1
2
3
4
5
6
7
8
9
📍__31-[ViewController textFiledTest]_block_invoke_2 + 215🎈 x = 12
📍__31-[ViewController textFiledTest]_block_invoke241 + 211🎈 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '123'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
📍__31-[ViewController textFiledTest]_block_invoke_2 + 215🎈 x = 123
📍__31-[ViewController textFiledTest]_block_invoke241 + 211🎈 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '1231'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
📍__31-[ViewController textFiledTest]_block_invoke_2 + 215🎈 x = 1231
📍__31-[ViewController textFiledTest]_block_invoke241 + 211🎈 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '12312'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
📍__31-[ViewController textFiledTest]_block_invoke_2 + 215🎈 x = 12312
📍__31-[ViewController textFiledTest]_block_invoke241 + 211🎈 x = <UITextField: 0x7fe810c51a90; frame = (97.5 313.5; 180 40); text = '123123'; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x7fe810f58fb0>; layer = <CALayer: 0x7fe810c51600>>
📍__31-[ViewController textFiledTest]_block_invoke_2 + 215🎈 x = 123123

我们很容易的监听到textFiled中发生的变化,其中x的类型默认为id类型, 我们已知它的类型的时候我们可以将其改变,就像上面代码,将id改成了NSString类型。

手势

1
2
3
4
5
6
7
self.view.userInteractionEnabled = YES;
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc]init];
[[tap rac_gestureSignal] subscribeNext:^(UITapGestureRecognizer * tap) {

LxDBAnyVar(tap);
}];
[self.view addGestureRecognizer:tap];

为了方便,我们直接添加到self.view上,点击屏幕,得到打印结果:

1
📍__29-[ViewController gestureTest]_block_invoke + 184🎈 tap = <UITapGestureRecognizer: 0x7fa2e3e1f9f0; state = Ended; view = <UIView 0x7fa2e3e20b70>; target= <(action=sendNext:, target=<RACPassthroughSubscriber 0x7fa2e3c064f0>)>>

通知

1
2
3
4
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] subscribeNext:^(NSNotification * notification) {

LxDBAnyVar(notification);
}];

我们建立了一个通知,叫做进入后台, 当程序进入后台的时候通知相应,当我们用RAC写通知的时候,我们有一个好处,就是不用removeObserver通知,因为RAC通知的监听者师RAC自己,它会帮你管理释放方法。可以看方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object {
@unsafeify(object);
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
@strongify(object);
id observer = [self addObserverForName:notificationName object:object queue:nil usingBlock:^(NSNotification *note) {
[subscriber sendNext:note];
}];

return [RACDisposable disposableWithBlock:^{
[self removeObserver:observer];
}];
}] setNameWithFormat:@"-rac_addObserverForName: %@ object: <%@: %p>", notificationName, [object class], object];
}

定时器

1
2
3
4
5
6
7
8
9
10
11
//1. 延迟某个时间后再做某件事
[[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{

LxPrintAnything(rac);
}];

//2. 每间隔多长时间做一件事
[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]]subscribeNext:^(NSDate * date) {

LxDBAnyVar(date);
}];

这是定时器最常用的两种写法,第一种方法,延迟时间去做某件事,更改afterDelay的属性。
第二种方法,每间隔多长时间做一件事,更改interval属性。

代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UIAlertView * alertView = [[UIAlertView alloc]initWithTitle:@"RAC" message:@"ReactiveCocoa" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Ensure", nil];

[[self rac_signalForSelector:@selector(alertView:clickedButtonAtIndex:) fromProtocol:@protocol(UIAlertViewDelegate)] subscribeNext:^(RACTuple * tuple) {

LxDBAnyVar(tuple);

LxDBAnyVar(tuple.first);
LxDBAnyVar(tuple.second);
LxDBAnyVar(tuple.third);
}];
[alertView show];


// 更简单的方式:
[[alertView rac_buttonClickedSignal]subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

用RAC去写代理的时候,会有局限,只能取代没有返回值的代理方法,什么是没有返回值的代理呢?比如说tableView的代理方法:

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

这两个方法一个返回的是CGFloat,一个是void,RAC只能取代void的代理。

KVO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
UIScrollView * scrollView = [[UIScrollView alloc]init];
scrollView.delegate = (id<UIScrollViewDelegate>)self;
[self.view addSubview:scrollView];

UIView * scrollViewContentView = [[UIView alloc]init];
scrollViewContentView.backgroundColor = [UIColor yellowColor];
[scrollView addSubview:scrollViewContentView];

@weakify(self);

[scrollView mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(80, 80, 80, 80));
}];

[scrollViewContentView mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.edges.equalTo(scrollView);
make.size.mas_equalTo(CGSizeMake(CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)));

}];

[RACObserve(scrollView, contentOffset) subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

用RAC写KVO的好处就是方法简单,keypath有代码提示。

第二部分 进阶

信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (RACSignal *)loginSignal
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

RACDisposable * schedulerDisposable = [[RACScheduler mainThreadScheduler]afterDelay:2 schedule:^{

if (arc4random()%10 > 1) {

[subscriber sendNext:@"Login response"];
[subscriber sendCompleted];
}
else {

[subscriber sendError:[NSError errorWithDomain:@"LOGIN_ERROR_DOMAIN" code:444 userInfo:@{}]];
}
}];

return [RACDisposable disposableWithBlock:^{

[schedulerDisposable dispose];
}];

}];
}

RAC的核心就是RACSignal,也就是信号,我们可以直接创建信号createSignal,并发送它sendNext,当信号完成后我们同时用dispose方法销毁它。发送信号,我们同时也要订阅信号,订阅信号代码如下:

1
2
3
4
5
6
7
8
9
10
[signal subscribeNext:^(id x) {

LxDBAnyVar(x);
} error:^(NSError *error) {

LxDBAnyVar(error);
} completed:^{

LxPrintAnything(completed);
}];

在信号发送的时候, 错误的时候,以及完成的时候,我们都可以得到相应。

信号的处理

map (映射)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
UITextField * textField = ({
UITextField * textField = [[UITextField alloc]init];
textField.backgroundColor = [UIColor cyanColor];

textField;
});
[self.view addSubview:textField];

@weakify(self); // __weak __typeof__(self) self_weak_ = self;

[textField mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self); // __strong __typeof__(self) self = self_weak_;
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];

[[textField.rac_textSignal map:^id(NSString *text) {

LxDBAnyVar(text);

return @(text.length);

}] subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

map这个函数,在这里不是地图的意思,代表映射。map能做的事情就是把监听的rac_textSignal所返回的值,替换成别的就像上面代码中的text的长度。

filter

为了方便演示,我就不再赋值创建textField的代码了,请到Demo中查看

1
2
3
4
5
6
7
8
9
10
11
12
13
[[[textField.rac_textSignal map:^id(NSString *text) {

LxDBAnyVar(text);

return @(text.length);

}]filter:^BOOL(NSNumber *value) {

return value.integerValue > 3;

}] subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

filter是个BOOL值,它代表的是一个条件,当这个条件发生的时候才会作出相应,比如上面代码中,当长度大于3的时候,才会打印x的值。

delay
1
2
3
4
5
6
7
8
9
10
11
//创建信号
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"rac"];
[subscriber sendCompleted];
return nil;
}]delay:2];
LxPrintAnything(start);
//创建订阅者
[signal subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

delay的作用就是延迟,或者说等待,如上,等待2秒之后打印了x。

startWith
1
2
3
4
5
6
7
8
9
10
11
12
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

// [subscriber sendNext:@"123"];//startWith:@"123"等同于这句话 也就是第一个发送,主要是位置
[subscriber sendNext:@"rac"];
[subscriber sendCompleted];
return nil;
}]startWith:@"123"];
LxPrintAnything(start);
//创建订阅者
[signal subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

startWith也就是最开始的意思,看以上代码 startWith:@"123"等同于[subscriber sendNext:@"123"] 也就是第一个发送,主要是位置.

timeOut
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

[[RACScheduler mainThreadScheduler]afterDelay:3 schedule:^{

[subscriber sendNext:@"rac"];
[subscriber sendCompleted];
}];


return nil;
}] timeout:2 onScheduler:[RACScheduler mainThreadScheduler]]
subscribeNext:^(id x) {

LxDBAnyVar(x);
} error:^(NSError *error) {

LxDBAnyVar(error);
} completed:^{

LxPrintAnything(completed);
}];

上面代码的意思就是,我设置了超时限制为(timeout)2秒钟,但是我代码延迟3秒钟发送,超时了,所以这条信息发生错误,会走error的方法。 这种情况可以用在封装http client中,当然你可能遇到别的需求,也需要它。

take – skip
1
2
3
4
5
6
7
8
9
10
11
12
RACSignal * signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"rac1"];
[subscriber sendNext:@"rac2"];
[subscriber sendNext:@"rac3"];
[subscriber sendNext:@"rac4"];
[subscriber sendCompleted];
return nil;
}]take:2];//Skip

[signal subscribeNext:^(id x) {
LxDBAnyVar(x);
}];

比如说我们发送了很多次请求

take表示我们只取前两次
skip表示跳过前两次
takeLast表示倒数的前两次
takeUntil这个值比较特殊,他后面的参数是个信号,它的意思是,当takeUntil发送这个信号的时候,上面的发送信号就会停止发送。

接下来是几个block回调方法

takeWhileBlock BOOL值,意思是当返回YES的时候,订阅者才能收到信号
skipWhileBlock BOOL值,意思是当返回YES的时候,订阅者就会跳过信号,NO的时候才接受
skipUntilBlock BOOL值,意思是 返回NO的时候,不会收到消息, 直到返回YES的时候才开始收消息。

即时搜索优化 (throttle,distinctUntilChanged,ignore)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
UITextField * textField = [[UITextField alloc]init];
textField.backgroundColor = [UIColor cyanColor];
[self.view addSubview:textField];

@weakify(self);

[textField mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];
//throttle 后面是个时间 表示rac_textSignal发送消息,0.3秒内没有再次发送就会相应,若是0.3内又发送消息了,便会在新的信息处重新计时
//distinctUntilChanged 表示两个消息相同的时候,只会发送一个请求
//ignore 表示如果消息和ignore后面的消息相同,则会忽略掉这条消息,不让其发送
[[[[[[textField.rac_textSignal throttle:0.3] distinctUntilChanged] ignore:@""] map:^id(id value) {

return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

// network request
[subscriber sendNext:value];
[subscriber sendCompleted];

return [RACDisposable disposableWithBlock:^{

// cancel request
}];
}];
}]switchToLatest] subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

以上代码,是用textField模拟一个即时搜索优化的功能,其中参数如下:

throttle 后面是个时间 表示rac_textSignal发送消息,0.3秒内没有再次发送就会相应,若是0.3内又发送消息了,便会在新的信息处重新计时
distinctUntilChanged 表示两个消息相同的时候,只会发送一个请求
ignore 表示如果消息和ignore后面的消息相同,则会忽略掉这条消息,不让其发送

这样做,是不是给服务器减小了很多的压力,更是节省了我们大量的代码。 其中我们用map建立了一个新的信号,我们知道textField的改变是一个信号, map就是在这个信号上,又加了一个信号,即signal of signals
订阅者所打印的消息x则是,map发出的信号。我们可以再map中发送新的信号,以及取消信号disposable.
当我们用map发送信号的时候,我们则需要使用 switchToLatest这个参数来获取最后一个信号,也就是我们最后所打印的x,就是map最后发错的这个信号。

repeat
1
2
3
4
5
6
7
8
9
10
11
12
13
[[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

[subscriber sendNext:@"rac"];
[subscriber sendCompleted];

return nil;
}]delay:1]repeat]take:3] subscribeNext:^(id x) {

LxDBAnyVar(x);
} completed:^{

LxPrintAnything(completed);
}];

repeat,顾名思义,就是重复发送这条消息,当我们在后面添加了delay和take的时候,意思就是每隔1秒发送一次这条消息,发送3次后停止。

merge – concat – zipWith
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
RACSignal * signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
LxPrintAnything(a);
[subscriber sendNext:@"a"];
[subscriber sendCompleted];
});

return nil;
}];

RACSignal * signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
LxPrintAnything(b);
[subscriber sendNext:@"b"];

[subscriber sendCompleted];
});

return nil;
}];

[[RACSignal merge:@[signalA, signalB]]subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

我们创建了两个请求,A和B,用GCD的方法A延迟两秒钟,B延迟了3秒钟,我们用merge方法合并了A和B,打印结果为

1
2
3
4
📍__23-[ViewController merge]_block_invoke_2 + 66🎈 a
📍__23-[ViewController merge]_block_invoke29 + 87🎈 x = a
📍__23-[ViewController merge]_block_invoke_215 + 77🎈 b
📍__23-[ViewController merge]_block_invoke29 + 87🎈 x = b

也就是A和B不管谁发送都会打印x,简单的说就是A和B的打印方法用的是同一个。他们之间关系是独立的,如果A发送失败,B依然会执行。

当我们用concat方法链接A和B之后,意思就是当A执行完了之后才会执行B,他们之间是依赖的关系,如果A发送失败,B也不会执行。

请注意合并(merge)和链接(concat)的区别。

zipWith,当用zipWith链接A和B的时候,只有在A.B每隔都至少发送过一次消息的时候才会执行zipWith的方法,它的返回值是一个集合,也就是数组,同时包含了A和B的打印结果。
zipWith的写法等同于 :

1
2
3
4
[[RACSignal combineLatestWith:@[signalA, signalB]subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

亦或者

1
2
3
4
[[RACSignal combineLatest:@[signalA, signalB]]subscribeNext:^(id x) {

LxDBAnyVar(x);
}];

但是使用combineLatest,可以再后面添加更多的信号.

RAC(<#TARGET, …#>) 宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//button setBackgroundColor:forState:

UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];
[self.view addSubview:button];

@weakify(self);

[button mas_makeConstraints:^(MASConstraintMaker *make) {

@strongify(self);
make.size.mas_equalTo(CGSizeMake(180, 40));
make.center.equalTo(self.view);
}];


RAC(button, backgroundColor) = [RACObserve(button, selected) map:^UIColor *(NSNumber * selected) {

return [selected boolValue] ? [UIColor redColor] : [UIColor greenColor];
}];

[[button rac_signalForControlEvents:UIControlEventTouchUpInside]subscribeNext:^(UIButton * btn) {

btn.selected = !btn.selected;
}];

比如Btn的设置背景颜色的属性,OC中并没有button setBackgroundColor:forState:这种方法,我们不能直接设置其选中后的颜色。在RAC中,则可以很简单的改变BTN的背景颜色。不得不说RAC的简单和强大。

做一个秒表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
UILabel * label = ({

UILabel * label = [[UILabel alloc]init];
label.backgroundColor = [UIColor cyanColor];
label;
});
[self.view addSubview:label];

@weakify(self);

[label mas_makeConstraints:^(MASConstraintMaker *make) {
@strongify(self);

make.size.mas_equalTo(CGSizeMake(240, 40));
make.center.equalTo(self.view);

}];

RAC(label, text) = [[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] map:^NSString *(NSDate * date) {

return date.description;
}];

只有这么多代码,我们便可以完美的做一个秒表,是否很cool?

结束

当我们大量使用RAC写代码的时候,会把一个个事件封装成一个个信号,通过触发信号,订阅这个信号来返回各种信息。RAC使我们的代码耦合性根底,聚合性更高。

若有不懂得地方可以留言,若有写错的地方,请及时与我联系,可以留言或者Email等。

文本所用的Demo,下载地址 戳这里.

文章目录
  1. 1. 前言
  2. 2. 简介
  3. 3. 安装
  4. 4. 撸代码
    1. 4.1. 第一部分 简单使用
      1. 4.1.1. 文本框事件
      2. 4.1.2. 手势
      3. 4.1.3. 通知
      4. 4.1.4. 定时器
      5. 4.1.5. 代理
      6. 4.1.6. KVO
    2. 4.2. 第二部分 进阶
      1. 4.2.1. 信号
      2. 4.2.2. 信号的处理
        1. 4.2.2.1. map (映射)
        2. 4.2.2.2. filter
        3. 4.2.2.3. delay
        4. 4.2.2.4. startWith
        5. 4.2.2.5. timeOut
        6. 4.2.2.6. take – skip
        7. 4.2.2.7. 即时搜索优化 (throttle,distinctUntilChanged,ignore)
        8. 4.2.2.8. repeat
        9. 4.2.2.9. merge – concat – zipWith
    3. 4.3. RAC(<#TARGET, …#>) 宏
      1. 4.3.1. 做一个秒表
  5. 5. 结束