PromiseKit入门指南

使用PromiseKit有一段时间了,但是一直没领悟到其精髓,所以打算把它的英文文档翻译一边,加深一边对它的理解。这个是第一篇。

开始使用

then 与 done

以下是一个经典的promise链:

firstly {
    login()
}.then { creds in
    fetch(avatar: creds.user)
}.done { image in
    self.imageView = image
}

如果这段代码使用闭包回调的话,会长成这个样子:

login { creds, error in
    if let creds = creds {
        fetch(avatar: creds.user) { image, error in
            if let image = image {
                self.imageView = image
            }
        }
    }
}

then 函数是另外一种构建闭包回调的方法,但这种做法要比之前的completion handlers方法好很多。它主要帮助我们更好的阅读这段代码。上面的promise链是非常容易阅读和理解的:一个异步操作引出另一个异步操作。这段伪代码与程序代码非常接近,我们很容易理解这段Swift代码的当前含义。

done 函数和 then含义是一样的,但是done没有返回一个承诺链。它常常被用于“成功链”的结尾。以上,我们可以看到在done函数中接受了一个最终的image对象,并且被用来初始化我们的UI操作。

让我们来比对下两种方法:

func login()->Promise<Creds>

//对比:

func login(completion:(Creds?,Error?)->Void) //额。这边有两个可选值

这个两者区别在于使用了前者使用了Promise包装了Creds对象,你的方法返回了Promise对象而不是一个回调函数。每一个在响应链中的回调函数都会返回一个Promise对象。Promise对象中定义then方法,该方法在继续链之前等待前一个Promise任务的完成。Promise任务链会以程序化的方式解决问题,一次完成一个任务。

一个Promise代表了一个未来的异步任务。它用泛型语法包装了一个对象的真实类型。比如,在以上的例子中,login是一个方法返回了一个Promise对象,但是他的真实类型是一个Creds的实例。

注意done是在PromiseKit 5中新特性。我们之前定义了then的变体,没有要求你返回一个Promise对象。不幸的是,这种惯例经常混淆Swift使用者,并且导致了奇怪且难以调试的错误消息。它使用使用PromiseKit是一件痛苦的事情。done的引入让你在使用了promise链的时候,编译器可以很便捷的给类型提示信息。

您可能会注意到与回调函数模式不同的是,promise链模式似乎忽略了错误。 不是这种情况! 事实上,与此相反:promise链模式的错误处理更加容易处理,更加难以忽略。

catch

使用promise链模式,错误会沿着链逐级传递,确保你的应用程序代码正确健壮,代码逻辑清晰明确:

firstly{
        login()
}.then{ creds in
        featch(avatar:creds.user)
}.done{ image in
        self.imageView = image  
}.catch{
        //整个promise链的任何错误都归于此
}

如果你忘了“抓住”链条,swift会发出警告。 但我们稍后会详细讨论这个问题。

每个promise链都是一个表示单个异步任务。如果任务执行失败,其promise将被rejected。 包含被拒绝promise链将跳过所有后续then任务。 将执行下一个catch任务。(严格地说,所有后续catch回调都被执行。)

为了好玩,让我们将此模式与闭包模式(completion handler模式)进行比较:

func handle(error: Error) {
    //…
}

login { creds, error in
    guard let creds = creds else { return handle(error: error!) }
    fetch(avatar: creds.user) { image, error in
        guard let image = image else { return handle(error: error!) }
        self.imageView.image = image
    }
}

这段代码使用guard和一个统一的错误处理,但是promise链的可读性说明了一切。

ensure

我们已经学会了异步组合。接下来让我们扩展更多的操作函数:

firstly {
    UIApplication.shared.isNetworkActivityIndicatorVisible = true
    return login()
}.then {
    fetch(avatar: $0.user)
}.done {
    self.imageView = $0
}.ensure {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
}.catch {
    //…
}

不论你的promise链的输出是什么--成功或者失败--你的ensure回调是永远会被执行的。

我们将此模式与它的闭包回调模式进行比对:

UIApplication.shared.isNetworkActivityIndicatorVisible = true

func handle(error: Error) {
    UIApplication.shared.isNetworkActivityIndicatorVisible = false
    //…
}

login { creds, error in
    guard let creds = creds else { return handle(error: error!) }
    fetch(avatar: creds.user) { image, error in
        guard let image = image else { return handle(error: error!) }
        self.imageView.image = image
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
    }
}

对于有些人来说,修改这段代码或者取消设置活动指示器,非常容易导致出现bug。使用promise链模式,这类错误几乎不可能会发生:在不使用改模式的情况下,Swift编译器不会给你编译提示。通过使用这种模式,您几乎不需要查看提交的代码。

