EasyAsync 0.2.1

EasyAsync 0.2.1

Sergii Kutnii维护。



EasyAsync 0.2.1

  • Sergii Kutnii

easyasync

这是一个库,包含了一些我最初在面试作业环境中编写的有用的Swift代码。在这个过程中,我开发了解决常见编程问题的几个有趣方案。该代码适用于iOS和OS X。

然后我将我认为能极大地简化生活的代码部分提取到这个包中。

当前版本是0.2.1。

特性

参见CHANGELOG.md

使用技巧

此库作为名为EasyAsync的CocoaPod提供。

Promise

简洁版

当使用异步API时,Promise是对回调地狱问题的一种解决方案。假设您需要从三个不同的来源通过HTTP获取数据。使用URLSession,您将编写:

URLSession.shared.dataTask(with: request1) {
    data1, response1, error1 in
    let request2 = createSecondRequest(fromData: data1)
    URLSession.shared.dataTask(with: request2) {
        data2, response2, error2 in
        let request3 = createThirdRequest(fromData: data2)
        URLSession.shared.dataTask(with: request3) {
            data3, response3, error3 in
            processData(data1, data2, data3)
        } .resume()
    } .resume()
} .resume()

这段代码不会按编写的顺序执行,如果添加错误处理,您的生命会变成一场噩梦。

使用Promise,等价的代码如下(我们使用Fetch.request(_:)——HTTP通信的基于Promise的解决方案):

var firstData: Data? = nil
var secondData: Data? = nil

Fetch.request(request1).then(async: {
    data1 -> Promise<Data> in
    firstData = data1
    let request = createSecondRequest(fromData: data1)
    return Fetch.request(request)
}).then(async: {
    data2 -> Promise<Data> in
    secondData = data2
    let request = createSecondRequest(fromData: data2)
    return Fetch.request(request)
}).then({
    thirdData in
    processData(firstData!, secondData!, thirdData)
}).rescue({
    reason in
    handleError(reason)
})

这样代码的执行顺序与编写的顺序相同。使用Promise,异步代码看起来更像同步代码。

请注意,这里增加了错误处理。如果在过程中某处抛出错误,则错误会沿着承诺链向下传播,直到遇到匹配的 rescue。因此,示例末尾的单个 rescue 语句可以捕获所有错误 - 这在“灾难之塔”中几乎是不可能的。

有关更多信息,请参阅 JavaScript承诺文档

制作承诺

在上述示例中,定义了两个变量来保存中间操作的结果。虽然这种解决方案可能最简洁,但它可能不是封装性最好的解决方案。使用 Swift 元组,可以编写一个消除额外变量需求的解决方案。

func fetchSecondData(for data1: Data) -> Promise<(Data, Data)> {
    let request = createSecondRequest(fromData: data1)
    let promise = Promise<(Data, Data)>()
    Fetch.request(request).then {
        data2 in
        promise.resolve((data1, data2))
    } .rescue {
        (reason: Any?) in
        promise.reject(reason)
    }
    
    return promise
}

func fetchThirdData(for dataTuple: (Data, Data)) -> Promise<(Data, Data, Data)> {
    let (data1, data2) = dataTuple
    let request = createThirdRequest(fromData: data2)
    let promise = Promise<(Data, Data, Data)>()
    Fetch.request(request).then {
        data3 in
        promise.resolve((data1, data2, data3))
    } .rescue {
        (reason: Any?) in
        promise.reject(reason)
    }
    
    return promise
}

Fetch.request(request1).then(async: {
    data1 -> Promise<(Data, Data)> in
    return fetchSecondData(for: data1)
}).then(async: {
    (dataTuple) -> Promise<(Data, Data, Data)> in
    return fetchThirdData(for: dataTuple)
}).then({
    ((data1, data2, data3)) in
    processData(data1, data2, data3)
}).rescue({
    reason in
    handleError(reason)
})

在此,在 fetchSecondDatafetchThirdData 中创建了自定义承诺。这是通过使用 Promise<T>() 初始化器和 resolvereject 方法实现的。这些是开发基于承诺的解决方案的基础。

承诺可以是三种状态之一:挂起、实现和拒绝。当使用 Promise<T>() 创建承诺时,它处于挂起状态,并且只要 resolvereject 在它上面调用,它就会保持这种状态。承诺本身什么也不做,它只是组织和安排回调的一种巧妙方法;因此,如果您正在创建自定义承诺,则解决或拒绝它们是您解决方案的责任。

当对挂起的承诺调用 resolvereject 时,该承诺将分别过渡到实现或拒绝状态。在实现状态下,承诺持有传递给 resolve 的值,在拒绝状态下,它具有传递给 reject 的拒绝原因。在实现或拒绝状态下,承诺被锁定,不能过渡到任何其他状态。

在将值 x 转换为实现状态后,承诺使用 x 作为参数触发其 then 块。在承诺因错误被拒绝,或从 then 块抛出错误后,该承诺调用合适的 rescue 块处理错误。

rescue 是有条件的。例如,如果您写下:

Fetch.request(myRequest).then {
    data in
    doSomething(data)
} .rescue {
    loadError: FetchError in
    print("HTTP error")
}

只有由 Fetch.request(_:) 抛出的连接错误将被捕获,而 doSomething(_:) 抛出的自定义错误则不会。

有关核心 Promise<T> 方法的详细说明,请参阅 doc/Promise.md。

