JS中的幾種異步編程方式以及promise基本功能的實(shí)現(xiàn)

參考文獻(xiàn)

Javascript異步編程的4種方法
阮一峰ES6教程---Promise

相關(guān)技術(shù)

Promis, class, 面向?qū)ο?/p>

JS中的同步和異步

  • 同步任務(wù)模式
    在JS中,因?yàn)樗膱?zhí)行時(shí)單線程的,也就是說一次只能執(zhí)行一個(gè)任務(wù),當(dāng)有多個(gè)任務(wù)需要執(zhí)行的時(shí)候,就需要排隊(duì),一個(gè)一個(gè)的進(jìn)行執(zhí)行,就像下面的數(shù)組一樣,從左到右一個(gè)一個(gè)的執(zhí)行:
    [任務(wù)一,任務(wù)二,任務(wù)三]
    這樣的任務(wù)執(zhí)行模式是同步執(zhí)行的,有可能會(huì)因?yàn)槠渲械哪骋粋€(gè)任務(wù)執(zhí)行時(shí)間非常長而阻塞后續(xù)任務(wù),導(dǎo)致頁面卡死;
  • 異步任務(wù)模式
    而異步任務(wù)模式則解決了上述問題,例如數(shù)組中的任務(wù)一執(zhí)行完畢后,執(zhí)行的不是數(shù)組第二位的任務(wù)二,而是一個(gè)任務(wù)二回調(diào)函數(shù)(需要將任務(wù)二做成回調(diào)函數(shù)放入任務(wù)一中),而且任務(wù)二可以不等待任務(wù)一執(zhí)行完畢就進(jìn)行執(zhí)行,這種模式的任務(wù)執(zhí)行是與任務(wù)隊(duì)列(上述數(shù)組)的排列順序是不一樣的,比如下面的代碼:
// 任務(wù)一
        function fn1() {
            console.log(3)
            setTimeout(function() {
                console.log(1)
            }, 0)
        }
// 任務(wù)二
        function fn2() {
            console.log(2)
        }
        fn1()
        fn2()

在該例子中,setTimeout函數(shù)是異步的,在任務(wù)一執(zhí)行過程中遇到了setTimeout,于是將其內(nèi)的任務(wù)操作放到了事件隊(duì)列的隊(duì)尾,先去執(zhí)行任務(wù)二,執(zhí)行完畢后再回過頭來執(zhí)行隊(duì)尾的任務(wù)console.log(1),在此過程中,任務(wù)一內(nèi)的執(zhí)行沒有阻塞任務(wù)二的執(zhí)行

輸出結(jié)果


幾種異步任務(wù)模式

  • 使用setTimeout
    JS中的定時(shí)器setTimeout是實(shí)現(xiàn)異步任務(wù)執(zhí)行最簡單也最常見的方式,通過使用setTimeout(callback, 0)可以讓執(zhí)行上一個(gè)任務(wù)執(zhí)行時(shí)間過長的操作放到下一個(gè)任務(wù)執(zhí)行完之后再執(zhí)行,比如上一小節(jié)舉的例子,而關(guān)于定時(shí)器setTimeoutsetInterval相關(guān)可以參考下面鏈接:
    setTimeout
    setInterval
    優(yōu)點(diǎn):簡單明了;
    缺點(diǎn):①、耦合嚴(yán)重;②、容易陷入回調(diào)地獄
    PS:回調(diào)地獄例子(該操作只有三步,如果是十步,嵌套將會(huì)非常嚴(yán)重)
        function fn(callback1, callback2) {
            // 耗時(shí)操作
            let a = 0
            for (let i = 0; i < 100; i++) {
                a++
            }
            setTimeout(function() {
                callback1(++a)
                setTimeout(function() {
                    callback2(++a)
                }, 0)
            }, 0)
        }

        function fn1(a) {
            console.log(a)
        }

        function fn2(a) {
            console.log(a)
        }
        fn(fn1, fn2)
  • 發(fā)布/訂閱
    發(fā)布/訂閱模式是通過信號的發(fā)送時(shí)機(jī)來決定什么時(shí)候執(zhí)行其內(nèi)相應(yīng)的任務(wù),下面是一個(gè)發(fā)布訂閱模式的代碼實(shí)現(xiàn):
        class EventCenter {
            // 定義事件中心
            constructor() {
                this.events = {}
            }

            // 發(fā)布器
            on(evt, handler) {
                // 檢測事件信號是否存在,當(dāng)存在時(shí)不做操作,不存在時(shí)創(chuàng)建給予這個(gè)信號的方法存儲(chǔ)器(數(shù)組)
                this.events[evt] = this.events[evt] || []
                    // 將傳入的方法放入數(shù)組中
                this.events[evt].push({
                    handler: handler
                })
            }

            // 訂閱器
            fire(evt, params) {
                // 檢測當(dāng)前被訂閱的信號是否存在,存在則執(zhí)行其內(nèi)的所有方法
                if (!this.events[evt]) {
                    return
                }
                for (let i = 0; i < this.events[evt].length; i++) {
                    this.events[evt][i].handler(params)
                }
            }
        }

        let center = new EventCenter()

        center.on('event', function(data) {
            console.log('event執(zhí)行了第一個(gè)任務(wù)')
        })

        center.on('event', function(data) {
            console.log('event執(zhí)行了第二個(gè)任務(wù)')
        })

        center.fire('event')