提示:PromiseKit为了这个函数可能在名字alwaysensure之间反复无常地切换。 为此表示歉意。 我们做的很糟糕。

你还可以使用finally作为一种ensure,用于终止promise链并且没有返回值:

spinner(visible: true)

firstly {
    foo()
}.done {
    //…
}.catch {
    //…
}.finally {
    self.spinner(visible: false)
}

when

使用闭包回调模式,对多个异步操作做出反应不是写的很慢就是很难写出优雅的代码。

operation1 { result1 in
    operation2 { result2 in
        finish(result1, result2)
    }
}

这种编码方式会使得代码目的不够清晰:

var result1: …!
var result2: …!
let group = DispatchGroup()
group.enter()
operation1 {
    result1 = $0
    group.leave()
}
operation2 {
    result2 = $0
    group.leave()
}
group.notify(queue: .main) {
    finish(result1, result2)
}

使用Promise模式会变得更加简单:

firstly {
    when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
    //…
}

when接受Promises对象,等待他们解决,并返回包含结果的Promise对象。

与任何Promise链一样,如果任何Promise任务失败了,该链将调用下一个catch任务回调。

PromiseKit 扩展工具包

当我们制作PromiseKit工具包时,我们知道我们只想使用Promise来实现异步行为。因此,只要有可能,我们就会为苹果的API提供扩展,根据Promise重新构建API。例如:

firstly {
    CLLocationManager.promise()
}.then { location in
    CLGeocoder.reverseGeocode(location)
}.done { placemarks in
    self.placemark.text = "\(placemarks.first)"
}

要使用这些扩展,你需要安装如下pod库:

pod "PromiseKit"
pod "PromiseKit/CoreLocation"
pod "PromiseKit/MapKit"

所有这些扩展都可以在PromiseKit组织上找到。去那里看看有什么可用的,并阅读源代码和文档。每个文件和函数已被大量记录在案。

我们还为Alamofire等公共库提供扩展。

创建 Promises

标准扩展会让您走很长的路,但有时您仍然需要创建自己的Promise链。如果你使用额第三方库没有提供Promise扩展,或者你已经写好了自己的异步程序。不管怎样,添加Promise都很容易。如果你看一下标准扩展库,您将看到它使用下面描述的相同方法。

假设我们有以下方法:

func fetch(completion: (String?, Error?) -> Void)

我们怎么才能转换成一个Promise呢,这很简单:

func fetch() -> Promise<String> {
    return Promise { fetch(completion: $0.resolve) }
}

您可能会发现扩展版本更具可读性:

func fetch() -> Promise<String> {
    return Promise { seal in
        fetch { result, error in
            seal.resolve(result, error)
        }
    }
}

这个seal对象是“Promise”初始化器提供的,用于定义很多方法来处理多重闭包回调的问题的。它甚至可以用来处理各种罕见的情况,从而使您很容易对现有代码库的添加Promise扩展。

注意:我们试图让它只做Promise(fetch),但是我们不能让这个更简单的模式在不需要额外消除Swift编译器歧义的情况下普遍工作。对不起,我们尝试过但没有成功。

注意:在PromiseKit 4中,这个初始化器为闭包提供了两个参数:fulfillreject。PromiseKit 5和6为您提供了一个对象,该对象具有fulfillreject方法,但也有方法解析的许多变体。通常,您只需传递要解析的回调函数参数给resolve,并让Swift找出应用于特定情况的变体(如上面的示例所示)。

注意:Guarantee(下面)是一个稍微不同的初始化器(因为它们不能出错),所以初始化器闭包的参数只是一个闭包。不是Resolver对象。因此,应该seal(value)而不是seal.fulfill(value)。这是因为没有什么变化在Guarantee中是未知的,它们只能fulfill

Guarantee<T>

从Promisekit 5开始,我们就提供了Guarantee作为Promise的补充类。我们这样做是为了补充Swift强大的错误处理系统。

Guarantee永远不会失败,所以它们不能被reject。一个很好的例子是after:

firstly {
    after(seconds: 0.1)
}.done {
    // 没有办法添加“catch”,因为after不能失败。
}

如果你不终止一个常规的Promise链,Swift会对你做出编译警告。(不是一个Guarantee链)。您应该通过提供一个catch或一个return来消除这个警告。(在后一种情况下,你可以获得得到的promise链。)

尽可能使用Guarantee,以便代码在需要的地方有错误处理,在不需要的地方没有错误处理。

一般来说,您应该能够交替使用GuaranteePromise,我们已经尽了最大的努力来确保这一点,所以如果您发现任何问题请及时给我们提issue。

