全部文章
- 简介
- 基础部分
- 快速上手
- Promise 的常见模式
- 常见问题
- 进阶部分
- 故障排除
- 附录
- API 说明
以下是对
PromiseKit
的README.md
的翻译。
为什么我应该使用 PromiseKit (X-Promises-Foo)?
- PromiseKit 非常注重开发人员的体验。作为开发者,如果你关系自己的编程体验,那就用 PromiseKit 吧。
- 你是否想修复所有的 bug?那就用 PromiseKit 吧。
- 你是否想提高编程和获取结果的速度?那就用 PromiseKit 吧。
- 你是否想要一个持续不断积极维护了 6 年的库?那就用 PromiseKit 吧。
- 你是否想要一个被社区公认为最好的提供 Promises/Futures 功能的库?那就用 PromiseKit 吧。
- 你是想直接在苹果的 SDK 上使用 Promise,而不用自己去实现相关的功能?那就用 PromiseKit 吧。
- 你是否想在 Swift 3.x, Swift 4.x, ObjC, iOS, tvOS, watchOS, macOS, Android & Linux 上使用 Promise?那就用 PromiseKit 吧。
- 为了保证 PromiseKit 的正确性,整个框架都通过测试用例进行了测试 Promises/A+ test suite。
怎样创建一个状态为 fulfilled 的 Void 的 promise?
let foo = Promise()
// or:
let bar = Promise.value(())
如何快速 return?
// viod 类型
func foo() -> Promise {
guard thingy else {
return Promise()
}
//…
}
// 非 viod 类型
func bar() -> Promise {
guard thingy else {
return .value(instanceOfSomethingNotVoid)
}
//…
}
是否需要关心循环引用?
通常情况下,不需要。Promise 在完成后,将会释放所有的处理程序,因此指向 self 的引用也将会被释放。
但是,如果你不想在调用链中包含任何副作用,那你依然需要使用 weak self(并检查 self == nil),来避免任何副作用的产生。
根据我们的经验,很多开发人员认为应该避免的副作用,实际上并不是副作用。
副作用包括修改应用程序的全局状态,而不包括修改 view-controller 的显示状态。所以,应该避免设置 UserDefaults 和修改应用程序的数据库,至于修改一个 UILabel 的 text 则不用担心。
关于这个话题,可以查看 This StackOverflow question 中的讨论。
是否需要持有 promise 的实例?
不需要。在处理程序执行结束之前,promise 中的处理程序会持有这个 promise。当所有的处理程序执行完毕时,这个 promise 就会被释放。所以,当你需要获取调用链完成之后的结果值时,你才需要持有这个 Promise。
应该在哪里写 catch?
catch 会终止调用链。所以你应该尽可能的将 catch 放在 Promise 的尾部。一个典型的场景是,在 view Controller 中你可以在 catch 中向用户展示一条提示信息。
也就是说,你应该在多个 then 后面只写一个 catch,并且返回的 promise 的内部不包含 catch 处理程序。
很明显这是一条建议,需要根据实际情况而定。
调用链的分支是如何运行的?
假设有这样一个的 promise:
let promise = foo()
然后调用了两次 then:
promise.then {
// branch A
}
promise.then {
// branch B
}
这样就有了一个分支调用链。当 promise 完成时,两个调用链将同时获取到它的结果。但是,这个两个链是完全分开的,Swift 也会提示你给这两个链都加上 catch 处理程序。
你可能会想省略掉其中的一个 catch,但是一定要小心,因为这样做的话,swift 就无法提示你调用链必须有错误处理程序了。
promise.then {
// branch A
}.catch { error in
//…
}
_ = promise.then {
print("foo")
// ignoring errors here as print cannot error and we handle errors above
}
更安全的做法应该是将两个分支合并进一个调用链中:
let p1 = promise.then {
// branch A
}
let p2 = promise.then {
// branch B
}
when(fulfilled: p1, p2).catch { error in
//…
}
值得注意的是:你也可以在一个 Promise 后添加多个 catch 处理程序。而且,如果调用链变为 rejected 时,这些 catch 都会被调用。
PromiseKit 程序包大么?
不大。PromiseKit 包含的源代码非常的少。实际上,它非常的轻量。和其他实现 promise 的框架相比,会多出 6 年来修改 bug 和调整所造成的代码。我们有出色的 Object-C 到 Swift 的桥接,并且考虑了很多业余项目不会考虑到的注意点。
为什么调试如此困难?
因为 Promise 是通过派发执行的,所以在发生错误的时,和平时使用跟踪路径的实现方式相比, 看到的堆栈信息要少一些。
一个解决方案是,在 debuging 的时候关闭派发:
// Swift
DispatchQueue.default = zalgo
//ObjC
PMKSetDefaultDispatchQueue(zalgo)
不要一直这样写。在正常的使用中,我们会始终会用派发的方式来避免这个常见的错误。可以查看这篇博客。
怎么没有 all()?
一些 Promise 框架提供了 all 来等待多个 Promise 的结果。我们提供的方法是 when,他们本质上是一样的。我们选择使用 when 是因为它是更常见的术语,而且在代码当中更易阅读
应该如何测试 Promise 返回的 API?
可以使用 XCTestExpectation
进行测试。
我们也提供了 wait() 和 hang()。如果一定要使用这些方法,那么你一定要小心一点,因为这些方法会阻止当前线程。
PromiseKit 是线程安全的么?
是的,全部都是。
但是,你在 then 中添加的代码可能不是线程安全的。
只要确保不要在并发队列中访问除了调用链之外的状态就可以了。PromiseKit 的处理程序默认运行在 main 队列,这个队列又是串行的,所以通常你不用考虑这个问题。
Object-C 和 Swift 中为什么使用不同的类?
Promise
是通用的,然而无法用 Object-C 进行表示。
PromiseKit 是否符合 Promise/A+?
是的。可以通过我们的测试程序查看这一点。
PromiseKit 和 RxSwift/ReactiveSwift 有什么差别?
PromiseKit 要简单很多。
PromiseKit 和 RxSwift 最大的差别是:RxSwift 中的 Observables(相当于 PromiseKit 当中的 Promise)不一定返回一个结果,他可能返回 0 个,一个或者无限个值流。这个概念上的细微差别,导致 API 即强大又复杂。
RxSwift 致力于编程方式上的转变。它建议将代码重构为值的传递。在恰当的地方使用 RxSwift,会大大的简化代码。但是,并非所有的应用程序都适合使用 RxSwift。
相比之下,PromiseKit 选择性的将响应式编程中最难的部分应用于纯 swift 开发,即异步管理部分。它是一个更加通用的工具。只需要将异步代码转化为使用 Promise,就可以使代码的更加的简洁、简单,而且健壮。(这个转化过程又非常的容易。)
对开发人员来说,用 Promises 写出的代码更加的清晰。RxSwift 则不然。去看看使用 RxSwift 实现的注册功能,然后你就会发现我说的是不是真的。(注意:这可是 RxSwift 自己的示例程序)
即使 PromiseKit 和 RxSwift 看起来相似,但是实现方式存在很多差异:
- RxSwift 使用一个特殊的元素(subscribers)来终止调用链。而 PromiseKit 中,链中的所有元素都使用了相同的代码模式。
- 在 RxSwift 的 API 中定义了非常多的操作符,然后为每个操作符提供相应的实现。PromiseKit 仅提供了一些实用的方法来帮助你解决特定的情况,编写调用链非常的简单,同时又不需要在库中添加额外的代码。
- PromiseKit 会派发所有的处理程序。RxSwift 仅在指定时才派发。此外,在 PromiseKit 中,调度状态是链的属性,而不是处理程序的。RxSwift 功能更强大,但是复杂。PromiseKit 简单,可预测,并且安全。
- 在 PromiseKit 中,分支链都会引用共享的父类。在 RxSwift 中,分支会重新创建一个并行链,然后在链的开头重新运行代码,也有可能不是这样。确定代码的执行规则非常的复杂。由其他代码创建的调用链,你根本无法判断出它的执行规则。
- 因为 RxSwift 链不一定会自己终止,所有 RxSwift 需要你负责一些垃圾回收的功能。所有的 promises 只会产生一个单一的值,执行结束的时候回自动销毁。
更多信息,可以查看这个话题。
Promise 什么时候开始执行?
时常有人困惑 Promise 是什么时候开始执行的?是立即执行?还是稍后?还是当调用 then 时?
答案是:Promise 的内容会在初始化的时候执行,而且是在当前线程上。举例来说,在创建 Promise 之后,就会立即在控制台打印:"Executing the promise body" ,而不用等到调用在 Promise 上 then。
let testPromise = Promise {
print("Executing the promise body.")
return $0.fulfill(true)
}
定义在 Promise 内的异步任务是怎样的呢?他们行为和没有使用 PromiseKit 时是一样的。一个简单的例子:
let testPromise = Promise { seal in
print("Executing the promise body.")
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
print("Executing asyncAfter.")
return seal.fulfill(true)
}
}
"Executing the promise body." 信息会被立即打印,而 "Executing asyncAfter." 信息只会在 3 秒之后打印。这种情况下,DispatchQueue 决定了何时执行交给它的任务,和 PromiseKit 没有关系。
如何将 Firebase 与 PromiseKit 结合在一起使用?
没有将 Firebase 和 PromiseKit 一起使用的好方法。有关更详细的解释,请查看下一个问题
目前最好的方式是将调用链嵌入到 Firebase 的处理方法中:
foo.observe(.value) { snapshot in
firstly {
bar(with: snapshot)
}.then {
baz()
}.then {
baffle()
}.catch {
//…
}
}
我需要 then 执行多次
这种情况下,不应该使用 PromiseKit。Promise 只执行一次。这是 Promise 的一个特性,而且这个特性为你的调用链提供了一个确定的预期。
如何修改执行处理程序的默认队列
你可以修改 PromiseKit.conf.Q 中的值。这里有两个变量来控制两种处理程序的默认队列。一种典型的模式是:将所有的处理程序放到后台队列上执行,而 finalizer 放在主队列上执行:
PromiseKit.conf.Q.map = .global()
PromiseKit.conf.Q.return = .main //NOTE this is the default
将这两个队里中的任何一个设置为 nil 时都要非常小心。设置会立即生效,在应用中这通常不符合你的预期。但是,在运行 specs 时,想要立即运行 promise 时非常有用。(这和 HTTP 请求中的 stubbing 思路是一样的。)
// in your test suite setup code
PromiseKit.conf.Q.map = nil
PromiseKit.conf.Q.return = nil
如何在服务器上使用 PromiseKit?
如果你的服务器框架要求不能使用 main 队列(例如:Kitura),就必须设置 PromiseKit 在默认的情况下不能派发到 main 队列。只要用下面的设置就行了:
PromiseKit.conf.Q = (map: DispatchQueue.global(), return: DispatchQueue.global())
注意:我们建议你使用自己的队列,而不是 .global(),以此来提高性能。
下面是完成的例子:
import Foundation
import HeliumLogger
import Kitura
import LoggerAPI
import PromiseKit
HeliumLogger.use(.info)
let pmkQ = DispatchQueue(label: "pmkQ", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem)
PromiseKit.conf.Q = (map: pmkQ, return: pmkQ)
let router = Router()
router.get("/") { _, response, next in
Log.info("Request received")
after(seconds: 1.0).done {
Log.info("Sending response")
response.send("OK")
next()
}
}
Log.info("Starting server")
Kitura.addHTTPServer(onPort: 8888, with: router)
Kitura.run()
如何设置控制台的输出?
默认情况下,PromiseKit 在某些事件下会在控制台输出消息。比如下面的事件:
- 一个 promise 或者 guarantee 阻塞了主线程
- 一个 promise 还没有完成就被释放了
- 在 promise 的执行过程中发生的错误没有被妥善处理
如果要关闭或者重定向这个输出过程,你可以在执行所有的 promise 之前,通过给 PMKConfiguration 一个线程安全的闭包来修改。
conf.logHandler = { event in }