輸出結(jié)果:


  • Promise
    Promise是ES6中新增的內(nèi)置對象,專門用于解決異步相關(guān)的問題,其內(nèi)最重要的兩個(gè)方法是thencatch,then方法第一個(gè)參數(shù)是resolve狀態(tài)時(shí)執(zhí)行的回調(diào),第二個(gè)參數(shù)則是reject狀態(tài)時(shí)執(zhí)行的回調(diào),而catch則是then中有一環(huán)是reject就執(zhí)行的回調(diào)函數(shù)
    通常使用的姿勢是這樣的
        function getData(){
            let promise = new Promise((resolve, reject) => {
                //  AJAX獲取數(shù)據(jù)。。。。
                if(success){
                    // 成功時(shí)執(zhí)行
                    resolve(fn1)
                }else{
                    // 失敗時(shí)執(zhí)行
                    reject(fn2)
                }
            })

            return promise
        }

        getData().then(fn1).catch(fn2)

可以從中發(fā)現(xiàn),除了thencatch兩個(gè)方法外,Promise還有鏈?zhǔn)秸{(diào)用的功能,那么下面就實(shí)現(xiàn)這樣的一個(gè)Promise。

Promise基本功能的實(shí)現(xiàn)

  • 首先寫一段測試代碼,使用Promise的調(diào)用方式
        let p = new Promise()

        function f1() {
            console.log('f1')
            setTimeout(function() {
                p.resolve('1')
            }, 1000)
            return p
        }

        function f2(result) {
            console.log('f2', result)
            setTimeout(function() {
                p.resolve('2')
            }, 1000)
        }

        function f3(result) {
            console.log('f3', result)
            setTimeout(function() {
                p.resolve('3')
            }, 1000)
        }

        function f4(result) {
            console.log('f4', result)
        }

        f1().then(f2).then(f3).catch(f4)
// 或者 f1().then(f2, f4).then(f3, f4)
  • 第一步、我們創(chuàng)建這么一個(gè)類,里面有thencatch兩個(gè)方法,并且能夠鏈?zhǔn)秸{(diào)用,為了防止重復(fù),這里用小寫開頭的promise
        class promise {
            constructor(){

            }

            then(success, fail) {
                // 鏈?zhǔn)秸{(diào)用
                return this
            }

            catch (fail) {
                // 鏈?zhǔn)秸{(diào)用
                return this
            }
        }
  • 第二步、因?yàn)橛?code>resolve和reject兩種狀態(tài),那么另外再設(shè)立兩個(gè)函數(shù)resolvereject分別進(jìn)行不同的狀態(tài)管理
        class promise {
            constructor() {

            }

            then(success, fail) {
                // 鏈?zhǔn)秸{(diào)用
                return this
            }

            catch (fail) {
                // 鏈?zhǔn)秸{(diào)用
                return this
            }

            // 成功狀態(tài)的管理
            resolve(result) {

            }

            // 失敗狀態(tài)的管理
            reject(result) {

            }
        }
  • 第三步、設(shè)定一個(gè)數(shù)組對需要執(zhí)行的方法進(jìn)行暫存,以及一個(gè)方法的執(zhí)行器,該執(zhí)行器依賴于resolvereject傳入的狀態(tài)進(jìn)行相應(yīng)的執(zhí)行
        class promise {
            constructor() {
                this.callbacks = []
            }

            then(success, fail) {
                // 鏈?zhǔn)秸{(diào)用
                return this
            }

            catch (fail) {
                // 鏈?zhǔn)秸{(diào)用
                return this
            }

            // 成功狀態(tài)的管理
            resolve(result) {
                this.actuator('resolve', result)
            }

            // 失敗狀態(tài)的管理
            reject(result) {
                this.actuator('reject', result)
            }

            // 執(zhí)行器
            actuator(status, result) {
                
            }
        }
  • 第四步、編寫then函數(shù)與執(zhí)行器中的邏輯
