版本 2.0.4

Promises 2.0.4

测试已测试
语言语言 Obj-CObjective C
许可证 MIT
发布最后发布2019年6月

Josh HoltzSoroush Khanlou维护。



Promises 2.0.4

Promise

Swift的Promise库,部分基于JavaScript的A+规范

什么是Promise?

Promise是一种在将来某个时间点将存在(或者失败并报错)的值的表示方式。这与如何用Optional表示一个可能或可能不存在的值相似。

使用特殊类型来表示将存在的values意味着,这些值可以被组合,以系统化的方式进行转换和构建。如果系统知道成功和失败是什么样子,构建那些异步操作就会变得容易得多。例如,它可以非常容易地编写可重用的代码,该代码可以

  • 执行一系列依赖于对方的异步操作,并在最后一处完成
  • 同时执行许多独立的异步操作,并使用一个完成块
  • 比较许多异步操作,并返回最先完成的值
  • 重试异步操作
  • 给异步操作添加超时

Promises非常适合任何成功或失败只能发生一次的异步操作,如HTTP请求。如果有一个异步操作可能会"成功"多次,或者随着时间的推移传递一系列的值而不是单个值,那么可以看一下SignalsObservables

基本用法

当值到达后,要访问该值,您需要使用一个代码块调用 then 方法。

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

users Promise 中对数据的所有使用都通过 then 方法控制。

除了执行副作用(如将 users 实例变量设置在 self 上)之外,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 失败(例如,由于缺少互联网),则没有方法为 users 变量构造一个有效的值,并且该块不会被执行。

创建 Promises

要创建一个 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 参数的 queue:,它将在主队列上执行。

对于基于代理的 API,您可以使用默认的初始化器创建处于 .pending 状态的 Promise。

let promise = Promise()

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

高级使用

由于 Promise 将成功和失败块的格式正式化,因此可以在其上构建行为。

总是

例如,如果你想在承诺成功或失败时执行代码,可以使用 总是

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

即使网络请求失败,活动指示器也会停止。请注意,传递给 总是 的代码块没有参数。因为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 是一个静态方法,它等待您给出的所有承诺都满足,一旦它们都满足了,它就使用所有满足值的数组自行满足。例如,您可能想为数组中的每个项目编写一次访问API端点的代码。`map` 和 `Promises.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块是函数可以抛出异常的安全空间,即使没有异步工作要做,有时候进入这些安全空间也是有用的。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.
	})

这(结合Optional.unwrap())在你想从一个可选值启动promise链时尤其有用。

其他行为

这些都是最有用的行为,但也有其他行为,如race(与多个promise竞争),retry(允许你多次重试单个promise)和recover(允许你在一个错误的情况下返回一个新的Promise,允许你从失败中恢复),以及其他。

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

可验证队列

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的结果。为此,每个单元格可以保留一个InvalidatableQueue。一个InvalidatableQueue是一个可以被无效化的执行上下文。如果上下文被无效化,那么传递给它的块将被丢弃而不执行。

为了与表单元格一起使用,队列应在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()
    }

}

警告:不要在正在在可验证队列上执行的东西上链式处理块。then块返回)不会停止链式处理,但返回值或promise的then块会停止链式处理。由于块无法执行,链中下一个值的计算结果将不可知,下一个promise将永久处于pending状态,阻止释放资源。

易于使用

在编写这个Promise库时,我做出了几个设计决策,旨在使他们尽可能容易使用。

简化命名

其他诺言库使用函数名称来命名then,例如mapflatMap。使用这些单子功能术语的好处很小,但在理解上的成本却很高。在这个库中,您调用then,并返回您需要的任何内容,该库会找出如何处理它。

错误参数化

其他诺言库允许您定义每个诺言将返回的错误类型。从理论上讲,这是一个有用的特性,允许您知道错误将是什么类型,在catch块中。

在实践中,这将变得很压制。在实践中,如果您使用来自两个不同领域的错误,您必须使用以下选项之一:a)使用最低公共分母的错误,如NSError,或 b)调用如mapError的函数将错误从一个领域转换为另一个领域。

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

抛出

最后,您可以在所有块中使用trythrow,并且库会自动将其转换为诺言拒绝。这使得处理抛出错误的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
    }
}

因为您在一个可以自由抛出且将为您处理(以拒绝的形式呈现)的环境中,您现在可以轻松地展开可选的。例如,如果您需要从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。简单地在Xcode中打开.xcodeproj,构建方案,然后从项目中打开playground(开始玩耍)。