如果您正在创建自己的Guarantee,那么语法将比Promise更简单

func fetch() -> Promise<String> {
    return Guarantee { seal in
        fetch { result in
            seal(result)
        }
    }
}

可以归结为:

func fetch() -> Promise<String> {
    return Guarantee(resolver: fetch)
}

map, compactMap等等

then向您提供前一个承诺的结果,并要求您返回另一个承诺。
map提供了前面承诺的结果,并要求您返回一个引用类型或值类型。
compactMap提供了前面承诺的结果,并要求您返回一个可选的。如果返回nil,则该链将会返回PMKError.compactMap错误。

理由:在PromiseKit 4之前,then会处理所有这些情况,这是非常糟糕的设计。我们希望这些痛苦会随着新版本的Swift而逐步消失。然而,很明显,各种各样的痛点都会存在。事实上,作为库的作者,我们应该在API的命名级别消除歧义。因此,我们将当时的三种主要类型分为thenmapdone。在使用了这些新函数之后,我们意识到这在实践中要好得多,所以我们也添加了compactMap(以Optional.compactMap为模型)。

compactMap有助于快速组合承诺链。例如:

firstly {
    URLSession.shared.dataTask(.promise, with: rq)
}.compactMap {
    try JSONSerialization.jsonObject($0.data) as? [String]
}.done { arrayOfStrings in
    //…
}.catch { error in
    // Foundation.JSONError if JSON was badly formed
    // PMKError.compactMap if JSON was of different type
}

提示:我们还为序列提供了您期望的大多数函数方法,例如mapthenMapcompactMapValuesfirstValue等。

get

我们提供了get函数,类似done,主要用于获取返回值。

firstly {
    foo()
}.get { foo in
    //…
}.done { foo in
    // same foo!
}

tap

我们提供tap用于debug。它与get类似,但是提供了PromiseResult<T>,因此您可以检查此时promise链中的值,而不会产生任何副作用:

firstly {
    foo()
}.tap {
    print($0)
}.done {
    //…
}.catch {
    //…
}

补充

firstly

我们在这一页上已经使用firstly函数好几次了,但是它到底是什么呢?事实上,它只是个语法糖。您并不真正需要它,但它有助于使您的promise链更具可读性。


firstly {
    login()
}.then { creds in
    //…
}

您也可以这样做:

login().then { creds in
    //…
}

这里有一个关键的理解:login()函数返回一个Promise,所有Promise都有一个then函数。firstly返回一个Promise,然后then也返回一个Promise!但是不要太担心这些细节。从学习这种模式开始。然后,当您准备好前进时,继续学习底层架构。

when的变体

when是Promisekit中很有用的几个函数之一,因此我们提供了几个变体。

  • 默认的whenwhen(fulfilled:),您通常应该使用它。这种变体
    等待他所有承诺组件完成,但如果有任何一个承诺失败了,when也失败,因此promise链将会被“拒绝”继续执行。需要注意的是,所有的promise都在when中继续执行。promise无法控制它们所代表的任务。promise只是任务的分装。

  • when(resolved:) 将会继续等待,即使它的一个或多个承诺组件失败了。when的这种变体产生的值是一个Result<T>的数组。因此,该变体要求其所有组件承诺具有相同的泛型类型。有关此限制,请参阅我们的高级模式指南。

  • race变体允许您竞逐多个承诺。无论谁先完成都是结果。有关典型用法,请参阅高级模式指南。

Swift 闭包的用法

Swift自动推断单行闭包的返回和返回类型。以下两种形式是相同的:

foo.then {
    bar($0)
}

// is the same as:

foo.then { baz -> Promise<String> in
    return bar(baz)
}

我们的文档经常为了清晰而省略返回值。

然而,这种简写既是福也是祸。您可能会发现,Swift编译器经常无法正确推断返回类型。如果您需要进一步的帮助,请参阅我们的故障排除指南。

PromiseKit 5中添加done函数,我们成功地避免了在使用PromiseKit和Swift过程中的许多常见痛点。

延伸阅读

以上信息是在使用PromiseKit中的90%了。我们强烈建议阅读API指南。有许多简短的函数可能对您有帮助,上面所有的内容在源代码的中的概述都更加全面。

在Xcode中编码时,单击PromiseKit函数来访问该文档。

这里是一些最近的文章,文档基于PromiseKit 5+:

小心一些网上的参考文章,他们中的许多人提到PromiseKit版本都小于5,这里面有些API是不相同的(抱歉,但Swift多年来已经改变了很多,因此我们也不得不这么做)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容