// 為了方便查看放到了最上面
        let p = new promise()
        function f1() {
            console.log('f1')
            setTimeout(function() {
                p.resolve('1')
            }, 1000)
            return p
        }
// ①、在寫then函數(shù)之前,先看看最開始Promise的調(diào)用方式是怎么樣的:f1().then(f2).then(f3).catch(f4),
然后在f1中嵌套回調(diào)f2并且返回這個(gè)Promise對象。
此外,then函數(shù)可以接受兩個(gè)參數(shù),一個(gè)成功回調(diào)一個(gè)失敗回調(diào),
所以思路就是創(chuàng)建一個(gè)對象里面有'resolve'和'reject'對應(yīng)這兩個(gè)回調(diào)然后放入callbacks數(shù)組中進(jìn)行管理;
            then(success, fail) {
                this.callbacks.push({
                    resolve: success,
                    reject: fail
                })
                // 鏈?zhǔn)秸{(diào)用
                return this
            }


// ②、這時(shí)候在調(diào)用f1時(shí)他會(huì)先返回Promise對象,然后再調(diào)用setTimeout里面的resolve回調(diào)并傳入?yún)?shù),而在resolve函數(shù)中調(diào)用了執(zhí)行器actuator,并且傳入了resolve這個(gè)狀態(tài)和在f1中傳入的參數(shù);
            // 成功狀態(tài)的管理
            resolve(result) {
                this.actuator('resolve', result)
            }

// ③、執(zhí)行actuator函數(shù),其實(shí)分析到了這一步就很簡單了,不過是將先前傳入callbaks中的函數(shù)取出來,然后執(zhí)行其中的成功回調(diào)就是了
            actuator(status, result) {
                // 取出之前傳入的回調(diào)函數(shù)對象(包含成功和失敗回調(diào)),然后執(zhí)行
                let handlerObj = this.callbacks.shift()
                handlerObj[type](result)
            }

// ④、整體代碼

        class promise {
            constructor() {
                this.callbacks = []
            }

            then(success, fail) {
                this.callbacks.push({
                    resolve: success,
                    reject: fail
                })
                // 鏈?zhǔn)秸{(diào)用
                return this
            }

            catch (fail) {
                // 鏈?zhǔn)秸{(diào)用
                return this
            }

            // 成功狀態(tài)的管理
            resolve(result) {
                this.actuator('resolve', result)
            }

            // 失敗狀態(tài)的管理
            reject(result) {
                this.actuator('reject', result)
            }

            // 執(zhí)行器
            actuator(status, result) {
                // 取出之前傳入的回調(diào)函數(shù)對象(包含成功和失敗回調(diào)),然后執(zhí)行
                let handlerObj = this.callbacks.shift()
                handlerObj[status](result)
            }
        }

其實(shí)到了這一步,Promise的基本功能(resolve和reject)已經(jīng)實(shí)現(xiàn)了,下面來看看f1().then(f2, f4).then(f3, f4).then(f4)的執(zhí)行結(jié)果吧
①、全部resolve狀態(tài)執(zhí)行結(jié)果


