Combine之Operator(Controlling timing 时间控制)

998 阅读4分钟

github.com/agelessman/…

本文主要讲解Combine中与时间相关的Operator,由于pipline是异步流,所以这些时间控制的Operator还是很强大的。

debounce

企业微信截图_c6641371-b3da-4022-8688-898c13631377.png

上图演示了debounce的基本原理:

  • 首先设置了时间窗口的时长,上图为0.5秒
  • publisher每次发送一个新的数据,都会重新开启一个时间窗口,并取消之前的时间窗口
  • 最后开启的时间窗口的时间结束后,如果没有新的数据,debounce把数据发送到下游

那么它的使用场景是什么呢?

  1. 处理搜索框过于频繁发起网络请求的问题,每当用户输入一个字符的时候,都发起网络请求,会浪费一部分网络资源,通过debounce,可以实现,当用户停止输入0.5秒再发送请求。
  2. 处理按钮的连续点击问题,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

image.png

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

image.png

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

image.png

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的示意图如下:

企业微信截图_c6641371-b3da-4022-8688-898c13631377.png

总结一下,throttle按照固定的顺序排列时间窗口,在时间窗口的结尾处发送数据,而debounce每次接收到新数据,都会重新开启一个新的时间窗口,同时取消之前的时间窗口。

举个例子。如果我在2秒内疯狂点击按钮,时间窗口的时长为0.5秒,那么throttle可以发送4次数据,而debounce不会发送数据,只有当我停止点击0.5秒后,才会发送一次数据。

timeout

image.png

如上图所示,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就会正常结束。

如下图所示:

image.png