Promise

一个 Swift 的 Promise 库,部分基于 Javascript 的 A+ 规范

什么是 Promise?

Promise 是一种表示未来将会存在(或将因错误而失败)的值的方法。这类似于 Optional 如何表示可能存在也可能不存在的值。

使用特殊类型来表示将来会存在的值意味着这些值可以以系统化的方式组合、转换和构建。如果系统知道成功和失败的样子,那么组合这些异步操作会变得容易得多。例如,编写可重用代码变得轻而易举,这些代码可以:

Promise 适用于任何只能成功或失败一次的异步操作,例如 HTTP 请求。如果存在可以“成功”多次或随时间传递一系列值而不是仅仅一个值的异步操作,请查看 SignalsObservables

基本用法

要访问到达后的值,你可以使用一个代码块调用 then 方法。

let usersPromise = fetchUsers() // Promise<[User]>
usersPromise.then({ users in
    self.users = users
})

users Promise 中的所有数据使用都通过 then 方法进行门控。

除了执行副作用(例如设置 self 上的 users 实例变量)之外,then 还可以让你做另外两件事。首先,你可以转换 Promise 的内容,其次,你可以启动另一个 Promise,以进行更多异步工作。要执行这些操作中的任何一个,请从传递给 then 的代码块中返回某些内容。每次调用 then 时,现有的 Promise 将返回一个新的 Promise。

let usersPromise = fetchUsers() // Promise<[User]>
let firstUserPromise = usersPromise.then({ users in // Promise<User>
    return users[0]
})
let followersPromise = firstUserPromise.then({ firstUser in //Promise<[Follower]>
    return fetchFollowers(of: firstUser)
})
followersPromise.then({ followers in
    self.followers = followers
})

基于你返回的是常规值还是 Promise,Promise 将确定是转换内部内容,还是触发下一个 Promise 并等待其结果。

只要你传递给 then 的代码块只有一行,它的类型签名将被推断出来,这将使 Promise 更易于阅读和使用。

由于每次调用 then 都会返回一个新的 Promise,因此你可以将它们写成一个大链条。上面的代码,作为一个链条,可以写成:

fetchUsers()
    .then({ users in
        return users[0]
    })
    .then({ firstUser in
        return fetchFollowers(of: firstUser)
    })
    .then({ followers in
        self.followers = followers
    })

要捕获沿途创建的任何错误,你还可以添加一个 catch 代码块:

fetchUsers()
    .then({ users in
        return users[0]
    })
    .then({ firstUser in
        return fetchFollowers(of: firstUser)
    })
    .then({ followers in
        self.followers = followers
    })
    .catch({ error in
        displayError(error)
    })

如果链中的任何步骤失败,则不会执行更多的 then 代码块。只会执行失败代码块。这也在类型系统中强制执行。如果 fetchUsers() promise 失败(例如,由于缺少互联网),则 promise 无法为 users 变量构造有效值,并且无法调用该代码块。

创建 Promise

要创建一个 promise,有一个方便的初始化程序,它接受一个代码块并提供 fulfillreject promise 的函数:

