一文带你使用 Swift 实现 Promise

开发 前端
我最近在找如何使用 Swift 实现 Promise 的资料,因为没找到好的文章,所以我想自己写一篇。通过本文,我们将实现自己的 Promise 类型,以便明了其背后的逻辑。

[[421175]]

前言

我最近在找如何使用 Swift 实现 Promise 的资料,因为没找到好的文章,所以我想自己写一篇。通过本文,我们将实现自己的 Promise 类型,以便明了其背后的逻辑。

要注意这个实现完全不适合生产环境。例如,我们的 Promise 没有提供任何错误机制,也没有覆盖线程相关的场景。我会在文章的后面提供一些有用的资源以及完整实现的链接,以飨愿深入挖掘的读者。

注:为了让本教程更有趣一点,我选择使用 TDD 来进行介绍。我们会先写测试,然后确保它们一个个通过。

第一个测试

先写第一个测试:

  1. test(named: "0. Executor function is called immediately") { assert, done in 
  2.     var string: String = "" 
  3.     _ = Promise { string = "foo" } 
  4.     assert(string == "foo"
  5.     done() 

通过此测试,我们想实现:传递一个函数给 Promise 的初始化函数,并立即调用此函数。

注:我们没有使用任何测试框架,仅仅使用一个自定义的test方法,它在 Playground 中模拟断言(gist[1])。

当我们运行 Playground,编译器会报错:

  1. error: Promise.playground:41:9: error: use of unresolved identifier 'Promise' 
  2.     _ = Promise { string = "foo" } 
  3.         ^~~~~~~ 

合理,我们需要定义 Promise 类。

  1. class Promise { 
  2.  

再运行,错误变为:

  1. error: Promise.playground:44:17: error: argument passed to call that takes no arguments 
  2.     _ = Promise { string = "foo" } 
  3.                 ^~~~~~~~~~~~~~~~~~ 

我们必须定义一个初始化函数,它接受一个闭包作为参数。而且这个闭包应该被立即调用。

  1. class Promise { 
  2.  
  3.     init(executor: () -> Void) { 
  4.         executor() 
  5.     } 

由此,我们通过第一个测试。目前我们还没有写出什么值得夸耀的东西,但耐心一点,我们的实现将在下一节继续增长。

  1. • Test 0. Executor function is called immediately passed  

我们先将此测试注释掉,因为将来的 Promise 实现会变得有些不同。

最低限度

第二个测试如下:

  1. test(named: "1.1 Resolution handler is called when promise is resolved sync") { assert, done in 
  2.     let string: String = "foo" 
  3.     let promise = Promise<String> { resolve in 
  4.         resolve(string) 
  5.     } 
  6.     promise.then { (value: String) in 
  7.         assert(string == value) 
  8.         done() 
  9.     } 

这个测试挺简单,但我们添加了一些新内容到 Promise 类。我们创建的这个 promise 有一个 resolution handler(即闭包的resolve参数),之后立即调用它(传递一个 value)。然后,我们使用 promise 的 then 方法来访问 value 并用断言确保其值。

在开始实现之前,我们需要引入另外一个不太一样的测试。

  1. test(named: "1.2 Resolution handler is called when promise is resolved async") { assert, done in 
  2.     let string: String = "foo" 
  3.     let promise = Promise<String> { resolve in 
  4.         after(0.1) { 
  5.             resolve(string) 
  6.         } 
  7.     } 
  8.     promise.then { (value: String) in 
  9.         assert(string == value) 
  10.         done() 
  11.     } 

不同于测试 1.1,这里的resove方法被延迟调用。这意味着,在then里,value 不会立马可用(因为 0.1 秒的延迟,调用then时,resolve还未被调用)。

我们开始理解这里的“问题”。我们必须处理异步。

我们的 promise 是一个状态机。当它被创建时,promise 处于pending状态。一旦resolve方法被调用(与一个 value),我们的 promise 将转到resolved状态,并存储这个 value。

then方法可在任意时刻被调用,而不管 promise 的内部状态(即不管 promise 是否已有一个 value)。当这个 promise 处于pending状态时,我们调用then,value 将不可用,因此,我们需要存储此回调。之后一旦 promise 变成resolved,我们就能使用 resolved value 来触发同样的回调。

现在我们对要实现的东西有了更好的理解,那就先以修复编译器的报错开始。

  1. error: Promise.playground:54:19: error: cannot specialize non-generic type 'Promise' 
  2.     let promise = Promise<String> { resolve in 
  3.                   ^      ~~~~~~~~ 

我们必须给Promise类型添加泛型。诚然,一个 promise 是这样的东西:它关联着一个预定义的类型,并能在被解决时,将一个此类型的 value 保留住。

  1. class Promise<Value> { 
  2.  
  3.     init(executor: () -> Void) { 
  4.         executor() 
  5.     } 

现在错误为:

  1. error: Promise.playground:54:37: error: contextual closure type '() -> Void' expects 0 arguments, but 1 was used in closure body 
  2.     let promise = Promise<String> { resolve in 
  3.                                     ^ 

我们必须提供一个resolve函数传递给初始化函数(即 executor)。

  1. class Promise<Value> { 
  2.  
  3.     init(executor: (_ resolve: (Value) -> Void) -> Void) { 
  4.         executor() 
  5.     } 

注意这个 resolve 参数是一个函数,它消耗一个 value:(Value) -> Void。一旦 value 被确定,这个函数将被外部世界调用。

编译器依然不开心,因为我们需要提供一个resolve函数给executor。让我们创建一个private的吧。

  1. class Promise<Value> { 
  2.  
  3.     init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { 
  4.         executor(resolve) 
  5.     } 
  6.  
  7.     private func resolve(_ value: Value) -> Void { 
  8.         // To implement 
  9.         // This will be called by the outside world when a value is determined 
  10.     } 

我们将在稍后实现resolve,当所有错误都被解决时。

下一个错误很简单,方法then还未定义。

  1. error: Promise.playground:61:5: error: value of type 'Promise<String>' has no member 'then' 
  2.     promise.then { (value: String) in 
  3.     ^~~~~~~ ~~~~ 

让我们修复之。

  1. class Promise<Value> { 
  2.  
  3.     init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { 
  4.         executor(resolve) 
  5.     } 
  6.  
  7.     func then(onResolved: @escaping (Value) -> Void) { 
  8.         // To implement 
  9.     } 
  10.  
  11.     private func resolve(_ value: Value) -> Void { 
  12.         // To implement 
  13.     } 

现在编译器开心了,让我们回到开始的地方。

我们之前说过一个Promise就是一个状态机,它有一个pending状态和一个resolved状态。我们可以使用 enum 来定义它们。

  1. enum State<T> { 
  2.     case pending 
  3.     case resolved(T) 

Swift 的美妙让我们可以直接存储 promise 的 value 在 enum 中。

现在我们需要在Promise的实现中定义一个状态,其默认值为.pending。我们还需要一个私有函数,它能在当前还处于.pending状态时更新状态。

  1. class Promise<Value> { 
  2.  
  3.     enum State<T> { 
  4.         case pending 
  5.         case resolved(T) 
  6.     } 
  7.  
  8.     private var state: State<Value> = .pending 
  9.  
  10.     init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { 
  11.         executor(resolve) 
  12.     } 
  13.  
  14.     func then(onResolved: @escaping (Value) -> Void) { 
  15.         // To implement 
  16.     } 
  17.  
  18.     private func resolve(_ value: Value) -> Void { 
  19.         // To implement 
  20.     } 
  21.  
  22.     private func updateState(to newState: State<Value>) { 
  23.         guard case .pending = state else { return } 
  24.         state = newState 
  25.     } 

注意updateState(to:)函数先检查了当前处于.pending状态。如果 promise 已经处于.resolved状态,那它就不能再变成其他状态了。

现在是时候在必要时更新 promise 的状态,即,当resolve函数被外部世界传递 value 调用时。

  1. private func resolve(_ value: Value) -> Void { 
  2.     updateState(to: .resolved(value)) 

快好了,只缺少 then 方法还未实现。我们说过必须存储回调,并在 promise 被解决时调用回调。这就来实现之。

  1. class Promise<Value> { 
  2.  
  3.     enum State<T> { 
  4.         case pending 
  5.         case resolved(T) 
  6.     } 
  7.  
  8.     private var state: State<Value> = .pending 
  9.     // we store the callback as an instance variable 
  10.     private var callback: ((Value) -> Void)? 
  11.  
  12.     init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { 
  13.         executor(resolve) 
  14.     } 
  15.  
  16.     func then(onResolved: @escaping (Value) -> Void) { 
  17.         // store the callback in all cases 
  18.         callback = onResolved 
  19.         // and trigger it if needed 
  20.         triggerCallbackIfResolved() 
  21.     } 
  22.  
  23.     private func resolve(_ value: Value) -> Void { 
  24.         updateState(to: .resolved(value)) 
  25.     } 
  26.  
  27.     private func updateState(to newState: State<Value>) { 
  28.         guard case .pending = state else { return } 
  29.         state = newState 
  30.         triggerCallbackIfResolved() 
  31.     } 
  32.  
  33.     private func triggerCallbackIfResolved() { 
  34.         // the callback can be triggered only if we have a value, 
  35.         // meaning the promise is resolved 
  36.         guard case let .resolved(value) = state else { return } 
  37.         callback?(value) 
  38.         callback = nil 
  39.     } 

我们定义了一个实例变量callback,以在 promise 处于.pending状态时保留回调。同时我们创建一个方法triggerCallbackIfResolved,它先检查状态是否为.resolved,然后传递拆包的 value 给回调。这个方法在两个地方被调用。一个是then方法中,如果 promise 已经在调用then时被解决。另一个在updateState方法中,因为那是 promise 更新其内部状态从.pending到.resolved的地方。

有了这些修改,我们的测试就成功通过了。

  1. • Test 1.1 Resolution handler is called when promise is resolved sync passed (1 assertions) 
  2. • Test 1.2 Resolution handler is called when promise is resolved async passed (1 assertions) 

我们走对了路,但我们还需要做出一点改变,以得到一个真正的Promise实现。先来看看测试。

  1. test(named: "2.1 Promise supports many resolution handlers sync") { assert, done in 
  2.     let string: String = "foo" 
  3.     let promise = Promise<String> { resolve in 
  4.         resolve(string) 
  5.     } 
  6.     promise.then { value in 
  7.         assert(string == value) 
  8.     } 
  9.     promise.then { value in 
  10.         assert(string == value) 
  11.         done() 
  12.     } 
  1. test(named: "2.2 Promise supports many resolution handlers async") { assert, done in 
  2.     let string: String = "foo" 
  3.     let promise = Promise<String> { resolve in 
  4.         after(0.1) { 
  5.             resolve(string) 
  6.         } 
  7.     } 
  8.     promise.then { value in 
  9.         assert(string == value) 
  10.     } 
  11.     promise.then { value in 
  12.         assert(string == value) 
  13.         done() 
  14.     } 

这回我们对每个 promise 都调用了两次then。

先看看测试输出。

  1. • Test 2.1 Promise supports many resolution handlers sync passed (2 assertions) 
  2. • Test 2.2 Promise supports many resolution handlers async passed (1 assertions) 

虽然测试通过了,但你可能也注意问题。测试 2.2 只有一个断言,但应该是两个。

如果我们思考一下,这其实符合逻辑。诚然,在异步的测试 2.2 中,当第一个then被调用时,promise 还处于.pending状态。如我们之前所见,我们存储了第一次then的回调。但当我们第二次调用then时,promise 还是没有被解决,依然处于.pending状态,于是,我们将回调擦除换成了新的。只有第二个回调会在将来被执行,第一个被忘记了。这使得测试虽然通过,但只有一个断言而不是两个。

解决办法也很简单,就是存储一个回调的数组,并在promise被解决时触发它们。

让我们更新一下。

  1. class Promise<Value> { 
  2.  
  3.     enum State<T> { 
  4.         case pending 
  5.         case resolved(T) 
  6.     } 
  7.  
  8.     private var state: State<Value> = .pending 
  9.     // We now store an array instead of a single function 
  10.     private var callbacks: [(Value) -> Void] = [] 
  11.  
  12.     init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) { 
  13.         executor(resolve) 
  14.     } 
  15.  
  16.     func then(onResolved: @escaping (Value) -> Void) { 
  17.         callbacks.append(onResolved) 
  18.         triggerCallbacksIfResolved() 
  19.     } 
  20.  
  21.     private func resolve(_ value: Value) -> Void { 
  22.         updateState(to: .resolved(value)) 
  23.     } 
  24.  
  25.     private func updateState(to newState: State<Value>) { 
  26.         guard case .pending = state else { return } 
  27.         state = newState 
  28.         triggerCallbacksIfResolved() 
  29.     } 
  30.  
  31.     private func triggerCallbacksIfResolved() { 
  32.         guard case let .resolved(value) = state else { return } 
  33.         // We trigger all the callbacks 
  34.         callbacks.forEach { callback in callback(value) } 
  35.         callbacks.removeAll() 
  36.     } 

测试通过,而且都有两个断言。

  1. • Test 2.1 Promise supports many resolution handlers sync passed (2 assertions) 
  2. • Test 2.2 Promise supports many resolution handlers async passed (2 assertions) 

恭喜!我们已经创建了自己的Promise类。你已经可以使用它来抽象异步逻辑,但它还有限制。

注:如果从全局来看,我们知道then可以被重命名为observe。它的目的是消费 promise 被解决后的 value,但它不返回什么。这意味着我们暂时没法串联多个 promise。

串联多个 Promise

如果我们不能串联多个 promise,那我们的Promise实现就不算完整。

先来看看测试,它将帮助我们实现这个特性。

  1. test(named: "3. Resolution handlers can be chained") { assert, done in 
  2.     let string: String = "foo" 
  3.     let promise = Promise<String> { resolve in 
  4.         after(0.1) { 
  5.             resolve(string) 
  6.         } 
  7.     } 
  8.     promise 
  9.         .then { value in 
  10.             return Promise<String> { resolve in 
  11.                 after(0.1) { 
  12.                     resolve(value + value) 
  13.                 } 
  14.             } 
  15.         } 
  16.         .then { value in // the "observe" previously defined 
  17.             assert(string + string == value) 
  18.             done() 
  19.         } 

如测试所见,第一个then创建了一个有新 value 的新Promise并返回了它。第二个then(我们前一节定义的,被称为observe)被串联在后面,它访问新的 value(其将是"foofoo")。

我们很快在终端里看到错误。

  1. error: Promise.playground:143:10: error: value of tuple type '()' has no member 'then' 
  2.         .then { value in 
  3.          ^ 

我们必须创建一个then的重载,它接受一个能返回 promise 的函数。为了能够串联调用then,这个方法必须也返回一个promise。这个then的原型如下。

  1. func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { 
  2.     // to implement 

注:细心的读者可能已经发现,我们在给Promise实现flatMap。就如给Optional和Array定义flatMap一样,我们也可以给Promise定义它。

困难来了。让我们一步步看看这个“flatMap”的then要怎么实现。

  • 我们需要返回一个Promise
  • 谁给我们这样一个 promise?onResolved 方法
  • 但onResolved 需要一个类型为Value的 value 为参数。我们该怎样得到这个 value? 我们可以使用之前定义的then(或者说 “observe”) 来在其可用时访问它

如果写成代码,大概如下:

  1. func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { 
  2.     then { value in // the "observe" one 
  3.         let promise = onResolved(value) // `promise` is a Promise<NewValue> 
  4.         // problem: how do we return `promise` to the outside ?? 
  5.     } 
  6.     return // ??!! 

就快好了。但我们还有个小问题需要修复:这个promise变量被传递给then的闭包所限制。我们不能将其作为函数的返回值。

我们要使用的技巧是创建一个包装Promise,它将执行我们目前所写的代码,然后在promise变量解决时被同时解决。换句话说,当onResolved方法提供的 promise 被解决并从外部得到一个值,那包装的 promise 也就被解决并得到同样的值。

可能文字有些抽象,但如果我们写成代码,将看得更清楚:

  1. func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { 
  2.     // We have to return a promise, so let's return a new one 
  3.     return Promise<NewValue> { resolve in 
  4.         // this is called immediately as seen in test 0. 
  5.         then { value in // the "observe" one 
  6.             let promise = onResolved(value) // `promise` is a Promise<NewValue> 
  7.             // `promise` has the same type of the Promise wrapper 
  8.             // we can make the wrapper resolves when the `promise` resolves 
  9.             // and gets a value 
  10.             promise.then { value in resolve(value) } 
  11.         } 
  12.     } 

如果我们整理一下代码,我们将有这样两个方法:

  1. // observe 
  2. func then(onResolved: @escaping (Value) -> Void) { 
  3.     callbacks.append(onResolved) 
  4.     triggerCallbacksIfResolved() 
  5.  
  6. // flatMap 
  7. func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { 
  8.     return Promise<NewValue> { resolve in 
  9.         then { value in 
  10.             onResolved(value).then(onResolved: resolve) 
  11.         } 
  12.     } 

最后,测试通过。

  1. • Test 3. Resolution handlers can be chained passed (1 assertions) 

串联多个 value

如果你能给某个类型实现flatMap,那你就能利用flatMap为其实现map。对于我们的Promise来说,map该是什么样子?

我们将使用如下测试:

  1. test(named: "4. Chaining works with non promise return values") { assert, done in 
  2.     let string: String = "foo" 
  3.     let promise = Promise<String> { resolve in 
  4.         after(0.1) { 
  5.             resolve(string) 
  6.         } 
  7.     } 
  8.     promise 
  9.         .then { value -> String in 
  10.             return value + value 
  11.         } 
  12.         .then { value in // the "observe" then 
  13.             assert(string + string == value) 
  14.             done() 
  15.         } 

注意第一个then没有再返回一个Promise,而是将其接收的值做了一个变换。这个新的then就对应于我们想添加的map。

编译器报错说我们必须实现此方法。

  1. error: Promise.playground:174:26: error: declared closure result 'String' is incompatible with contextual type 'Void' 
  2.         .then { value -> String in 
  3.                          ^~~~~~ 
  4.                          Void 

这个方法很接近flatMap,唯一的不同是其参数onResolved函数返回一个NewValue而不是Promise

  1. // map 
  2. func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { 
  3.     // to implement 

之前我们说可以利用flatMap实现map。在我们的情况里,我们看到我们需要返回一个Promise。如果我们使用这个“flatMap”的then,并创建一个promise,再以映射后的 value 来直接解决,我们就搞定了。让我来证明之。

  1. // map 
  2. func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { 
  3.     return then { value in // the "flatMap" defined before 
  4.         // must return a Promise<NewValue> here 
  5.         // this promise directly resolves with the mapped value 
  6.         return Promise<NewValue> { resolve in 
  7.             let newValue = onResolved(value) 
  8.             resolve(newValue) 
  9.         } 
  10.     } 

再一次,测试通过。

  1. • Test 4. Chaining works with non promise return values passed (1 assertions) 

如果我们移除注释,再看看我们做出了什么。我们有三个then方法的实现,能被使用或串联。

  1. // observe 
  2. func then(onResolved: @escaping (Value) -> Void) { 
  3.     callbacks.append(onResolved) 
  4.     triggerCallbacksIfResolved() 
  5.  
  6. // flatMap 
  7. func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> { 
  8.     return Promise<NewValue> { resolve in 
  9.         then { value in 
  10.             onResolved(value).then(onResolved: resolve) 
  11.         } 
  12.     } 
  13.  
  14. // map 
  15. func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> { 
  16.     return then { value in 
  17.         return Promise<NewValue> { resolve in 
  18.             resolve(onResolved(value)) 
  19.         } 
  20.     } 

使用示例

实现告一段落。我们的Promise类已足够完整来展示我们能够用它做什么。

假设我们的app有一些用户,结构如下:

  1. struct User { 
  2.     let id: Int 
  3.     let name: String 

假设我们还有两个方法,一个获取用户id列表,另一个使用id获取某个用户。而且假设我们想显示第一个用户的名字。

通过我们的实现,我们可以这样做,使用之前定义的这三个then。

  1. func fetchIds() -> Promise<[Int]> { 
  2.     ... 
  3.  
  4. func fetchUser(id: Int) -> Promise<User> { 
  5.     ... 
  6.  
  7. fetchIds() 
  8.     .then { ids in // flatMap 
  9.         return fetchUser(id: ids[0]) 
  10.     } 
  11.     .then { user in // map 
  12.         return user.name 
  13.     } 
  14.     .then { name in // observe 
  15.         print(name
  16.     } 

代码变得十分易读、简洁,而且没有嵌套。

结论本文结束,希望你喜欢它。

参考资料

[1]gist:

https://gist.github.com/felginep/039ca3b21e4f0cabb1c06126d9164680

[2]Promises in Swift by Khanlou:

http://khanlou.com/2016/08/promises-in-swift/

[3]JavaScript Promises … In Wicked Detail:

https://www.mattgreer.org/articles/promises-in-wicked-detail/

[4]PromiseKit 6 Release Details:

https://promisekit.org/news/2018/02/PromiseKit-6.0-Released/

[5]TDD Implementation of Promises in JavaScript:

https://www.youtube.com/watch?v=C3kUMPtt4hY

[6]Implementing Promises in Swift:

https://felginep.github.io/2019-01-06/implementing-promises-in-swift

本文转载自微信公众号「Swift社区」

 

责任编辑:姜华 来源: Swift社区
相关推荐

2023-12-21 17:11:21

Containerd管理工具命令行

2023-11-20 08:18:49

Netty服务器

2022-12-20 07:39:46

2023-07-31 08:18:50

Docker参数容器

2021-05-29 10:11:00

Kafa数据业务

2023-11-06 08:16:19

APM系统运维

2022-11-11 19:09:13

架构

2022-07-18 21:53:46

RocketMQ广播消息

2024-10-10 09:12:10

Spring接口初始化

2023-11-08 08:15:48

服务监控Zipkin

2022-02-24 07:34:10

SSL协议加密

2023-10-27 08:15:45

2019-06-13 21:31:19

AI

2023-03-06 21:29:41

mmap技术操作系统

2024-05-22 09:45:49

2022-05-16 10:49:28

网络协议数据

2022-04-08 09:01:14

CSS自定义属性前端

2021-09-13 22:34:56

区块链新基建数字化转型

2020-11-27 09:40:53

Rollup前端代码

2022-02-28 12:07:56

RxJS函数式
点赞
收藏

51CTO技术栈公众号