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)
})在此,在 fetchSecondData 和 fetchThirdData 中创建了自定义承诺。这是通过使用 Promise<T>() 初始化器和 resolve 和 reject 方法实现的。这些是开发基于承诺的解决方案的基础。
承诺可以是三种状态之一:挂起、实现和拒绝。当使用 Promise<T>() 创建承诺时,它处于挂起状态,并且只要 resolve 或 reject 在它上面调用,它就会保持这种状态。承诺本身什么也不做,它只是组织和安排回调的一种巧妙方法;因此,如果您正在创建自定义承诺,则解决或拒绝它们是您解决方案的责任。
当对挂起的承诺调用 resolve 或 reject 时,该承诺将分别过渡到实现或拒绝状态。在实现状态下,承诺持有传递给 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 方法。
请注意示例中 fetchSecondData 和 fetchThirdData 的相似性。事实上,将数据通过承诺传递的任务可能非常常见,以至于已经将该解决方案添加到 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然后
- 如果
promise1或promise2因某些原因被拒绝,则promise3也会因第一个被拒绝的承诺的原因被拒绝; - 如果
promise1和promise2都已履行,则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然后
- 如果
promise1或promise2因某些值而履行,则promise3也会因第一个被履行承诺的值而履行; - 如果
promise1和promise2都被拒绝,则promise3将以元组原因(promise1.rejectReason, promise2.rejectReason)被拒绝。
提示:使用括号来控制返回的元组结构 也就是说,始终编写
(promise1 &&& promise2) &&& promise3,而不是promise1 &&& promise2 &&& promise3-- 这样就不必总是记住&&&的结合性。
&&& 和 ||| 运算符为 JavaScript 的 Promise.all 和 Promise.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 包装在承诺中时,请在回调中捕获承诺。