let promise = Promise<Data>(work: { fulfill, reject in
    try fulfill(Data(contentsOf: someURL)
})

它将自动在全局后台线程上运行。

你可以使用此初始化程序来包装基于完成块的 API,例如 URLSession

let promise = Promise<(Data, HTTPURLResponse)>(work: { fulfill, reject in
    self.dataTask(with: request, completionHandler: { data, response, error in
        if let error = error {
            reject(error)
        } else if let data = data, let response = response as? HTTPURLResponse {
            fulfill((data, response))
        } else {
            fatalError("Something has gone horribly wrong.")
        }
    }).resume()
})

如果你包装的 API 对其运行的线程敏感,例如任何 UIKit 代码,请确保向 work: 初始化程序添加一个 queue: .main 参数,它将在主队列上执行。

对于基于委托的 API,你可以使用默认初始化程序在 .pending 状态下创建一个 promise。

let promise = Promise()

并使用 fulfillreject 实例方法来更改其状态。

高级用法

由于 promise 形式化了成功和失败代码块的外观,因此可以在其之上构建行为。

always

例如,如果你想在 promise 完成时执行代码 - 无论它成功还是失败 - 你可以使用 always

activityIndicator.startAnimating()
fetchUsers()
    .then({ users in
        self.users = users
    })
    .always({
        self.activityIndicator.stopAnimating()
    })

即使网络请求失败,活动指示器也会停止。请注意,你传递给 always 的代码块没有参数。因为 Promise 不知道它会成功还是失败,所以它不会给你一个值或一个错误。

ensure

ensure 是一个采用谓词的方法,如果该谓词失败,则拒绝 promise 链。

URLSession.shared.dataTask(with: request)
    .ensure({ data, httpResponse in
        return httpResponse.statusCode == 200
    })
    .then({ data, httpResponse in
        // the statusCode is valid
    })
    .catch({ error in 
        // the network request failed, or the status code was invalid
    })

静态方法

静态方法,例如 zipraceretryallkickoff 存在于名为 Promises 的命名空间中。以前,它们是 Promise 类中的静态函数,但这意味着你必须在使用它们之前使用泛型来专门化它们,例如 Promise<()>.all。这很丑陋且难以键入,因此从 v2.0 开始,你现在可以编写 Promises.all

all

Promises.all 是一个静态方法,它等待你给它的所有 promise 完成,一旦它们完成,它就会用所有已完成值的数组来完成自身。例如,你可能想要编写代码来为数组中的每个项目点击一次 API 端点。mapPromises.all 使之非常容易

let userPromises = users.map({ user in
    APIClient.followUser(user)
})
Promises.all(userPromises)
    .then({
        //all the users are now followed!
    })
    .catch  ({ error in
        //one of the API requests failed
    })

kickoff

因为 promise 的 then 代码块对于 throw 的函数来说是一个安全的空间,所以即使你没有异步工作要做,有时进入这些安全空间也是有用的。Promises.kickoff 就是为此而设计的。

Promises
	.kickoff({
		return try initialValueFromThrowingFunction()
	})
	.then({ value in
		//use the value from the throwing function
	})
	.catch({ error in
		//if there was an error, you can handle it here.
	})

当你想要从可选值启动 promise 链时,这(与 Optional.unwrap() 结合使用)特别有用。

其他行为

这些是一些最有用的行为,但也有其他行为,例如 race(它竞争多个 promise)、retry(它允许你多次重试单个 promise)和 recover(它允许你返回一个新的 Promise 给出一个错误,允许你从失败中恢复)等等。

你可以在 Promises+Extras.swift 文件中找到这些行为。

Invalidatable Queues (可失效队列)

Promise 上接受代码块的每个方法都接受一个执行上下文,其参数名称为 on:。通常,此执行上下文是一个队列。

Promise<Void>(queue: .main, work: { fulfill, reject in
    viewController.present(viewControllerToPresent, animated: flag, completion: {
        fulfill()
    })
}).then(on: DispatchQueue.global(), {
	return try Data(contentsOf: someURL)
})

因为 ExecutionContext 是一个协议,所以可以在此处传递其他内容。一个特别有用的是 InvalidatableQueue。当使用表格单元格时,通常需要忽略 promise 的结果。为此,每个单元格都可以保留一个 InvalidatableQueueInvalidatableQueue 是一个可以失效的执行上下文。如果上下文失效,则传递给它的代码块将被丢弃并且不会执行。

为了在表格单元格中使用它,应该在 prepareForReuse() 上失效并重置队列。

class SomeTableViewCell: UITableViewCell {
    var invalidatableQueue = InvalidatableQueue()
        
    func showImage(at url: URL) {
        ImageFetcher(url)
            .fetch()
            .then(on: invalidatableQueue, { image in
                self.imageView.image = image
            })
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        invalidatableQueue.invalidate()
        invalidatableQueue = InvalidatableQueue()
    }

}

警告:不要从在可失效队列上执行的任何内容链接代码块。返回 Voidthen 代码块不会停止链,但返回值或 promise 的 then 代码块停止链。因为无法执行代码块,所以链中下一个值的结果将无法计算,并且下一个 promise 将永远保持 pending 状态,从而阻止资源被释放。

易用性

在编写这个 Promise 库时,我做了一些设计决策,倾向于使该库尽可能易于使用。

简化命名

其他 promise 库对 then 使用函数式名称,例如 mapflatMap。使用这些单子函数术语的好处很小,但从理解的角度来看,成本很高。在这个库中,你调用 then,并返回你需要的一切,库会弄清楚如何处理它。

错误参数化

其他 promise 库允许你定义每个 promise 将返回的错误类型。从理论上讲,这是一个有用的功能,允许你知道 catch 代码块中的错误类型。

在实践中,这是令人窒息的。在实践中,如果你使用来自两个不同域的错误,你必须 a) 使用最低公分母错误,例如 NSError,或者 b) 调用一个像 mapError 这样的方法来将错误从一个域转换为另一个域。

另请注意,Swift 的内置错误处理系统没有类型化的错误,而是选择模式匹配。

抛出

最后,你可以在所有代码块中使用 trythrow,并且该库会自动将其转换为 promise 拒绝。这使得使用抛出的 API 更加容易。为了扩展我们的 URLSession 示例,我们可以轻松地使用抛出的 JSONSerialization API。

URLSession.shared.dataTask(with: request)
    .ensure({ data, httpResponse in httpResponse.statusCode == 200 })
    .then({ data, httpResponse in
        return try JSONSerialization.jsonObject(with: data)
    })
    .then({ json in
        // use the json
    })

使用可选类型可以通过一个小扩展来简化。

struct NilError: Error { }

extension Optional {
    func unwrap() throws -> Wrapped {
        guard let result = self else {
            throw NilError()
        }
        return result
    }
}

因为你处于一个可以自由抛出并且可以为你处理(以拒绝 Promise 的形式)的环境中,所以你现在可以轻松地解包可选类型。例如,如果你需要从 json 字典中获取一个特定的键

.then({ json in
    return try (json["user"] as? [String: Any]).unwrap()
})

你会将你的可选类型转换为非可选类型。

线程模型

这个库的线程模型非常简单。 init(work:) 默认发生在后台队列中,而其他基于代码块的方法(thencatchalways 等)在主线程上执行。这些可以通过为第一个参数传入一个 DispatchQueue 对象来覆盖。

Promise<Void>(work: { fulfill, reject in
    viewController.present(viewControllerToPresent, animated: flag, completion: {
        fulfill()
    })
}).then(on: DispatchQueue.global(), {
	return try Data(contentsOf: someURL)
}).then(on: DispatchQueue.main, {
	self.data = data
})

安装

CocoaPods

  1. 将以下内容添加到你的 Podfile 中:

    pod 'Promises'
  2. 使用框架集成你的依赖项:将 use_frameworks! 添加到你的 Podfile 中。

  3. 运行 pod install

玩玩看

要开始使用这个库,你可以使用包含的 Promise.playground。只需打开 .xcodeproj,构建 scheme,然后打开 playground(在项目内)并开始玩耍。