Basic use and source code analysis of PromiseKit

Keywords: iOS Programming Swift Linux Attribute

Promise is an application framework that deals with a series of asynchronous operations. It can ensure that a series of asynchronous operations can be executed in sequence. When an error occurs, it can be handled by catch ing the error. Promise framework is also a good interpretation of swift's face-to-face protocol programming and functional programming

There are two types of 1Promise and 2Guarantee. Guarantee does not implement the CatchMixin protocol and cannot catch errors. It is not allowed to throw errors. The first type is commonly used for error handling. Promise is a promise to be executed and may not be executed; guarantee is a guarantee to be executed

Basic usage:

func threeRequest111() {
         firstly {
                request1(with: ["test1": "first"])
            }
            .then { (v) -> Promise<NSDictionary> in
                print("🍀", v)
                return self.request2(para: ["test2": "second"])
            }
            .then { (v) -> Promise<NSDictionary> in
                print("🍀🍀", v)
                return self.request3(para: ["test3": "third"])
            }
            .map({ (dic) -> [String:String]  in
                if let dic1 = dic as? [String:String]{
                    return dic1
                }else{
                    return [String:String]()
                }
            }).done({ (dic) in
                print(dic)
            })
            .catch { (error) in
                print(error.localizedDescription)
        
            }.finally {
                print("finaly")
        }
    }

func request1(with parameters: [String: String]) -> Promise<(NSDictionary)> {

        return Promise<NSDictionary>(resolver: { (resolver) in

            Alamofire.request("https://httpbin.org/get", method: .get, parameters: parameters).validate().responseJSON() { (response) in

                switch response.result {

                case .success(let dict):

                    delay(time: 1, task: {

                        resolver.fulfill(dict as! NSDictionary)

                    })

                case .failure(let error):

                    resolver.reject(error)                  

                }

            }

        })

    }

    func request2(para:[String:String]) -> Promise<NSDictionary> {

        return request1(with: para)

    }

    func request3(para:[String:String]) -> Promise<NSDictionary> {

        return request1(with: para)
    }

 

Source code analysis

At the beginning, I wanted to study the source code with problems

1. How to ensure the sequential execution of a series of block s

Save the external incoming thenBlock, etc. into an array, handlers.append(to). When your own task finishes executing, execute the task with the array

2. How the return value promise in a closure is related to promise in the first function

rv.pipe(to: rp.box.seal)

Two. Promise main function

1.then function

func then<U: Thenable>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> U) -> Promise<U.T> {
        let rp = Promise<U.T>(.pending)
        pipe {//Upward promise Add task to
            switch $0 {
            case .fulfilled(let value):
                on.async(flags: flags) {
                    do {
                        let rv = try body(value)
                        guard rv !== rp else { throw PMKError.returnedSelf }
                        rv.pipe(to: rp.box.seal)
                    } catch {
                        rp.box.seal(.rejected(error))
                    }
                }
            case .rejected(let error):
                rp.box.seal(.rejected(error))
            }
        }
        return rp
    }

The then function adds the pipe closure (including the closure to be executed externally) to the previous promise.box.handlers:

handlers.append(to)

to is the pipe closure in the then function, and it is added synchronously with the fence function when adding, which ensures the sequential execution of tasks