②、f2為reject時(shí)候執(zhí)行結(jié)果

        function f2(result) {
            console.log('f2', result)
            setTimeout(function() {
                p.reject('2')
            }, 1000)
        }
  • 最后、我們再添加catch方法進(jìn)去
    再上述代碼的基礎(chǔ)上,catch方法的實(shí)現(xiàn)其實(shí)已經(jīng)變得很簡單了,只需要在constructor里設(shè)立一個(gè)oncatch用以保存?zhèn)魅隿atch的回調(diào),然后當(dāng)中間某個(gè)環(huán)節(jié)reject的時(shí)候調(diào)用這個(gè)oncatch方法就好了,全部實(shí)現(xiàn)代碼以及調(diào)用實(shí)例:
        class promise {
            constructor() {
                    // 定義回調(diào)函數(shù)管理器和catch
                    this.callbacks = []
                    this.oncatch
                }
                // 當(dāng)狀態(tài)為reject時(shí)候,傳入reject狀態(tài)給執(zhí)行器
            reject(result) {
                    this.actuator('reject', result)
                }
                // 當(dāng)狀態(tài)為resolve時(shí)候,傳入resolve狀態(tài)給執(zhí)行器
            resolve(result) {
                    this.actuator('resolve', result)
                }
                // 執(zhí)行器
            actuator(status, result) {
                // 檢測當(dāng)狀態(tài)為reject并且oncatch不為空時(shí),執(zhí)行oncatch保存的失敗回調(diào), 適用于f1().then(f2).then(f3).catch(f4)
                if (status === 'reject' && this.catch) {
                    this.callbacks = []
                    this.oncatch(result)
                // 檢測當(dāng)callbacks第一位有方法時(shí),執(zhí)行相應(yīng)狀態(tài)的方法,適用于f1().then(f2, f4).then(f3, f4)
                } else if (this.callbacks[0]) {
                    let handlerObj = this.callbacks.shift()
                    if (handlerObj[status]) {
                        handlerObj[status](result)
                    }
                }
            }

            then(success, fail) {
                // 將傳入的成功和失敗回調(diào)組成對象,放入回調(diào)數(shù)組中進(jìn)行管理
                this.callbacks.push({
                    resolve: success,
                    reject: fail
                })
                // 用于鏈?zhǔn)秸{(diào)用
                return this
            }

            catch (fail) {
                // 保存?zhèn)魅氲氖』卣{(diào)
                this.oncatch = fail
                // 用于鏈?zhǔn)秸{(diào)用
                return this
            }
        }


        let p = new promise()

        function f1() {
            console.log('f1')
            setTimeout(function() {
                p.resolve('1')
            }, 1000)
            return p
        }

        function f2(result) {
            console.log('f2', result)
            setTimeout(function() {
                p.resolve('2')
            }, 1000)
        }

        function f3(result) {
            console.log('f3', result)
            setTimeout(function() {
                p.resolve('3')
            }, 1000)
        }

        function f4(result) {
            console.log('f4', result)
        }
        // 第一種調(diào)用
        f1().then(f2).then(f3).catch(f4)
        // 第二種調(diào)用
        f1().then(f2, f4).then(f3, f4)

附錄,ES7中新增了await async關(guān)鍵字用于實(shí)現(xiàn)更簡單更直觀的異步代碼,甚至寫出來后和同步執(zhí)行的代碼看起來沒什么區(qū)別,但在本質(zhì)上還是使用了Promise,想要了解的同學(xué)也可以點(diǎn)擊下面的鏈接一起學(xué)習(xí),在此就不再多做介紹了.

體驗(yàn)異步的終極解決方案-ES7的Async/Await

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 你不知道JS:異步 第三章:Promises 在第二章,我們指出了采用回調(diào)來表達(dá)異步和管理并發(fā)時(shí)的兩種主要不足:缺...
    purple_force閱讀 2,261評論 0 4
  • 本文適用的讀者 本文寫給有一定Promise使用經(jīng)驗(yàn)的人,如果你還沒有使用過Promise,這篇文章可能不適合你,...
    HZ充電大喵閱讀 7,466評論 6 19
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 11,141評論 26 95
  • 特點(diǎn) Promise能將回調(diào)分離出來,在異步操作執(zhí)行之后,用鏈?zhǔn)椒椒▓?zhí)行回調(diào),雖然es5用封裝函數(shù)也能實(shí)現(xiàn),但是如...
    一二三kkxx閱讀 738評論 0 1
  • 弄懂js異步 講異步之前,我們必須掌握一個(gè)基礎(chǔ)知識-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,889評論 0 5

友情鏈接更多精彩內(nèi)容