一、前言
- 異步編程是開發 App 的一個難點,多執行緒操作,執行緒的切換,多層嵌套 callback,使得代碼十分的混亂,難以理解,
func load(_ callback: (Value) -> Void) {
loadService.load { result in
let saveableItem = result.transfromToSaveableItem
saveQueue.async {
saveService.save(saveableItem) { saved in
let displayableItem = saved.tranfromToDisplayableItem
mainQueue.async {
callback(displayableItem)
}
}
}
}
}
- Github 有很多直接或者間接能解決這個問題的方案,比如 RxSwift 等函式回應式編程框架,通過 observeOn,可以輕松的切換執行緒,并且使用 map,flatmap 等運算子鏈式的操作結果,可以讓我們輕松的寫出結構清晰的代碼,具體可以參考我的專欄:RAC+RxSwift,
- Promise 是一種鏈接異步任務的方式,有三大要素:
-
- 保存當前狀態的 State;
-
- 保存回呼函式的 Callbacks;
-
- 控制執行緒安全的方式,
- 通常來說,異步任務會在異步操作完成時執行回呼閉包(有時候要準備兩個閉包,一個代表成功,一個代表失敗),要執行多個異步操作,必須將第二個異步操作放在第一個異步操作的完成閉包中執行:
APIClient.fetchCurrentUser(success: { currentUser in
APIClient.fetchFollowers(user: currentUser, success: { followers in
// 得到一個 followers 陣列
}, failure: { error in
// 錯誤處理
})
}, failure: { error in
// 錯誤處理
})
- Promise 的作用是格式化完成閉包,簡化鏈式異步呼叫的形式,如果系統能夠分辨成功和失敗,那么組合這些異步操作就變得容易很多,比如,撰寫具有下列功能的可重用代碼:
-
- 使用尾閉包執行一系列依賴關系的異步操作;
-
- 通過一個完成閉包同時執行多個獨立的異步操作;
-
- 多個異步操作競爭,回傳第一個完成的值;
-
- 重試異步操作;
-
- 為異步操作設定超時時間,
- 如上代碼轉換為 Promise 樣式如下:
APIClient.fetchCurrentUser().then({ currentUser in
return APIClient.fetchFollowers(user: currentUser)
}).then({ followers in
// you now have an array of followers
)}.onFailure({ error in
// hooray, a single failure block!
})
- 可能你也注意到了,Promise 是將嵌套/縮進樣式的代碼變成一個層級的代碼:Promise 是一個 Monad,
- Promise 在 JavaScript 社區中反響熱烈,因為 Node.js 的設計中包含了非常多異步操作,即便是簡單的任務也需要用到鏈式的異步回呼,即便只有三四個這樣的操作,代碼會變得笨重,Promise 終結了提心吊膽寫回呼的日子,Promise 已經寫進了 JavaScript ES6 的規范,JavaScript Promise 的運作機制請參考:JavaScript Promises … in Wicked Detail,
- JavaScript Promise 實作的一個亮點是它有一個非常明確的規范,稱為 A+,具體詳情可以在 promisejs.org查看,這意味著依賴 JavaScript 的弱型別系統,多個 Promise 的實作可以融合,彼此之間可以互相操作,只要 Promise 中的 then 函式定義符合規范,它就可以和其他庫中的 Promise 連接,這實在是太棒了,
- A+ 規范有一個非常好的 API,不使用 monad 中那個命名簡單且易于理解的 then 方法(在 A+ 規范中被重構為 flatMap 和 map),不過這個 API 不適合每個人,但我真的很喜歡它,并開始在 Swift 中實作一個類似的庫,你可以在 Github 的 Promise 上找個到這個庫,撰寫的程序很具有啟發性,
二、列舉
- 大家都知道,列舉非常棒,因為 Promise 本質上是狀態機,所以列舉用在這里非常合適,JavaScript Promise 實作的參考如下所示:
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
function Promise() {
// 保存 PENDING, FULFILLED 或者 REJECTED 的狀態
var state = PENDING;
// 當出現 FULFILLED 或 REJECTED 狀態時保存值或者錯誤
var value = null;
// 保存被 .then 或者 .done 函式觸發的成功 & 失敗的處理操作
var handlers = [];
}
- 應該是找不到比 Swift 的列舉實作更完美的例子了,如下,是 Swift 中的實作:
enum State<Value> {
case pending
case fulfilled(value: Value)
case rejected(error: ErrorType)
}
final class Promise<Value> {
private var state: State<Value>
private var callbacks: [Callback<Value>] = []
}
- 外部資料依賴于 Promise 的具體狀態,所以被封裝到對應 case 的關聯值中,當 Promise 處于 .pending 狀態時,任何外部資料都沒有意義,列舉在型別系統中表達出的語意是不可思議的,唯一要批判的是泛型不能被嵌套進其它型別中,并且這個缺陷在 Swift 3 中不會更改,具體請參考:Nested generic type in function is not allowed,
三、型別系統
- 創建一個新的 JavaScript Promise 時,可以使用便捷構造器:
var promise = new Promise(function(resolve, reject) {
someAsyncRequest(function(error, result) {
if (error) {
reject(error);
}
resolve(result);
});
});
- 傳入了一個包含兩個其它函式的函式,主要有兩個功能:第一個函式引數對應 Promise 操作成功的情況,第二個對應了失敗的情況,這兩個函式的順序很重要,因為 JavaScript 不是型別安全的,如果在上面的第一行代碼中寫錯了順序,寫成了 reject, resolve,很容易就向 resolve 函式中引入了錯誤,另一方面,Swift 是型別安全的,這意味著 reject 函式的型別是 (ErrorType) -> Void),該函式不會被成功的結果所接受,所以再也不用擔心弄亂 reject 和 resolve 函式的順序,
四、太多的型別可能會令人沮喪
- Promise 的型別中使用了泛型 Value,這是它的內部值型別,意味著可以通過型別推斷而不用指定具體的型別:
let promise = Promise(value: "initialValue") // a fulfilled Promise<String>
- 因為 Promise 經常被鏈式呼叫,依靠型別推斷來確定型別將會特別有用,必須向鏈中的每個步驟添加明確的型別是件令人沮喪的事,最終的樣式也不是很有 Swift 的風格:
let promise = Promise<String, APIError>(value: "initialValue")
- 這使得一行簡單的代碼增加了很多不必要的包袱,所以我洗掉了指定錯誤型別的功能,不幸的是,洗掉顯式的錯誤型別意味著不得不失去一個小小的型別系統的優勢,假設使用了一個叫 NoError 的空列舉,它有效地表達出 Promise 不能失敗的語意,因為空的列舉不能被初始化,所以 Promise 不能進入到 rejected 的狀態,這是一個令人心痛的損失,但我認為這是值得的,因為這樣在其它背景關系中使用 Promise 變得更簡單,希望能在實踐中使用這個類,以便深入體會并思考不設定錯誤型別否是個明智的決定,
五、函式式編程中的方法難以理解
- Promise 的型別是一個 monad,也就是說可以對它呼叫 flatMap,傳遞給 flatMap 的函式會回傳一個新的 Promise,回傳的 Promise 的狀態將成為該呼叫鏈的狀態,
- 不過,flatMap 的函式名是絕對不能忽視的,它無法使用一種易讀的方式表達函式中發生的事情,這是 A+ 規范的 Promise API 的優勢之一,JavaScript 中的 then 函式被多載為 flatMap 函式(為呼叫鏈回傳一個新的 Promise)和 map 函式(為呼叫鏈中的下一個 Promise 回傳一個新值),then 只意味著“下一步做這件事”,而不知道下一件事情的作業原理,
六、實作 Promise
① 第一個測驗
- 先寫第一個測驗:
test(named: "0. Executor function is called immediately") { assert, done in
var string: String = ""
_ = Promise { string = "foo" }
assert(string == "foo")
done()
}
- 通過此測驗,想實作傳遞一個函式給 Promise 的初始化函式,并立即呼叫此函式(沒有使用任何測驗框架,僅僅使用一個自定義的 test 方法,它在 Playground 中模擬斷言:PromisePlayground.swift),
- 當我們運行 Playground,編譯器會報錯:
error: Promise.playground:41:9: error: use of unresolved identifier 'Promise'
_ = Promise { string = "foo" }
^~~~~~~
- 合理,因此需要定義 Promise 類:
class Promise {
}
- 再運行,錯誤變為:
error: Promise.playground:44:17: error: argument passed to call that takes no arguments
_ = Promise { string = "foo" }
^~~~~~~~~~~~~~~~~~
- 必須定義一個初始化函式,它接受一個閉包作為引數,而且這個閉包應該被立即呼叫:
class Promise {
init(executor: () -> Void) {
executor()
}
}
- 由此,我們通過第一個測驗,目前還沒有寫出什么值得夸耀的東西,但耐心一點,實作將繼續增長:
Test 0. Executor function is called immediately passed
- 我們先將此測驗注釋掉,因為將來的 Promise 實作會變得有些不同,
② 最低限度
- 第二個測驗如下:
test(named: "1.1 Resolution handler is called when promise is resolved sync") { assert, done in
let string: String = "foo"
let promise = Promise<String> { resolve in
resolve(string)
}
promise.then { (value: String) in
assert(string == value)
done()
}
}
- 這個測驗挺簡單,但添加了一些新內容到 Promise 類,創建的這個 promise 有一個 resolution handler(即閉包的 resolve 引數),之后立即呼叫它(傳遞一個 value),然后使用 promise 的then方法來訪問 value 并用斷言確保其值,
- 在開始實作之前,需要引入另外一個不太一樣的測驗:
test(named: "1.2 Resolution handler is called when promise is resolved async") { assert, done in
let string: String = "foo"
let promise = Promise<String> { resolve in
after(0.1) {
resolve(string)
}
}
promise.then { (value: String) in
assert(string == value)
done()
}
}
- 不同于測驗 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 來觸發同樣的回呼,
- 現在對要實作的東西有了更好的理解,那就先以修復編譯器的報錯開始:
error: Promise.playground:54:19: error: cannot specialize non-generic type 'Promise'
let promise = Promise<String> { resolve in
^ ~~~~~~~~
- 必須給 Promise 型別添加泛型,誠然,一個 promise 是這樣的東西:它關聯著一個預定義的型別,并能在被解決時,將一個此型別的 value 保留住:
class Promise<Value> {
init(executor: () -> Void) {
executor()
}
}
- 現在錯誤為:
error: Promise.playground:54:37: error: contextual closure type '() -> Void' expects 0 arguments, but 1 was used in closure body
let promise = Promise<String> { resolve in
^
- 必須提供一個 resolve 函式傳遞給初始化函式(即 executor):
class Promise<Value> {
init(executor: (_ resolve: (Value) -> Void) -> Void) {
executor()
}
}
- 注意這個 resolve 引數是一個函式,它消耗一個 value:(Value) -> Void,一旦 value 被確定,這個函式將被外部世界呼叫,
編譯器依然不開心,因為需要提供一個 resolve 函式給 executor,那就創建一個 private:
class Promise<Value> {
init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) {
executor(resolve)
}
private func resolve(_ value: Value) -> Void {
// To implement
// This will be called by the outside world when a value is determined
}
}
- 當所有錯誤都被解決的時候,再來實作 resolve,下一個錯誤很簡單,方法 then 還未定義:
error: Promise.playground:61:5: error: value of type 'Promise<String>' has no member 'then'
promise.then { (value: String) in
^~~~~~~ ~~~~
- 現在修復它:
class Promise<Value> {
init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) {
executor(resolve)
}
func then(onResolved: @escaping (Value) -> Void) {
// To implement
}
private func resolve(_ value: Value) -> Void {
// To implement
}
}
- 現在編譯通過,我們之前說過一個 Promise 就是一個狀態機,它有一個 pending 狀態和一個 resolved 狀態,可以使用 enum 來定義它們:
enum State<T> {
case pending
case resolved(T)
}
- Swift 可以直接存盤 promise 的 value 在 enum 中,現在我要在 Promise 的實作中定義一個狀態,其默認值為 .pending,還需要一個私有函式,它能在當前還處于 .pending 狀態時更新狀態:
class Promise<Value> {
enum State<T> {
case pending
case resolved(T)
}
private var state: State<Value> = .pending
init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) {
executor(resolve)
}
func then(onResolved: @escaping (Value) -> Void) {
// To implement
}
private func resolve(_ value: Value) -> Void {
// To implement
}
private func updateState(to newState: State<Value>) {
guard case .pending = state else { return }
state = newState
}
}
- 注意 updateState(to:) 函式先檢查了當前處于 .pending 狀態,如果 promise 已經處于 .resolved 狀態,那它就不能再變成其它狀態,
現在是時候在必要時更新 promise 的狀態,即當 resolve 函式被外部世界傳遞 value 呼叫時:
private func resolve(_ value: Value) -> Void {
updateState(to: .resolved(value))
}
- 現在只缺少 then 方法還未實作,必須存盤回呼,并在 promise 被解決時呼叫回呼,現在來實作:
class Promise<Value> {
enum State<T> {
case pending
case resolved(T)
}
private var state: State<Value> = .pending
// we store the callback as an instance variable
private var callback: ((Value) -> Void)?
init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) {
executor(resolve)
}
func then(onResolved: @escaping (Value) -> Void) {
// store the callback in all cases
callback = onResolved
// and trigger it if needed
triggerCallbackIfResolved()
}
private func resolve(_ value: Value) -> Void {
updateState(to: .resolved(value))
}
private func updateState(to newState: State<Value>) {
guard case .pending = state else { return }
state = newState
triggerCallbackIfResolved()
}
private func triggerCallbackIfResolved() {
// the callback can be triggered only if we have a value,
// meaning the promise is resolved
guard case let .resolved(value) = state else { return }
callback?(value)
callback = nil
}
}
- 這里定義了一個實體變數 callback,以在 promise 處于.pending 狀態時保留回呼,同時,創建一個方法 triggerCallbackIfResolved,它先檢查狀態是否為 .resolved,然后傳遞拆包的 value 給回呼,這個方法在兩個地方被呼叫,一個是 then 方法中,如果 promise 已經在呼叫 then 時被解決;另一個在 updateState 方法中,因為那是 promise 更新其內部狀態從 .pending 到 .resolved 的地方,
- 有了這些修改,測驗就成功通過:
Test 1.1 Resolution handler is called when promise is resolved sync passed (1 assertions)
Test 1.2 Resolution handler is called when promise is resolved async passed (1 assertions)
- 但現在還需要做出一點改變,以得到一個真正的 Promise 實作,先來看看測驗:
test(named: "2.1 Promise supports many resolution handlers sync") { assert, done in
let string: String = "foo"
let promise = Promise<String> { resolve in
resolve(string)
}
promise.then { value in
assert(string == value)
}
promise.then { value in
assert(string == value)
done()
}
}
test(named: "2.2 Promise supports many resolution handlers async") { assert, done in
let string: String = "foo"
let promise = Promise<String> { resolve in
after(0.1) {
resolve(string)
}
}
promise.then { value in
assert(string == value)
}
promise.then { value in
assert(string == value)
done()
}
}
- 這回對每個 promise 都呼叫了兩次 then,看看測驗輸出:
Test 2.1 Promise supports many resolution handlers sync passed (2 assertions)
Test 2.2 Promise supports many resolution handlers async passed (1 assertions)
- 雖然測驗通過了,但你可能也注意問題,測驗 2.2 只有一個斷言,但應該是兩個,如果思考一下,這其實符合邏輯,在異步的測驗 2.2 中,當第一個 then 被呼叫時,promise 還處于 .pending 狀態,如之前所見,存盤了第一次 then 的回呼,但當第二次呼叫 then 時,promise 還是沒有被解決,依然處于 .pending 狀態,于是將回呼擦除換成了新的,只有第二個回呼會在將來被執行,第一個被忘記了,這使得測驗雖然通過,但只有一個斷言而不是兩個,
- 解決辦法也很簡單,就是存盤一個回呼的陣列,并在 promise 被解決時觸發它們,更新一下:
class Promise<Value> {
enum State<T> {
case pending
case resolved(T)
}
private var state: State<Value> = .pending
// We now store an array instead of a single function
private var callbacks: [(Value) -> Void] = []
init(executor: (_ resolve: @escaping (Value) -> Void) -> Void) {
executor(resolve)
}
func then(onResolved: @escaping (Value) -> Void) {
callbacks.append(onResolved)
triggerCallbacksIfResolved()
}
private func resolve(_ value: Value) -> Void {
updateState(to: .resolved(value))
}
private func updateState(to newState: State<Value>) {
guard case .pending = state else { return }
state = newState
triggerCallbacksIfResolved()
}
private func triggerCallbacksIfResolved() {
guard case let .resolved(value) = state else { return }
// We trigger all the callbacks
callbacks.forEach { callback in callback(value) }
callbacks.removeAll()
}
}
- 測驗通過,并且都有兩個斷言:
Test 2.1 Promise supports many resolution handlers sync passed (2 assertions)
Test 2.2 Promise supports many resolution handlers async passed (2 assertions)
- 現在就已經創建了自己的 Promise 類,已經可以使用它來抽象異步邏輯,但它還有限制,
- 如果從全域來看,我們知道 then 可以被重命名為 observe,它的目的是消費 promise 被解決后的 value,但它不回傳什么,這意味著暫時沒法串聯多個 promise,
③ 串聯多個 Promise
- 如果不能串聯多個 promise,那 Promise 實作就不算完整,先來看看測驗,它將幫助我們實作這個特性:
test(named: "3. Resolution handlers can be chained") { assert, done in
let string: String = "foo"
let promise = Promise<String> { resolve in
after(0.1) {
resolve(string)
}
}
promise
.then { value in
return Promise<String> { resolve in
after(0.1) {
resolve(value + value)
}
}
}
.then { value in // the "observe" previously defined
assert(string + string == value)
done()
}
}
- 如測驗所見,第一個 then 創建了一個有新 value 的新 Promise 并回傳了它,第二個 then(前一節定義被稱為 observe)被串聯在后面,它訪問新的 value(其將是 “foofoo” ),但是很快在終端里看到錯誤:
error: Promise.playground:143:10: error: value of tuple type '()' has no member 'then'
.then { value in
^
- 我們必須創建一個 then 的多載,它接受一個能回傳 promise 的函式,為了能夠串聯呼叫 then,這個方法必須也回傳一個 promise,這個 then 的原型如下:
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> {
// to implement
}
- 細心的你可能已經發現,在給 Promise 實作 flatMap,就如給 Optional 和 Array 定義 flatMap 一樣,也可以給 Promise 定義它,那么這個 “flatMap” 的 then 要怎么實作呢?
-
- 需要回傳一個Promise?
-
- 怎么給一個 promise?onResolved 方法?
-
- onResolved 需要一個型別為 Value 的 value 為引數,該怎樣得到這個 value 呢?
- 如果寫成代碼,大概如下:
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> {
then { value in // the "observe" one
let promise = onResolved(value) // `promise` is a Promise<NewValue>
// problem: how do we return `promise` to the outside ??
}
return // ??!!
}
- 到這里就快好了,但還有個小問題需要修復:這個 promise 變數被傳遞給 then 的閉包所限制,我們不能將其作為函式的回傳值,我們要使用的技巧是創建一個包裝 Promise,它將執行我們目前所寫的代碼,然后在 promise 變數解決時被同時解決,換句話說,當 onResolved 方法提供的 promise 被解決并從外部得到一個值,那包裝的 promise 也就被解決并得到同樣的值,
- 可能文字有些抽象,但如果寫成代碼,將看得更清楚:
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> {
// We have to return a promise, so let's return a new one
return Promise<NewValue> { resolve in
// this is called immediately as seen in test 0.
then { value in // the "observe" one
let promise = onResolved(value) // `promise` is a Promise<NewValue>
// `promise` has the same type of the Promise wrapper
// we can make the wrapper resolves when the `promise` resolves
// and gets a value
promise.then { value in resolve(value) }
}
}
}
- 如果整理一下代碼,將有這樣兩個方法:
// observe
func then(onResolved: @escaping (Value) -> Void) {
callbacks.append(onResolved)
triggerCallbacksIfResolved()
}
// flatMap
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> {
return Promise<NewValue> { resolve in
then { value in
onResolved(value).then(onResolved: resolve)
}
}
}
- 最后測驗通過:
Test 3. Resolution handlers can be chained passed (1 assertions)
④ 串聯多個 value
- 如果能給某個型別實作 flatMap,那么就能利用 flatMap 為其實作 map,對于 Promise 來說,map 該是什么樣子?使用如下測驗:
test(named: "4. Chaining works with non promise return values") { assert, done in
let string: String = "foo"
let promise = Promise<String> { resolve in
after(0.1) {
resolve(string)
}
}
promise
.then { value -> String in
return value + value
}
.then { value in // the "observe" then
assert(string + string == value)
done()
}
}
- 注意第一個 then 沒有再回傳一個 Promise,而是將其接收的值做了一個變換,這個新的 then 就對應于想添加的 map,
編譯器報錯說必須實作此方法:
error: Promise.playground:174:26: error: declared closure result 'String' is incompatible with contextual type 'Void'
.then { value -> String in
^~~~~~
Void
- 這個方法很接近 flatMap,唯一的不同是其引數 onResolved 函式回傳一個 NewValue,而不是 Promise,
// map
func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> {
// to implement
}
- 之前說可以利用 flatMap 實作 map,在這里的情況,我們看到需要回傳一個 Promise,如果使用這個 “flatMap” 的 then,并創建一個 promise,再以映射后的 value 來直接解決,就可以搞定:
// map
func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> {
return then { value in // the "flatMap" defined before
// must return a Promise<NewValue> here
// this promise directly resolves with the mapped value
return Promise<NewValue> { resolve in
let newValue = onResolved(value)
resolve(newValue)
}
}
}
- 再一次,測驗通過:
Test 4. Chaining works with non promise return values passed (1 assertions)
- 如果我們移除注釋,再看看做出了什么,有三個 then 方法的實作,能被使用或串聯:
// observe
func then(onResolved: @escaping (Value) -> Void) {
callbacks.append(onResolved)
triggerCallbacksIfResolved()
}
// flatMap
func then<NewValue>(onResolved: @escaping (Value) -> Promise<NewValue>) -> Promise<NewValue> {
return Promise<NewValue> { resolve in
then { value in
onResolved(value).then(onResolved: resolve)
}
}
}
// map
func then<NewValue>(onResolved: @escaping (Value) -> NewValue) -> Promise<NewValue> {
return then { value in
return Promise<NewValue> { resolve in
resolve(onResolved(value))
}
}
}
七、使用示例
- 實作就完成,Promise 類已足夠完整來展示能夠用它做什么,假設 App 有一些用戶,結構如下:
struct User {
let id: Int
let name: String
}
- 假設還有兩個方法,一個獲取用戶 id 串列,另一個使用 id 獲取某個用戶,而且假設想顯示第一個用戶的名字,
通過實作,可以這樣做,使用之前定義的這三個 then:
func fetchIds() -> Promise<[Int]> {
...
}
func fetchUser(id: Int) -> Promise<User> {
...
}
fetchIds()
.then { ids in // flatMap
return fetchUser(id: ids[0])
}
.then { user in // map
return user.name
}
.then { name in // observe
print(name)
}
- 代碼變得十分易讀、簡潔,而且沒有嵌套,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/335197.html
標籤:其他
上一篇:Swift之深入決議如何使用Swift UI實作3D Scroll效果
下一篇:鴻蒙ide使用