Barrier. Sync (flags:. Barrier) {/ / fence synchronization

When the closure (external time-consuming task) in the previous promise is completed, reever calls full:

resolver.fulfill(T) -> box.seal(.fulfilled(value)) ->

override func seal(_ value: T) {
        var handlers: Handlers<T>!
        barrier.sync(flags: .barrier) {
            guard case .pending(let _handlers) = self.sealant else {
                return  // already fulfilled!
            }
            handlers = _handlers
            self.sealant = .resolved(value)
        }
        if let handlers = handlers {
            handlers.bodies.forEach{ $0(value) }
        }
    }
//The task in handlers.bodies (that is, the block in the next then) with the result value is actually the pipe closure added by the sky above:

  pipe {//Upward promise Add task to

            switch $0 {

            case .fulfilled(let value):

                on.async(flags: flags) {

                    do {

                        let rv = try body(value)

                        guard rv !== rp else { throw PMKError.returnedSelf }

                        rv.pipe(to: rp.box.seal)//implement rp.handlers.foreach This task is added to rv.handlers

                    } catch {

                        rp.box.seal(.rejected(error))

                    }

                }

            case .rejected(let error):

                rp.box.seal(.rejected(error))

            }

        }

Another thing around here is that switch $0 is the result < T > type, while handler.bodies.foreach {$0 (value)} is called. The value passed in is the T type, and it doesn't match. Take a look around, when initializing resolve, T represents result, so it matches correctly

 

We know that a Promise: RV needs to be returned in the then function block, and a Promise: rp has been created in the then function, which is passed to the next then function. Next, we will simplify the understanding of then{rv1:Promise}.then{rv2:promise} with two adjacent then functions

rp1.box.seal is stored in the handler of rv1, that is, whether to execute the pipe where rv2 is located

Figure 1 above

 

2.catch error capture function. Why can promise go to catch no matter which link reports an error

catch is defined in the CatchMixin protocol. promise implements this protocol. catch has a default implementation:

@discardableResult
    func `catch`(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer {
        let finalizer = PMKFinalizer()
        pipe {
            switch $0 {
            case .rejected(let error):
                guard policy == .allErrors || !error.isCancelled else {
                    fallthrough
                }
                on.async(flags: flags) {
                    body(error)
                    finalizer.pending.resolve(())
                }
            case .fulfilled:
                finalizer.pending.resolve(())
            }
        }
        return finalizer
    }

catch is usually at the end or finally in the promise chain, so its return value is pmkfinizer

The content of pipe is added to the handels of the previous promise through pipe

When any link in the project chain executes reject(error), execute the seal in the next project

rp.box.seal(.rejected(error))

Follow the promise chain all the way down, and each one will execute this until catch, and go to the code task in catch

3. Firsty, it's easier to understand the t-function after you look at firsty. It's different from then in that it executes first because it has no promise in front of it. It's a function rather than a promise method. The return value is a promise like then

public func firstly<U: Thenable>(execute body: () throws -> U) -> Promise<U.T> {
    do {
        let rp = Promise<U.T>(.pending)
        try body().pipe(to: rp.box.seal)//implement body,Establish Promise,Execute after returning pipe(The external operation may be asynchronous and time-consuming, so it will be executed first pipe(to),return rp,Then it's external.then Functions, etc.
        return rp
    } catch {
        return Promise(error: error)
    }
}

4. The when function is mainly used to implement the tasks in front of them at the same time. After they are all executed, the tasks will be executed in order.

Basic usage:

//request1 and request2,Execute in parallel request3;Parameter is promise Array, multiple tasks executing at the same time
        when(fulfilled: [request1(with: ["para1":"hello"]),request2(para: ["para2":"nihao"])])
        .then { (dic) -> Promise<NSDictionary> in
            self.request3(para: ["uhu":"nih"])
            }.catch { (error) in
                print(error.localizedDescription)
        }
        //promises There are different types of results in the array promise,Up to 5 different types of Promise
        when(fulfilled: request1(with: ["para1":"hello"]), request4())
            .then { (arg0) -> Promise<[String:String]> in
            let (dic, str) = arg0
            return Promise<[String:String]>(resolver: { (res) in
                res.fulfill([str : "\(dic)"])
            })
            }.catch { (error) in
                print(error.localizedDescription)
        }
//Check the source code. Two Int values indicate the number of completed tasks and the total number of tasks
private func _when<U: Thenable>(_ thenables: [U]) -> Promise<Void> {
    var countdown = thenables.count
    guard countdown > 0 else {
        return .value(Void())
    }

    let rp = Promise<Void>(.pending)

#if PMKDisableProgress || os(Linux)
    var progress: (completedUnitCount: Int, totalUnitCount: Int) = (0, 0)
#else
    let progress = Progress(totalUnitCount: Int64(thenables.count))
    progress.isCancellable = false
    progress.isPausable = false
#endif

    let barrier = DispatchQueue(label: "org.promisekit.barrier.when", attributes: .concurrent)

    for promise in thenables {
        promise.pipe { result in
            barrier.sync(flags: .barrier) {
                switch result {
                case .rejected(let error):
                    //If it is pending(Wait) state, passing the error down, which ensures several parallel tasks
                    //There is only the first task that goes wrong, and it goes wrong, because once it goes wrong, any other task is meaningless
                    if rp.isPending {
                        progress.completedUnitCount = progress.totalUnitCount
                        rp.box.seal(.rejected(error))
                    }
                case .fulfilled:
                    //This condition ensures that if one of several parallel tasks has made an error, the task that has been completed correctly will not be passed down here
                    guard rp.isPending else { return }
                    progress.completedUnitCount += 1
                    countdown -= 1
                    if countdown == 0 {
                        rp.box.seal(.fulfilled(()))
                    }
                }
            }
        }
    }

    return rp
}

Second, take a look at other functions provided by Promise

Map Conversion function, converting the result
public func map<U>(on: DispatchQueue? = default, flags: DispatchWorkItemFlags? = default, _ transform: @escaping (Self.T) throws -> U) -> PromiseKit.Promise<U>
done Function, done Function sum then The difference between functions is block The return value is void,It is only executed in sequence, and the result value of the previous step is used done function
func done(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> Void) -> Promise<Void> 
Get Function, he and done Almost. It will return the upper result value automatically
 func get(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) throws -> Void) -> Promise<T>
tap Function, he will return a Result<T>,You can see the returned value here without any side effects on the whole chain
func tap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Result<T>) -> Void) -> Promise<T>
asVoid Function, which returns a new promise,And last promise Connect, and the last one promise Value of is discarded
func asVoid() -> Promise<Void>

