由一个问题引发的思考-异步编程
解释下,题目好像取的有点大了,异步编程… 文章的内容涉及一个问题和 PromiseKit 的源码分析。
由一个问题引发的思考。我们在操作一系列的网络请求并且这些网络请求之前存在依赖关系。比如:
- 用户登陆完之后服务端返回 token
- 获取用户的联系人的列表(需要 token 认证)
以上两个网络请求中间就涉及到 token 衔接,我们很容易想到这个问题,通过一次嵌套就能完成。
1 | APICilent.login(url: "https://xxxx/api/login", params: paramDict, compelationHandler: {result, reponse, error in |
此时我们只有两个依赖请求,如果这样的请求很多个的话,我们就陷入了回调地狱(也就是出现大量的 }
这样的符号)在 else 中无法看清是哪个对应关系。这样的代码既不美观也很难维护。这个问题在最早发生在 JavaScript,所以我们有时看到原生的 JavaScript 代码中就含有大量的这样的 ‘又臭又长’ 的代码。
但是在 Cocoa 框架中好像没有很好的解决方法。直到有一天,我看到 ES6 的新特性 Promise。
Promise 是异步编程的一种解决方案。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果
Promise 对象有两个特点:
- 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态,只有异步操作可以决定哪种状态,其他操作无法改变这个状态。
- Pending 进行中
- Resolve 已完成,又称 Fulfill
- Rejected 已失败
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变只有两种可能:
- Pending 变为 Resolved
- Pending变为 Rejected
then 方法
Promise 对象的一个方法 then
它为 Promise 添加一个回调函数,then
方法返回一个新的 Promise 实例,因此可以采用链式写法。
1 | getJSON("/post/1.json").then(function(post) { |
catch 方法
catch 方法是 then 方法的一个别名,用于发生错误时的回调函数,如果异步操作抛出异常,状态就会变成 Rejected,就会调用 catch 方法。
1 | getJSON("/posts.json").then(function(posts) { |
以上是简单的介绍了一下 Promise,在 OC 和 Swift 中也有类似的第三方库 —— PromiseKit。基本的思想与 Promise 相似,也有 Pending
、Resolved
、Rejected
三个状态,同样有 then
、catch
等等,但是在 PromiseKit 框架中还有 always
方法(在一次请求中总是执行的方法)。
接下来我们看看使用 PromiseKit 是如何优化多个依赖请求的(这里的优化指的是在代码结构上上的优化,并非性能上的优化)。
1 | let url = URLRequest(url: URL(string: "https://httpbin.org/ip")!) |
从上面的代码来看,回调地狱就解决了,没有那么多的 }
,并且代码的结构和可读性也增强了。PromiseKit 在各个异步操作间传递的是 Promise 对象,对应着各个层级的异步操作方法 then
。我们来简单分析下源码。
源码分析
1 | // 代码-1 |
PromiseKit 框架的中的主要类:
- Promise
- State
State 还有两个子类 SealedState 和 UnealedState。
我们在使用该框架前,需要去封装一些方法,比如 Alamofire 网络库,我们就需要做以下的封装。
1 | // 代码-2 |
responseJSON()
是 Alamofire 请求的结果回调函数,这里我们扩展这些回调函数,并返回一个 Promise<Any>
。方法的实现中实例化一个 Promise 对象,fulfill
和 reject
请求成功回调、失败回调。然后根据请求的结果来调用相应的闭包。这里 case .success(let value)
作为模式匹配来取得请求成功的数据;相应的, case .failure(let error)
获取错误的信息。
1 | // 代码-3 |
从函数签名来看是一个构造函数,创建一个 pending 状态的 Promise 对象, fulfill
和 reject
一个元组作为参数,其中初始化了一个成员变量 state
,UnsealedState(resolver:)
传递一个 inout 类型也就是引用类型变量,resolve
闭包是将任务结果传递给 state
变量,来处理任务状态的变化。这一步非常重要,也就是我们之前所说的 Promise 的状态转移(pending 转变到 fulfill 或者 reject,后边我们再讲述 state
中的代码)。这里的 .fulfilled($0)
也就是我们 代码-2 中 value 值。将 value 值也就是成功的数据做一层包装,说明该值处于成功状态,并且成功状态的数据是 $0 。第二个回调是失败的状态,如果现在 Promise 的状态是是 Pending 说明任务还没解决,如果进入这个判断里,说明任务失败,比如网络请求失败。
1 | // 代码-4 |
这个类就是 代码-3 中那个关键的一步 UnsealedState(resolver:)
参数是一个 inout 类型。该类的成员变量 seal 和 barrier,分别是表示 Promise 未处理的状态和表示一个 GCD 调度队列。此时这里的 resolution 变量也就是 代码-3 中的 .fulfilled($0)
,handlers 保存着还未处理的任务,再以同步的方式处理状态的变化,在 seal 中获取 .pending
状态的任务。然后将结果 resolution 传递给 handler。
1 | // 代码-5 |
这个 then
方法是 代码-1 中的 then,同样的该方法也是返回一个 Promise 对象,来处理已解决的任务。
Parameter body: The closure that is executed when this Promise is fulfilled
更加具体一些是处理成功回调,通过 state 对象的 then
方法,来进一步处理任务。
1 | // 代码-6 |
类似的初始化一个 Promise 对象,类似 代码-3 这里就是不做详细的介绍了,
1 | // 代码-7 |
代码-5 中 state 调用的 then 方法,pipe 方法参数闭包中的 contain_zalgo
方法是 PromiseKit 中的最深层的方法。pipe 方法中又调用子类 UnsealedState<T>
的 get 方法,同时它将闭包传递出去,再在 get 方法中判断任务是否解决,已经解决,则闭包回调 seal 至 pipe 方法,此时 seal 处于 pending 状态,则加入到 handlers 中。如果以已经解决,将 pipe 方法参数中的 body 回调至 then 方法,判断任务解决的结果成功还是失败,再做相应的处理。
1 | // 代码-8 |
上面两个 enum 是对每个异步操作一层包装,说明每个操作当前所处的状态,也就是我们上面所提到的三种状态。通过对任务或者任务结果的包装,再通过类似 case .fulfilled(let value)
匹配来获取的正处于某种状态的数据。
1 | // 代码-9 |
Handlers 来保存还未解决的任务。并且该任务是 (Resolution<T>) -> Void
这样的闭包。
小结
Alamofire.request(url).responseJSON()
这个部分通过封装网络库 Alamofire 的响应方法 responseJSON(),回调到 UnsealedState 类中,像 Alamofire.request(url).responseJSON().then
,在后面链式调用一个 then 方法,则会在 handers 中增加一个未处理的任务闭包,等网络请求返回时。通过 case .pending(let hh) = self.seal
筛选出未处理的闭包,然后处理,这部分处理其实就是将结果分发到各自的闭包。这样一个 Promise 就结束了。
我们发现 PromiseKit 的 swift 实现使用了大量的闭包,导致源码阅读起来比较绕。所以定期的多读几遍收获还是很多的。但是对于闭包来讲,如果语义上合适,尾随闭包会增加代码的可读性;反之,反而让代码晦涩难懂。
总结
对于 PromiseKit 我们会发现这个库和 RxSwift 很类似。他们都是基于函数式编程思想。它们同样是对异步操作的管理。对于语义来说,也许 PromiseKit 更好理解一些。 比如
- then 方法处理接下里做什么
- always 方法来处理一个总是要操作的步骤,比如:网络请求的 loading 动画
- when 当几个 promise 都获取到结果时,做什么?
- catch 方法处理 error
对于 RxSwift 而言 map
、flatMap
等等,概念和理解都要比 PromiseKit 要辛苦一点。但是 PromiseKit 的缺点也很明显,需要自己去实现相应的 category,比如上面 Alamfire 的网络请求,就需要我们再封装一层才能使用。
参考
github-PromiseKit
iOS如何优雅的处理“回调地狱Callback hell”
Swift 并行编程现状和展望
写在最后
希望对你有帮助,写的不正确的地方,欢迎拍砖~