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 包装在承诺中时,请在回调中捕获承诺。