//There are also some functions when the result value is Sequence (i.e. array). Some principles for array are similar to some higher-order functions of array
public extension Thenable where T: Sequence 
func mapValues<U>(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]>

Attached 1, the framework also encapsulates a KVO to implement KVOProxy, a KVO created for guarantee. Any class can add an observer. When receiving the observation attribute changes, the content of guarantee will be executed. Guarantee is a guarantee that it will be executed. Therefore, in KVOProxy, KVOProxy owns itself, retainCycle = self, and will not be released if it is not received and dropped back

extension NSObject {
    public func observe(_: PMKNamespacer, keyPath: String) -> Guarantee<Any?> {
        return Guarantee { KVOProxy(observee: self, keyPath: keyPath, resolve: $0) }
    }
}

private class KVOProxy: NSObject {
    var retainCycle: KVOProxy?
    let fulfill: (Any?) -> Void

    @discardableResult
    init(observee: NSObject, keyPath: String, resolve: @escaping (Any?) -> Void) {
        fulfill = resolve
        super.init()
        observee.addObserver(self, forKeyPath: keyPath, options: NSKeyValueObservingOptions.new, context: pointer)
        retainCycle = self
        //Holding creates a circular reference by itself. It will not be released if it is not received and dropped back
    }

    fileprivate override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let change = change, context == pointer {
            defer { retainCycle = nil }//Delay execution, the last execution in the scope, and release after receiving the callback
            fulfill(change[NSKeyValueChangeKey.newKey])
            if let object = object as? NSObject, let keyPath = keyPath {
                object.removeObserver(self, forKeyPath: keyPath)
            }
        }
    }

    private lazy var pointer: UnsafeMutableRawPointer = {
        return Unmanaged<KVOProxy>.passUnretained(self).toOpaque()
    }()
}

Attached 2, the framework has made some extension s to alamofire

Posted by Hagbard on Thu, 14 Nov 2019 00:55:09 -0800