为了让生活更简单,Promise<T>.resolve(_ value:)Promise<T>.reject(_ reason: 构建自动解决和拒绝的承诺。

您可能主要使用的初始化器定义如下

public typealias Initializer = (@escaping (T) -> (), @escaping (Any?) -> ()) -> ()
public convenience init(_ initializer: @escaping Initializer) {

初始化器块将在稍后时间执行(在当前实现中,它是在主调度队列上异步安排的),并且承诺将以挂起状态创建。当调用初始化器时,第一个参数是承诺的 resolve,第二个是 reject 方法。

请注意示例中 fetchSecondDatafetchThirdData 的相似性。事实上,将数据通过承诺传递的任务可能非常常见,以至于已经将该解决方案添加到 Promise<T> 自身,并使用 &&& 操作符。

假设您有 promise1: Promise<T1>value2: T2。那么 promise1 &&& value2 会产生一个 Promise<(T1, T2)>

  • 如果 promise1 被解决,它将使用元组值 (promise1.value!, value2) 被解决;
  • 如果以某些原因拒绝 promise1,则也会以相同的原因被拒绝。

value2 &&& promise1 的结果将类似,但值顺序相反: (value2, promise1.value!)

您还可以使用 &&& 结合两个承诺。如果 promise1: Promise<T1>promise2: Promise<T2>,则

promise3: Promise<(T1, T2)> = promise1 &&& promise2

然后

  • 如果 promise1promise2 因某些原因被拒绝,则 promise3 也会因第一个被拒绝的承诺的原因被拒绝;
  • 如果 promise1promise2 都已履行,则 promise3 会以元组值 (promise1.value!, promise2.value!) 被履行。

因此,使用 &&&,我们的示例将如下所示

fetch.request(request1).then(async: {
    data1 in
    let nextRequest = createSecondRequest(fromData: data1)
    return data1 &&& Fetch.request(nextRequest)
}).then(async: {
    ((data1, data2)) in
    let nextRequest = crreateThirdrequest(from: data2)
    return (data1, data2) &&& Fetch.request(nextRequest)
}).then({
    (((data1, data2), data3)) in
    processData(data1, data2, data3)
}).rescue({
    reason in
    handleError(reason)
})

还有用于承诺的 ||| 运算符。如果 promise1: Promise<T1>promise2: Promise<T2>,则

promise3: Promise<Any> = promise1 ||| promise2

然后

  • 如果 promise1promise2 因某些值而履行,则 promise3 也会因第一个被履行承诺的值而履行;
  • 如果 promise1promise2 都被拒绝,则 promise3 将以元组原因 (promise1.rejectReason, promise2.rejectReason) 被拒绝。

提示:使用括号来控制返回的元组结构 也就是说,始终编写 (promise1 &&& promise2) &&& promise3,而不是 promise1 &&& promise2 &&& promise3 -- 这样就不必总是记住 &&& 的结合性。

&&&||| 运算符为 JavaScript 的 Promise.allPromise.race 提供了替代方案。

由于类型安全,在 JavaScript 中会比在 JavaScript 中更有用的精确等价会少得多。

承诺和内存管理

对于承诺而言,最优秀的内存管理规则可能是 不要保持快速承诺。考虑以下示例

class MyClass {
    var promise: Promise<Data> = Promise<Data>()
    
    func processData() {
        guard .fulfilled == promise.state else {
            return
        }
        
        let data = promise.value!
        //Do something with data
    }
    
    init(request: URLRequest) {
        promise = Fetch.request(request).then {
            _ in
            //Memory leak!
            self.processData()
        }
    }
}

在这里,由于承诺保持了对其 then 块的强引用,MyClass 拥有一个承诺,并且 self 被捕获在 then 块中,创建了一个保留周期,从而导致内存泄漏。

当然,可以通过在 then 处理器中对 self 进行动态引用来打破这个周期,但是最好不要保持承诺的引用。

由于承诺履行或拒绝后不能转换到任何其他状态,承诺本质上是一个一次性对象,因此存储的承诺非常有限。

应该将承诺视为一个瞬态对象。更好的 MyClass 实现

class MyClass {
    var data: Data?

    func processData() {
        guard nil != data else {
            return
        }

        let theData = data!
        //Do something with theData
    }

    init(request: URLRequest) {
        Fetch.request(request).then {
            data in
            //No memory leak here, even though self is captured
            self.data = data
            self.processData()
        }
    }
}

那么,谁为您保留承诺?让我们看看 Fetch.request(_:) 实现的内部情况

class Fetch {
    class func request(_ request: URLRequest) -> Promise<Data> {
        let innerPromise = Promise<Data>()
        let task = URLSession.shared.dataTask(with: request) {
            data, response, error in
            guard nil == error else {
                innerPromise.reject(FetchError.connectionError)
                return
            }

            let code = (response as? HTTPURLResponse)?.statusCode ?? 400
            guard code <= 400 else {
                innerPromise.reject(FetchError.httpError(code))
                return
            }

            guard nil != data else {
                innerPromise.reject(FetchError.noData)
                return
            }

            innerPromise.resolve(data!)
        }

        let promise = Promise<Data>(discard: {
            [weak task] in
            task?.cancel()
        })

        promise.chain(after: innerPromise)
        task.resume()

        return promise
    }
}

在这里,innerPromise 被捕捉在 URLSessionTask 完成块中。返回的承诺在 innerPromise 后被链接,即当 innerPromise 解析/拒绝时,链接的承诺将以相同的值/理由解析/拒绝(参见 doc/Promise.md 2.2)。这也给了 innerPromise 返回承诺的所有权。注意返回承诺的舍弃块中的弱数据任务引用 - 它有助于避免保留周期。

因此,在这种情况下,整个承诺链由 Fetch 实现拥有。如果您在自己的解决方案中使用承诺,您应该遵循相同的模式。在将基于回调的 API 包装在承诺中时,请在回调中捕获承诺。