本文主要讲解Combine中与时间相关的Operator,由于pipline是异步流,所以这些时间控制的Operator还是很强大的。
debounce
上图演示了debounce
的基本原理:
- 首先设置了时间窗口的时长,上图为0.5秒
- publisher每次发送一个新的数据,都会重新开启一个时间窗口,并取消之前的时间窗口
- 最后开启的时间窗口的时间结束后,如果没有新的数据,
debounce
把数据发送到下游
那么它的使用场景是什么呢?
- 处理搜索框过于频繁发起网络请求的问题,每当用户输入一个字符的时候,都发起网络请求,会浪费一部分网络资源,通过
debounce
,可以实现,当用户停止输入0.5秒再发送请求。 - 处理按钮的连续点击问题,
debounce
只接收0.5秒后的最后一次点击事件,因此自动忽略了中间的多次连续点击事件
下边的代码是官方的一个示例:
let bounces:[(Int,TimeInterval)] = [
(0, 0),
(1, 0.2), // 0.2s interval since last index
(2, 1), // 0.7s interval since last index
(3, 1.2), // 0.2s interval since last index
(4, 1.5), // 0.2s interval since last index
(5, 2) // 0.5s interval since last index
]
let subject = PassthroughSubject<Int, Never>()
cancellable = subject
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { index in
print ("Received index \(index)")
}
for bounce in bounces {
DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
subject.send(bounce.0)
}
}
delay
delay
是一个非常简单的Operator,它能够让pipline在收到publisher发送的数据后,等待一定的时长,然后再发送数据到下游。
由上图可以看出,在0.2秒和0.5秒时发送了数据,但最终数据在1.2秒和1.5秒才被输出。
代码如下:
let bounces:[(Int,TimeInterval)] = [
(0, 0.2),
(1, 0.5)
]
let subject = PassthroughSubject<Int, Never>()
cancellable = subject
.delay(for: 1.0,
scheduler: RunLoop.main)
.sink { index in
print ("Received index \(index) in \(Date().timeIntervalSince1970)")
}
for bounce in bounces {
DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
subject.send(bounce.0)
print ("send \(Date().timeIntervalSince1970)")
}
}
输出如下:
send 1606736282.5276031
send 1606736282.8222818
Received index 0 in 1606736283.528779
Received index 1 in 1606736283.822552
官方的一个例子:
let df = DateFormatter()
df.dateStyle = .none
df.timeStyle = .long
cancellable = Timer.publish(every: 1.0, on: .main, in: .default)
.autoconnect()
.handleEvents(receiveOutput: { date in
print ("Sending Timestamp \'\(df.string(from: date))\' to delay()")
})
.delay(for: .seconds(3), scheduler: RunLoop.main, options: .none)
.sink(
receiveCompletion: { print ("completion: \($0)", terminator: "\n") },
receiveValue: { value in
let now = Date()
print ("At \(df.string(from: now)) received Timestamp \'\(df.string(from: value))\' sent: \(String(format: "%.2f", now.timeIntervalSince(value))) secs ago", terminator: "\n")
}
)
// Prints:
// Sending Timestamp '5:02:33 PM PDT' to delay()
// Sending Timestamp '5:02:34 PM PDT' to delay()
// Sending Timestamp '5:02:35 PM PDT' to delay()
// Sending Timestamp '5:02:36 PM PDT' to delay()
// At 5:02:36 PM PDT received Timestamp '5:02:33 PM PDT' sent: 3.00 secs ago
// Sending Timestamp '5:02:37 PM PDT' to delay()
// At 5:02:37 PM PDT received Timestamp '5:02:34 PM PDT' sent: 3.00 secs ago
// Sending Timestamp '5:02:38 PM PDT' to delay()
// At 5:02:38 PM PDT received Timestamp '5:02:35 PM PDT' sent: 3.00 secs ago
当Timer.publisher调用了.autoconnect()
后,就会立刻触发,如果我们想延时几秒的话,可以使用delay
。
measureInterval
measureInterval也是一个非常有意思的Operator,它能够记录publisher发送数据的间隔时间。
以上图为例,当publisher发送数据0时,我们获得的时间间隔大约 0.2秒,发送数据1时,时间间隔大约为1.3秒,measureInterval返回的数据的类型是SchedulerTimeType.Stride
,它表示两者之间的距离。
注意,时间间隔不是严格准确的,存在一定范围的偏差。
let bounces:[(Int,TimeInterval)] = [
(0, 0.2),
(1, 1.5)
]
let subject = PassthroughSubject<Int, Never>()
cancellable = subject
.measureInterval(using: RunLoop.main)
.sink { print($0) }
for bounce in bounces {
DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
subject.send(bounce.0)
}
}
打印结果:
Stride(magnitude: 0.25349903106689453)
Stride(magnitude: 1.2467479705810547)
throttle
throttle
是一个比较简单的Operator,如上图所示,它会设定一个固定时间间隔的时间窗口,在上图中,这个时间窗口的时长为0.5秒,当时间窗口结束后,就发送数据。
上图中就是分别在0.5秒,1.0秒,1.5秒的时刻发送数据。
注意,latest
可以指定发送的数据是该窗口内的第一个数据还是最后一个数据。当publisher发送的数据刚好在时间窗口边缘的时候,结果是不确定的。
代码如下:
let bounces:[(Int,TimeInterval)] = [
(1, 0.2),
(2, 1),
(3, 1.2),
(4, 1.4),
]
let subject = PassthroughSubject<Int, Never>()
cancellable = subject
.throttle(for: 0.5,
scheduler: RunLoop.main,
latest: true)
.sink { index in
print ("Received index \(index) in \(Date().timeIntervalSince1970)")
}
for bounce in bounces {
DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
subject.send(bounce.0)
}
}
要想理解throttle
,最好和debounce
做一个对比,相同的代码下,debounce
的示意图如下:
总结一下,throttle
按照固定的顺序排列时间窗口,在时间窗口的结尾处发送数据,而debounce
每次接收到新数据,都会重新开启一个新的时间窗口,同时取消之前的时间窗口。
举个例子。如果我在2秒内疯狂点击按钮,时间窗口的时长为0.5秒,那么throttle
可以发送4次数据,而debounce
不会发送数据,只有当我停止点击0.5秒后,才会发送一次数据。
timeout
如上图所示,timeout
用于设置pipline的超时时间,通常以秒为单位。先看下代码:
enum MyError: Error {
case timeout
}
let subject = PassthroughSubject<Int, Never>()
cancellable = subject
.setFailureType(to: MyError.self)
.timeout(.seconds(1),
scheduler: RunLoop.main,
customError: {
MyError.timeout
})
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
subject.send(1)
}
timeout
的前两个参数没什么好说的,第三个参数有点意思,当发生超时的时候,customError
就会调用,然后返回一个错误。
它返回的错误类型跟上游publisher返回的错误类型需保持一致,上边的代码中,如果我们想要返回我们自定义的错误类型,就要使用.setFailureType(to: MyError.self)
把PassthroughSubject<Int, Never>()
的Never错误类型设置成MyError。
那么现在产生了一个新的问题,如果把customError
赋值为nil,会怎样呢?
.timeout(.seconds(1),
scheduler: RunLoop.main,
customError: nil)
打印结果:
finished
可以看出,如果指定了customError,则pipline返回该闭包中的错误,如果没有指定,pipline就会正常结束。
如下图所示: