<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        浪里行舟

        浪里行舟 查看完整檔案

        其它編輯HKU SPACE  |  前端 編輯前端工匠  |  前端 編輯 github.com/ljianshu/Blog 編輯
        編輯

        微信:frontJS,記得備注sf
        公眾號:前端工匠
        文章首發地址:https://github.com/ljianshu/Blog(包括源代碼和思維導圖)

        個人動態

        浪里行舟 關注了用戶 · 3月21日

        sprina @sprina1997

        一起成長

        關注 22

        浪里行舟 贊了文章 · 2020-08-12

        Typescript 設計模式之工廠方法

        在現實生活中,工廠是負責生產產品的,比如牛奶、面包或禮物等,這些產品滿足了我們日常的生理需求。此外,在日常生活中,我們也離不開大大小小的系統,這些系統是由不同的組件對象構成。

        而作為一名 Web 軟件開發工程師,在軟件系統的設計與開發過程中,我們可以利用設計模式來提高代碼的可重用性、可擴展性和可維護性。在眾多設計模式當中,有一種被稱為工廠模式的設計模式,它提供了創建對象的最佳方式。

        工廠模式可以分為三類:

        • 簡單工廠模式(Simple Factory Pattern)
        • 工廠方法模式(Factory Method Pattern)
        • 抽象工廠模式(Abstract Factory Pattern)

        本文阿寶哥將介紹簡單工廠模式與工廠方法模式,而抽象工廠模式將在后續的文章中介紹,下面我們先來介紹簡單工廠模式。

        一、簡單工廠模式

        1.1 簡單工廠模式簡介

        簡單工廠模式又叫 靜態方法模式,因為工廠類中定義了一個靜態方法用于創建對象。簡單工廠讓使用者不用知道具體的參數就可以創建出所需的 ”產品“ 類,即使用者可以直接消費產品而不需要知道產品的具體生產細節。

        相信對于剛接觸簡單工廠模式的小伙伴來說,看到以上的描述可能會覺得有點抽象。這里為了讓小伙伴更好地理解簡單工廠模式,阿寶哥以用戶買車為例,來介紹一下 BMW 工廠如何使用簡單工廠模式來生產?。

        在上圖中,阿寶哥模擬了用戶購車的流程,pingan 和 qhw 分別向 BMW 工廠訂購了 BMW730 和 BMW840 型號的車型,接著工廠按照對應的模型進行生產并在生產完成后交付給用戶。接下來,阿寶哥將介紹如何使用簡單工廠來描述 BMW 工廠生產指定型號車子的過程。

        1.2 簡單工廠模式實戰

        1. 定義 BMW 抽象類
        abstract class BMW {
          abstract run(): void;
        }
        1. 創建 BMW730 類(BMW 730 Model)
        class BMW730 extends BMW {
          run(): void {
            console.log("BMW730 發動咯");
          }
        }
        1. 創建 BMW840 類(BMW 840 Model)
        class BMW840 extends BMW {
          run(): void {
            console.log("BMW840 發動咯");
          }
        }
        1. 創建 BMWFactory 工廠類
        class BMWFactory {
          public static produceBMW(model: "730" | "840"): BMW {
            if (model === "730") {
              return new BMW730();
            } else {
              return new BMW840();
            }
          }
        }
        1. 生產并發動 BMW730 和 BMW840
        const bmw730 = BMWFactory.produceBMW("730");
        const bmw840 = BMWFactory.produceBMW("840");
        
        bmw730.run();
        bmw840.run();

        以上代碼運行后的輸出結果為:

        BMW730 發動咯 
        BMW840 發動咯 

        通過觀察以上的輸出結果,我們可以知道我們的 BMWFactory 已經可以正常工作了。在 BMWFactory 類中,阿寶哥定義了一個 produceBMW() 方法,該方法會根據傳入的模型參數來創建不同型號的車子。

        看完簡單工廠模式實戰的示例,你是不是覺得簡單工廠模式還是挺好理解的。那么什么場景下使用簡單工廠模式呢?要回答這個問題我們需要來了解一下簡單工廠的優缺點。

        1.3 簡單工廠模式優缺點

        1.3.1 優點
        • 將創建實例與使用實例的任務分開,使用者不必關心對象是如何創建的,實現了系統的解耦;
        • 客戶端無須知道所創建的具體產品類的類名,只需要知道具體產品類所對應的參數即可。
        1.3.2 缺點
        • 由于工廠類集中了所有產品創建邏輯,一旦不能正常工作,整個系統都要受到影響。
        • 系統擴展困難,一旦添加新產品就不得不修改工廠邏輯,在產品類型較多時,也有可能造成工廠邏輯過于復雜,不利于系統的擴展和維護。

        了解完簡單工廠的優缺點,我們來看一下它的應用場景。

        1.4 簡單工廠模式應用場景

        在滿足以下條件下可以考慮使用簡單工廠模式:

        • 工廠類負責創建的對象比較少:由于創建的對象比較少,不會造成工廠方法中業務邏輯過于復雜。
        • 客戶端只需知道傳入工廠類靜態方法的參數,而不需要關心創建對象的細節。

        介紹完簡單工廠模式,接下來我們來介紹本文的主角 ”工廠方法模式“。

        二、工廠方法模式

        2.1 工廠方法簡介

        工廠方法模式(Factory Method Pattern)又稱為工廠模式,也叫多態工廠(Polymorphic Factory)模式,它屬于類創建型模式。

        在工廠方法模式中,工廠父類負責定義創建產品對象的公共接口,而工廠子類則負責生成具體的產品對象, 這樣做的目的是將產品類的實例化操作延遲到工廠子類中完成,即通過工廠子類來確定究竟應該實例化哪一個具體產品類。

        在上圖中,阿寶哥模擬了用戶購車的流程,pingan 和 qhw 分別向 BMW 730 和 BMW 840 工廠訂購了 BMW730 和 BMW840 型號的車型,接著工廠按照對應的模型進行生產并在生產完成后交付給用戶。接下來,阿寶哥來介紹如何使用工廠方法來描述 BMW 工廠生產指定型號車子的過程。

        2.2 工廠方法實戰

        1. 定義 BMW 抽象類
        abstract class BMW {
          abstract run(): void;
        }
        1. 創建 BMW730 類(BMW 730 Model)
        class BMW730 extends BMW {
          run(): void {
            console.log("BMW730 發動咯");
          }
        }
        1. 創建 BMW840 類(BMW 840 Model)
        class BMW840 extends BMW {
          run(): void {
            console.log("BMW840 發動咯");
          }
        }
        1. 定義 BMWFactory 接口
        interface BMWFactory {
          produceBMW(): BMW;
        }
        1. 創建 BMW730Factory 類
        class BMW730Factory implements BMWFactory {
          produceBMW(): BMW {
            return new BMW730();
          }
        }
        1. 創建 BMW840Factory 類
        class BMW840Factory implements BMWFactory {
          produceBMW(): BMW {
            return new BMW840();
          }
        }
        1. 生產并發動 BMW730 和 BMW840
        const bmw730Factory = new BMW730Factory();
        const bmw840Factory = new BMW840Factory();
        
        const bmw730 = bmw730Factory.produceBMW();
        const bmw840 = bmw840Factory.produceBMW();
        
        bmw730.run();
        bmw840.run();

        通過觀察以上的輸出結果,我們可以知道我們的 BMW730Factory 和 BMW840Factory 工廠已經可以正常工作了。相比前面的簡單工廠模式,工廠方法模式通過創建不同的工廠來生產不同的產品。下面我們來看一下工廠方法有哪些優缺點。

        2.3 工廠方法優缺點

        2.3.1 優點
        • 在系統中加入新產品時,無須修改抽象工廠和抽象產品提供的接口,只要添加一個具體工廠和具體產品就可以了。這樣,系統的可擴展性也就變得非常好,更加符合 “開閉原則”。而簡單工廠模式需要修改工廠類的判斷邏輯。
        • 符合單一職責的原則,即每個具體工廠類只負責創建對應的產品。而簡單工廠模式中的工廠類存在一定的邏輯判斷。
        • 基于工廠角色和產品角色的多態性設計是工廠方法模式的關鍵。它能夠使工廠可以自主確定創建何種產品對象,而如何創建這個對象的細節則完全封裝在具體工廠內部。工廠方法模式之所以又被稱為多態工廠模式,是因為所有的具體工廠類都具有同一抽象父類。
        2.3.2 缺點
        • 在添加新產品時,需要編寫新的具體產品類,而且還要提供與之對應的具體工廠類,系統中類的個數將成對增加,在一定程度上增加了系統的復雜度,有更多的類需要編譯和運行,會給系統帶來一些額外的開銷。
        • 一個具體工廠只能創建一種具體產品。

        最后我們來簡單介紹一下工廠方法的應用場景。

        2.4 工廠方法應用場景

        • 一個類不知道它所需要的對象的類:在工廠方法模式中,客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品對象由具體工廠類創建;客戶端需要知道創建具體產品的工廠類。
        • 一個類通過其子類來指定創建哪個對象:在工廠方法模式中,對于抽象工廠類只需要提供一個創建產品的接口,而由其子類來確定具體要創建的對象,利用面向對象的多態性和里氏代換原則,在程序運行時,子類對象將覆蓋父類對象,從而使得系統更容易擴展。

        三、參考資源

        四、推薦閱讀

        查看原文

        贊 18 收藏 9 評論 0

        浪里行舟 贊了文章 · 2020-07-24

        機器學習算法入門指南(全)

        前言

        機器學習 作為人工智能領域的核心組成,是計算機程序學習數據經驗以優化自身算法,并產生相應的“智能化的”建議與決策的過程。

        一個經典的機器學習的定義是:

        A computer program is said to learn from experience E with respect to some class of tasks T and performance measure P,
        if its performance at tasks in T, as measured by P, improves with experience E.

        一、機器學習概論

        機器學習是關于計算機基于數據分布構建出概率統計模型,并運用模型對數據進行分析與預測的方法。按照學習數據分布的方式的不同,主要可以分為監督學習和非監督學習

        1.1 監督學習

        從有標注的數據(x為變量特征空間, y為標簽)中,通過選擇的模型及確定的學習策略,再用合適算法計算后學習到最優模型,并用模型預測的過程。模型預測結果Y的取值有限的或者無限的,可分為分類模型或者回歸模型;

        1.2 非監督學習:

        從無標注的數據(x為變量特征空間),通過選擇的模型及確定的學習策略,再用合適算法計算后學習到最優模型,并用模型發現數據的統計規律或者內在結構。按照應用場景,可以分為聚類,降維和關聯分析等模型;

        二、機器學習建模流程

        2.1 明確業務問題

        明確業務問題是機器學習的先決條件,這里需要抽象出現實業務問題的解決方案:需要學習什么樣的數據作為輸入,目標是得到什么樣的模型做決策作為輸出。

        (如一個簡單的新聞分類場景就是學習已有的新聞及其類別標簽數據,得到一個分類模型,通過模型對每天新的新聞做類別預測,以歸類到每個新聞頻道。)

        2.2 數據選擇:收集及輸入數據

        數據決定了機器學習結果的上限,而算法只是盡可能逼近這個上限。
        意味著數據的質量決定了模型的最終效果,在實際的工業應用中,算法通常占了很小的一部分,大部分工程師的工作都是在找數據、提煉數據、分析數據。數據選擇需要關注的是:

        ① 數據的代表性:無代表性的數據可能會導致模型的過擬合,對訓練數據之外的新數據無識別能力;

        ② 數據時間范圍:監督學習的特征變量X及標簽Y如與時間先后有關,則需要明確數據時間窗口,否則可能會導致數據泄漏,即存在和利用因果顛倒的特征變量的現象。(如預測明天會不會下雨,但是訓練數據引入明天溫濕度情況);

        ③ 數據業務范圍:明確與任務相關的數據表范圍,避免缺失代表性數據或引入大量無關數據作為噪音;

        2.3 特征工程:數據預處理及特征提取

        特征工程就是將原始數據加工轉化為模型有用的特征,技術手段一般可分為:

        數據預處理:特征表示,缺失值/異常值處理,數據離散化,數據標準化等;
        特征提取:特征衍生,特征選擇,特征降維等;

        • 特征表示

          數據需要轉換為計算機能夠處理的數值形式。如果數據是圖片數據需要轉換為RGB三維矩陣的表示。

        圖片
        字符類的數據可以用多維數組表示,有Onehot獨熱編碼表示、word2vetor分布式表示及bert動態編碼等;
        自然語言

        • 異常值處理

          收集的數據由于人為或者自然因素可能引入了異常值(噪音),這會對模型學習進行干擾。

          通常需要對人為引起的異常值進行處理,通過業務判斷和技術手段(python、正則式匹配、pandas數據處理及matplotlib可視化等數據分析處理技術)篩選異常的信息,并結合業務情況刪除或者替換數值。

        • 缺失值處理

          數據缺失的部分,通過結合業務進行填充數值、不做處理或者刪除。
          根據缺失率情況及處理方式分為以下情況:

          ① 缺失率較高,并結合業務可以直接刪除該特征變量。經驗上可以新增一個bool類型的變量特征記錄該字段的缺失情況,缺失記為1,非缺失記為0;

          ② 缺失率較低,結合業務可使用一些缺失值填充手段,如pandas的fillna方法、訓練隨機森林模型預測缺失值填充;

          ③ 不做處理:部分模型如隨機森林、xgboost、lightgbm能夠處理數據缺失的情況,不需要對缺失數據做任何的處理。

        • 數據離散化

          數據離散化能減小算法的時間和空間開銷(不同算法情況不一),并可以使特征更有業務解釋性。

          離散化是將連續的數據進行分段,使其變為一段段離散化的區間,分段的原則有等距離、等頻率等方法。

        • 數據標準化

          數據各個特征變量的量綱差異很大,可以使用數據標準化消除不同分量量綱差異的影響,加速模型收斂的效率。常用的方法有:

          ① min-max 標準化:

          將數值范圍縮放到(0,1),但沒有改變數據分布。max為樣本最大值,min為樣本最小值。

          ② z-score 標準化:

          將數值范圍縮放到0附近, 經過處理的數據符合標準正態分布。u是平均值,σ是標準差。

        • 特征衍生

        基礎特征對樣本信息的表述有限,可通過特征衍生出新含義的特征進行補充。特征衍生是對現有基礎特征的含義進行某種處理(組合/轉換之類),常用方法如:

        ① 結合業務的理解做衍生,比如通過12個月工資可以加工出:平均月工資,薪資變化值,是否發工資 等等;

        ② 使用特征衍生工具:如feature tools等技術;

        • 特征選擇

        特征選擇篩選出顯著特征、摒棄非顯著特征。特征選擇方法一般分為三類:


        ① 過濾法:按照特征的發散性或者相關性指標對各個特征進行評分后選擇,如方差驗證、相關系數、IV值、卡方檢驗及信息增益等方法。

        ② 包裝法:每次選擇部分特征迭代訓練模型,根據模型預測效果評分選擇特征的去留。

        ③ 嵌入法:使用某些模型進行訓練,得到各個特征的權值系數,根據權值系數從大到小來選擇特征,如XGBOOST特征重要性選擇特征。

        • 特征降維

        如果特征選擇后的特征數目仍太多,這種情形下經常會有數據樣本稀疏、距離計算困難的問題(稱為 “維數災難”),可以通過特征降維解決。

        常用的降維方法有:主成分分析法(PCA),
        線性判別分析法(LDA)等。

        2.4 模型訓練

        模型訓練是選擇模型學習數據分布的過程。這過程還需要依據訓練結果調整算法的(超)參數,使得結果變得更加優良。

        • 2.4.1 數據集劃分

          訓練模型前,一般會把數據集分為訓練集和測試集,并可再對訓練集再細分為訓練集和驗證集,從而對模型的泛化能力進行評估。

          ① 訓練集(training set):用于運行學習算法。

          ② 開發驗證集(development set)用于調整參數,選擇特征以及對算法其它優化。常用的驗證方式有交叉驗證Cross-validation,留一法等;

          ③ 測試集(test set)用于評估算法的性能,但不會據此改變學習算法或參數。

        • 2.4.2 模型選擇

          常見的機器學習算法如下:

        模型選擇取決于數據情況和預測目標??梢杂柧毝鄠€模型,根據實際的效果選擇表現較好的模型或者模型融合。
        

        模型選擇

        • 2.4.3 模型訓練

          訓練過程可以通過調參進行優化,調參的過程是一種基于數據集、模型和訓練過程細節的實證過程。
          超參數優化需要基于對算法的原理的理解和經驗,此外還有自動調參技術:網格搜索、隨機搜索及貝葉斯優化等。

        2.5 模型評估

        模型評估的標準:模型學習的目的使學到的模型對新數據能有很好的預測能力(泛化能力)?,F實中通常由訓練誤差及測試誤差評估模型的訓練數據學習程度及泛化能力。

        • 2.5.1 評估指標

          ① 評估分類模型:
          常用的評估標準有查準率P、查全率R、兩者調和平均F1-score 等,并由混淆矩陣的統計相應的個數計算出數值:

          混淆矩陣

          查準率是指分類器分類正確的正樣本(TP)的個數占該分類器所有預測為正樣本個數(TP+FP)的比例;

          查全率是指分類器分類正確的正樣本個數(TP)占所有的正樣本個數(TP+FN)的比例。

          F1-score是查準率P、查全率R的調和平均:

          ② 評估回歸模型:
          常用的評估指標有RMSE均方根誤差 等。反饋的是預測數值與實際值的擬合情況。

          ③ 評估聚類模型:可分為兩類方式,一類將聚類結果與某個“參考模型”的結果進行比較,稱為“外部指標”(external index):如蘭德指數,FM指數 等;
          另一類是直接考察聚類結果而不利用任何參考模型,稱為“內部指標”(internal index):如緊湊度、分離度 等。

        • 2.5.2 模型評估及優化

          根據訓練集及測試集的指標表現,分析原因并對模型進行優化,常用的方法有:

        2.6 模型決策

        決策是機器學習最終目的,對模型預測信息加以分析解釋,并應用于實際的工作領域。

        需要注意的是工程上是結果導向,模型在線上運行的效果直接決定模型的成敗,不僅僅包括其準確程度、誤差等情況,還包括其運行的速度(時間復雜度)、資源消耗程度(空間復雜度)、穩定性的綜合考慮。

        三、 參考文獻

        《機器學習》--周志華

        《統計學習方法》--李航

        Google machine-learning


        關于作者

        歡迎關注“算法進階”公眾號,這里定期推送機器學習、深度學習等技術好文。歡迎一起學習交流進步!

        查看原文

        贊 1 收藏 0 評論 0

        浪里行舟 贊了文章 · 2020-07-24

        程序員說模型過擬合的時候,說的是什么?

        前言

        機器學習中,模型的擬合效果意味著對新數據的預測能力的強弱(泛化能力)。而程序員評價模型擬合效果時,常說“過擬合”及“欠擬合”,那究竟什么是過/欠擬合呢?什么指標可以判斷擬合效果?以及如何優化?

        欠擬合&過擬合的概念

        注:在機器學習或人工神經網絡中,過擬合與欠擬合有時也被稱為“過訓練”和“欠訓練”,本文不做術語差異上的專業區分。

        欠擬合是指相較于數據而言,模型參數過少或者模型結構過于簡單,以至于無法學習到數據中的規律。

        過擬合是指模型只過分地匹配特定數據集,以至于對其他數據無良好地擬合及預測。其本質是模型從訓練數據中學習到了統計噪聲,由此分析影響因素有:

        1. 訓練數據過于局部片面,模型學習到與真實數據不相符的噪音;
        2. 訓練數據的噪音數據干擾過大,大到模型過分記住了噪音特征,反而忽略了真實的輸入輸出間的關系;
        3. 過于復雜的參數或結構模型(相較于數據而言),在可以“完美地”適應數據的同時,也學習更多的噪聲;


        如上圖以虛線的區分效果來形象表示模型的擬合效果。Underfitting代表欠擬合模型,Overfitting代表過擬合模型,Good代表擬合良好的模型。

        擬合效果的評估方式


        現實中通常由訓練誤差及測試誤差(泛化誤差)評估模型的學習程度及泛化能力。

        欠擬合時訓練誤差和測試誤差在均較高,隨著訓練時間及模型復雜度的增加而下降。在到達一個擬合最優的臨界點之后,訓練誤差下降,測試誤差上升,這個時候就進入了過擬合區域。它們的誤差情況差異如下表所示:

        擬合效果的深入分析

        對于擬合效果除了通過訓練、測試的誤差估計其泛化誤差及判斷擬合程度之外,我們往往還希望了解它為什么具有這樣的泛化性能。統計學常用“偏差-方差分解”(bias-variance decomposition)來分析模型的泛化性能:其泛化誤差為偏差、方差與噪聲之和。

        噪聲(ε) 表達了在當前任務上任何學習算法所能達到的泛化誤差的下界,即刻畫了學習問題本身(客觀存在)的難度。

        偏差(Bias) 是指用所有可能的訓練數據集訓練出的所有模型的輸出值與真實值之間的差異,刻畫了模型的擬合能力。偏差較小即模型預測準確度越高,表示模型擬合程度越高。

        方差(Variance) 是指不同的訓練數據集訓練出的模型對同預測樣本輸出值之間的差異,刻畫了訓練數據擾動所造成的影響。方差較大即模型預測值越不穩定,表示模型(過)擬合程度越高,受訓練集擾動影響越大。

        如下用靶心圖形象表示不同方差及偏差下模型預測的差異:

        偏差越小,模型預測值與目標值差異越小,預測值越準確;

        方差越小,不同的訓練數據集訓練出的模型對同預測樣本預測值差異越小,預測值越集中;

        “偏差-方差分解” 說明,模型擬合過程的泛化性能是由學習算法的能力、數據的充分性以及學習任務本身的難度所共同決定的。

        當模型欠擬合時:模型準確度不高(高偏差),受訓練數據的擾動影響較?。ǖ头讲睿?,其泛化誤差大主要由高的偏差導致。

        當模型過擬合時:模型準確度較高(低偏差),模型容易學習到訓練數據擾動的噪音(高方差),其泛化誤差大由高的方差導致。

        擬合效果的優化方法

        可結合交叉驗證評估模型的表現,可較準確判斷擬合程度。在優化欠/過擬合現象上,主要有如下方法:

        模型欠擬合

        • 增加特征維度:如增加新業務層面特征,特征衍生來增大特征假設空間,以增加特征的表達能力;
        • 增加模型復雜度:如增加模型訓練時間、結構復雜度,嘗試復雜非線性模型等,以增加模型的學習能力;

        模型過擬合

        • 增加數據: 如尋找更多訓練數據樣本,數據增強等,以減少對局部數據的依賴;
        • 特征選擇:通過篩選掉冗余特征,減少冗余特征產生噪聲干擾;
        • 降低模型復雜度

          1. 簡化模型結構:如減少神經網絡深度,決策樹的數目等。
          2. L1/L2正則化:通過在代價函數加入正則項(權重整體的值)作為懲罰項,以限制模型學習的權重。

            (拓展:通過在神經網絡的網絡層引入隨機的噪聲,也有類似L2正則化的效果)
        
        1. 提前停止(Early stopping):通過迭代次數截斷的方法,以限制模型學習的權重。

        • 結合多個模型

          1. 集成學習:如隨機森林(bagging法)通過訓練樣本有放回抽樣和隨機特征選擇訓練多個模型,綜合決策,可以減少對部分數據/模型的依賴,減少方差及誤差;
          2. Dropout: 神經網絡的前向傳播過程中每次按一定的概率(比如50%)隨機地“暫?!币徊糠稚窠浽淖饔?。這類似于多種網絡結構模型bagging取平均決策,且模型不會依賴某些局部的特征,從而有更好泛化性能。

        關于作者

        歡迎關注“算法進階”公眾號,這里定期推送機器學習、深度學習等技術好文。歡迎一起學習交流進步!

        查看原文

        贊 1 收藏 1 評論 1

        浪里行舟 關注了用戶 · 2020-07-23

        馬嘉倫 @majialun

        Flutter資深開發者,有多個flutter開源庫獲得高分評價,碼云最有價值開源項目 flutter-p2p-engine代碼主要提供人,長期維護者。博客作者,最高閱讀量50k+,文章曾被谷歌開發者官方公眾號刊登并贈送禮品。

        關注 809

        浪里行舟 贊了文章 · 2020-07-21

        實戰技巧,Vue原來還可以這樣寫

        兩只黃鸝鳴翠柳,一堆bug上西天。

        每天上班寫著重復的代碼,當一個cv仔,忙到八九點,工作效率低,感覺自己沒有任何提升。如何能更快的完成手頭的工作,今天小編整理了一些新的Vue使用技巧。你們先加班,我先下班陪女神去逛街了。

        本文首發于公眾號【前端有的玩】,關注我,我們一起玩前端,每天都有不一樣的干貨知識點等著你哦

        hookEvent,原來可以這樣監聽組件生命周期

        1. 內部監聽生命周期函數

        今天產品經理又給我甩過來一個需求,需要開發一個圖表,拿到需求,瞄了一眼,然后我就去echarts官網復制示例代碼了,復制完改了改差不多了,改完代碼長這樣

        
        <template>
          <div class="echarts"></div>
        </template>
        <script>
          export default {
           mounted() {
             this.chart = echarts.init(this.$el)
              // 請求數據,賦值數據 等等一系列操作...
              // 監聽窗口發生變化,resize組件
             window.addEventListener('resize',this.$_handleResizeChart)
          },
          updated() {
            // 干了一堆活
          },
          created() {
             // 干了一堆活
          },
          beforeDestroy() {
            // 組件銷毀時,銷毀監聽事件
            window.removeEventListener('resize', this.$_handleResizeChart)
          },
          methods: {
            $_handleResizeChart() {
             this.chart.resize()
            },
          // 其他一堆方法
         }
        }
        </script>
        

        功能寫完開開心心的提測了,測試沒啥問題,產品經理表示做的很棒。然而code review時候,技術大佬說了,這樣有問題。

        大佬:這樣寫不是很好,應該將監聽resize事件與銷毀resize事件放到一起,現在兩段代碼分開而且相隔幾百行代碼,可讀性比較差
        我:那我把兩個生命周期鉤子函數位置換一下,放到一起?
        大佬:hook聽過沒?
        我:Vue3.0才有啊,咋,咱要升級Vue?
        

        然后技術大佬就不理我了,并向我扔過來一段代碼

        export default {
          mounted() {
            this.chart = echarts.init(this.$el)
            // 請求數據,賦值數據 等等一系列操作...
            // 監聽窗口發生變化,resize組件
            window.addEventListener('resize', this.$_handleResizeChart)
            // 通過hook監聽組件銷毀鉤子函數,并取消監聽事件
            this.$once('hook:beforeDestroy', () => {
              window.removeEventListener('resize', this.$\_handleResizeChart)
            })
          },
          updated() {},
          created() {},
          methods: {
            $_handleResizeChart() {
              this.chart.resize()
            }
          }
        }

        看完代碼,恍然大悟,大佬不愧是大佬,原來`Vue`還可以這樣監聽生命周期函數。

        _在`Vue`組件中,可以用過`$on\`,\`$once`去監聽所有的生命周期鉤子函數,如監聽組件的`updated`鉤子函數可以寫成 `this.$on('hook:updated', () => {})`_

        2. 外部監聽生命周期函數

        今天同事在公司群里問,想在外部監聽組件的生命周期函數,有沒有辦法???

        為什么會有這樣的需求呢,原來同事用了一個第三方組件,需要監聽第三方組件數據的變化,但是組件又沒有提供change事件,同事也沒辦法了,才想出來要去在外部監聽組件的updated鉤子函數。查看了一番資料,發現Vue支持在外部監聽組件的生命周期鉤子函數。

        <template>
           <!--通過@hook:updated監聽組件的updated生命鉤子函數-->
           <!--組件的所有生命周期鉤子都可以通過@hook:鉤子函數名 來監聽觸發-->
           <custom-select @hook:updated="$_handleSelectUpdated" />
        </template>
        <script>
          import CustomSelect from '../components/custom-select'
          export default {
             components: {
                CustomSelect
             },
           methods: {
             $_handleSelectUpdated() {
               console.log('custom-select組件的updated鉤子函數被觸發')
             }
           }
         }
        </script>

        小項目還用Vuex?用Vue.observable手寫一個狀態管理吧

        在前端項目中,有許多數據需要在各個組件之間進行傳遞共享,這時候就需要有一個狀態管理工具,一般情況下,我們都會使用Vuex,但對于小型項目來說,就像Vuex官網所說:“如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗余的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex”。這時候我們就可以使用Vue2.6提供的新API Vue.observable手動打造一個Vuex

        1. 創建 store

        import Vue from 'vue'
        // 通過Vue.observable創建一個可響應的對象
        export const store = Vue.observable({
          userInfo: {},
          roleIds: []
        })
        // 定義 mutations, 修改屬性
        export const mutations = {
           setUserInfo(userInfo) {
             store.userInfo = userInfo
           },
           setRoleIds(roleIds) {
             store.roleIds = roleIds
           }
        }

        2. 在組件中引用

        <template>
           <div>
             {{ userInfo.name }}
           </div>
        </template>
        <script>
          import { store, mutations } from '../store'
          export default {
            computed: {
              userInfo() {
                return store.userInfo 
              }
           },
           created() {
             mutations.setUserInfo({
               name: '子君'
             })
           }
        }
        </script>

        開發全局組件,你可能需要了解一下Vue.extend

        Vue.extend是一個全局Api,平時我們在開發業務的時候很少會用到它,但有時候我們希望可以開發一些全局組件比如Loading,Notify,Message等組件時,這時候就可以使用Vue.extend。

        同學們在使用element-uiloading時,在代碼中可能會這樣寫

        // 顯示loading
        const loading = this.$loading()
        // 關閉loading
        loading.close()

        這樣寫可能沒什么特別的,但是如果你這樣寫

        const loading = this.$loading()
        const loading1 = this.$loading()
        setTimeout(() => {
          loading.close()
        }, 1000 * 3)

        這時候你會發現,我調用了兩次loading,但是只出現了一個,而且我只關閉了loading,但是loading1也被關閉了。這是怎么實現的呢?我們現在就是用Vue.extend + 單例模式去實現一個loading

        1. 開發loading組件

        <template>
          <transition name="custom-loading-fade">
            <!--loading蒙版-->
            <div v-show="visible" class="custom-loading-mask">
              <!--loading中間的圖標-->
              <div class="custom-loading-spinner">
                <i class="custom-spinner-icon"></i>
                <!--loading上面顯示的文字-->
                <p class="custom-loading-text">{{ text }}</p>
              </div>
            </div>
          </transition>
        </template>
        <script>
        export default {
          props: {
          // 是否顯示loading
            visible: {
              type: Boolean,
              default: false
            },
            // loading上面的顯示文字
            text: {
              type: String,
              default: ''
            }
          }
        }
        </script>

        開發出來loading組件之后,如果需要直接使用,就要這樣去用

        <template>
          <div class="component-code">
            <!--其他一堆代碼-->
            <custom-loading :visible="visible" text="加載中" />
          </div>
        </template>
        <script>
        export default {
          data() {
            return {
              visible: false
            }
          }
        }
        </script>

        但這樣使用并不能滿足我們的需求

        1. 可以通過js直接調用方法來顯示關閉
        2. loading可以將整個頁面全部遮罩起來

        2.通過Vue.extend將組件轉換為全局組件

        1. 改造loading組件,將組件的props改為data

        export default {
          data() {
            return {
              text: '',
              visible: false
            }
          }
        }

        2. 通過Vue.extend改造組件

        // loading/index.js
        import Vue from 'vue'
        import LoadingComponent from './loading.vue'
        
        // 通過Vue.extend將組件包裝成一個子類
        const LoadingConstructor = Vue.extend(LoadingComponent)
        
        let loading = undefined
        
        LoadingConstructor.prototype.close = function() {
          // 如果loading 有引用,則去掉引用
          if (loading) {
            loading = undefined
          }
          // 先將組件隱藏
          this.visible = false
          // 延遲300毫秒,等待loading關閉動畫執行完之后銷毀組件
          setTimeout(() => {
            // 移除掛載的dom元素
            if (this.$el && this.$el.parentNode) {
              this.$el.parentNode.removeChild(this.$el)
            }
            // 調用組件的$destroy方法進行組件銷毀
            this.$destroy()
          }, 300)
        }
        
        const Loading = (options = {}) => {
          // 如果組件已渲染,則返回即可
          if (loading) {
            return loading
          }
          // 要掛載的元素
          const parent = document.body
          // 組件屬性
          const opts = {
            text: '',
            ...options
          }
          // 通過構造函數初始化組件 相當于 new Vue()
          const instance = new LoadingConstructor({
            el: document.createElement('div'),
            data: opts
          })
          // 將loading元素掛在到parent上面
          parent.appendChild(instance.$el)
          // 顯示loading
          Vue.nextTick(() => {
            instance.visible = true
          })
          // 將組件實例賦值給loading
          loading = instance
          return instance
        }
        
        export default Loading

        3. 在頁面使用loading

        import Loading from './loading/index.js'
        export default {
          created() {
            const loading = Loading({ text: '正在加載。。。' })
            // 三秒鐘后關閉
            setTimeout(() => {
              loading.close()
            }, 3000)
          }
        }

        通過上面的改造,loading已經可以在全局使用了,如果需要像element-ui一樣掛載到Vue.prototype上面,通過this.$loading調用,還需要改造一下

        4. 將組件掛載到Vue.prototype上面

        Vue.prototype.$loading = Loading
        // 在export之前將Loading方法進行綁定
        export default Loading
        
        // 在組件內使用
        this.$loading()

        自定義指令,從底層解決問題

        什么是指令?指令就是你女朋友指著你說,“那邊搓衣板,跪下,這是命令!”。開玩笑啦,程序員哪里會有女朋友。

        通過上一節我們開發了一個loading組件,開發完之后,其他開發在使用的時候又提出來了兩個需求

        1. 可以將loading掛載到某一個元素上面,現在只能是全屏使用
        2. 可以使用指令在指定的元素上面掛載loading

        有需求,咱就做,沒話說

        1.開發v-loading指令

        import Vue from 'vue'
        import LoadingComponent from './loading'
        // 使用 Vue.extend構造組件子類
        const LoadingContructor = Vue.extend(LoadingComponent)
        
        // 定義一個名為loading的指令
        Vue.directive('loading', {
          /**
           * 只調用一次,在指令第一次綁定到元素時調用,可以在這里做一些初始化的設置
           * @param {*} el 指令要綁定的元素
           * @param {*} binding 指令傳入的信息,包括 {name:'指令名稱', value: '指令綁定的值',arg: '指令參數 v-bind:text 對應 text'}
           */
          bind(el, binding) {
            const instance = new LoadingContructor({
              el: document.createElement('div'),
              data: {}
            })
            el.appendChild(instance.$el)
            el.instance = instance
            Vue.nextTick(() => {
              el.instance.visible = binding.value
            })
          },
          /**
           * 所在組件的 VNode 更新時調用
           * @param {*} el
           * @param {*} binding
           */
          update(el, binding) {
            // 通過對比值的變化判斷loading是否顯示
            if (binding.oldValue !== binding.value) {
              el.instance.visible = binding.value
            }
          },
          /**
           * 只調用一次,在 指令與元素解綁時調用
           * @param {*} el
           */
          unbind(el) {
            const mask = el.instance.$el
            if (mask.parentNode) {
              mask.parentNode.removeChild(mask)
            }
            el.instance.$destroy()
            el.instance = undefined
          }
        })

        2.在元素上面使用指令

        <template>
          <div v-loading="visible"></div>
        </template>
        <script>
        export default {
          data() {
            return {
              visible: false
            }
          },
          created() {
            this.visible = true
            fetch().then(() => {
              this.visible = false
            })
          }
        }
        </script>
        

        3.項目中哪些場景可以自定義指令

        1. 為組件添加loading效果
        2. 按鈕級別權限控制 v-permission
        3. 代碼埋點,根據操作類型定義指令
        4. input輸入框自動獲取焦點
        5. 其他等等。。。

        深度watchwatch立即觸發回調,我可以監聽到你的一舉一動

        在開發Vue項目時,我們會經常性的使用到watch去監聽數據的變化,然后在變化之后做一系列操作。

        1.基礎用法

        比如一個列表頁,我們希望用戶在搜索框輸入搜索關鍵字的時候,可以自動觸發搜索,此時除了監聽搜索框的change事件之外,我們也可以通過watch監聽搜索關鍵字的變化

        <template>
          <!--此處示例使用了element-ui-->
          <div>
            <div>
              <span>搜索</span>
              <input v-model="searchValue" />
            </div>
            <!--列表,代碼省略-->
          </div>
        </template>
        <script>
        export default {
          data() {
            return {
              searchValue: ''
            }
          },
          watch: {
            // 在值發生變化之后,重新加載數據
            searchValue(newValue, oldValue) {
              // 判斷搜索
              if (newValue !== oldValue) {
                this.$_loadData()
              }
            }
          },
          methods: {
            $_loadData() {
              // 重新加載數據,此處需要通過函數防抖
            }
          }
        }
        </script>

        2.立即觸發

        通過上面的代碼,現在已經可以在值發生變化的時候觸發加載數據了,但是如果要在頁面初始化時候加載數據,我們還需要在created或者mounted生命周期鉤子里面再次調用$_loadData方法。不過,現在可以不用這樣寫了,通過配置watch的立即觸發屬性,就可以滿足需求了

        // 改造watch
        export default {
          watch: {
            // 在值發生變化之后,重新加載數據
            searchValue: {
            // 通過handler來監聽屬性變化, 初次調用 newValue為""空字符串, oldValue為 undefined
              handler(newValue, oldValue) {
                if (newValue !== oldValue) {
                  this.$_loadData()
                }
              },
              // 配置立即執行屬性
              immediate: true
            }
          }
        }

        3.深度監聽(我可以看到你內心的一舉一動)

        一個表單頁面,需求希望用戶在修改表單的任意一項之后,表單頁面就需要變更為被修改狀態。如果按照上例中watch的寫法,那么我們就需要去監聽表單每一個屬性,太麻煩了,這時候就需要用到watch的深度監聽deep

        export default {
          data() {
            return {
              formData: {
                name: '',
                sex: '',
                age: 0,
                deptId: ''
              }
            }
          },
          watch: {
            // 在值發生變化之后,重新加載數據
            formData: {
              // 需要注意,因為對象引用的原因, newValue和oldValue的值一直相等
              handler(newValue, oldValue) {
                // 在這里標記頁面編輯狀態
              },
              // 通過指定deep屬性為true, watch會監聽對象里面每一個值的變化
              deep: true
            }
          }
        }
        

        隨時監聽,隨時取消,了解一下$watch

        有這樣一個需求,有一個表單,在編輯的時候需要監聽表單的變化,如果發生變化則保存按鈕啟用,否則保存按鈕禁用。這時候對于新增表單來說,可以直接通過watch去監聽表單數據(假設是formData),如上例所述,但對于編輯表單來說,表單需要回填數據,這時候會修改formData的值,會觸發watch,無法準確的判斷是否啟用保存按鈕?,F在你就需要了解一下$watch

        export default {
          data() {
            return {
              formData: {
                name: '',
                age: 0
              }
            }
          },
          created() {
            this.$_loadData()
          },
          methods: {
            // 模擬異步請求數據
            $_loadData() {
              setTimeout(() => {
                // 先賦值
                this.formData = {
                  name: '子君',
                  age: 18
                }
                // 等表單數據回填之后,監聽數據是否發生變化
                const unwatch = this.$watch(
                  'formData',
                  () => {
                    console.log('數據發生了變化')
                  },
                  {
                    deep: true
                  }
                )
                // 模擬數據發生了變化
                setTimeout(() => {
                  this.formData.name = '張三'
                }, 1000)
              }, 1000)
            }
          }
        }

        根據上例可以看到,我們可以在需要的時候通過this.$watch來監聽數據變化。那么如何取消監聽呢,上例中this.$watch返回了一個值unwatch,是一個函數,在需要取消的時候,執行 unwatch()即可取消

        函數式組件,函數是組件?

        什么是函數式組件?函數式組件就是函數是組件,感覺在玩文字游戲。使用過React的同學,應該不會對函數式組件感到陌生。函數式組件,我們可以理解為沒有內部狀態,沒有生命周期鉤子函數,沒有this(不需要實例化的組件)。

        在日常寫bug的過程中,經常會開發一些純展示性的業務組件,比如一些詳情頁面,列表界面等,它們有一個共同的特點是只需要將外部傳入的數據進行展現,不需要有內部狀態,不需要在生命周期鉤子函數里面做處理,這時候你就可以考慮使用函數式組件。

        1. 先來一個函數式組件的代碼

        export default {
          // 通過配置functional屬性指定組件為函數式組件
          functional: true,
          // 組件接收的外部屬性
          props: {
            avatar: {
              type: String
            }
          },
          /**
           * 渲染函數
           * @param {*} h
           * @param {*} context 函數式組件沒有this, props, slots等都在context上面掛著
           */
          render(h, context) {
            const { props } = context
            if (props.avatar) {
              return <img data-original={props.avatar}></img>
            }
            return <img data-original="default-avatar.png"></img>
          }
        }

        在上例中,我們定義了一個頭像組件,如果外部傳入頭像,則顯示傳入的頭像,否則顯示默認頭像。上面的代碼中大家看到有一個render函數,這個是Vue使用JSX的寫法,關于JSX,小編將在后續文章中會出詳細的使用教程。

        2.為什么使用函數式組件

        1. 最主要最關鍵的原因是函數式組件不需要實例化,無狀態,沒有生命周期,所以渲染性能要好于普通組件
        2. 函數式組件結構比較簡單,代碼結構更清晰

        3. 函數式組件與普通組件的區別

        1. 函數式組件需要在聲明組件是指定functional
        2. 函數式組件不需要實例化,所以沒有this,this通過render函數的第二個參數來代替
        3. 函數式組件沒有生命周期鉤子函數,不能使用計算屬性,watch等等
        4. 函數式組件不能通過$emit對外暴露事件,調用事件只能通過context.listeners.click的方式調用外部傳入的事件
        5. 因為函數式組件是沒有實例化的,所以在外部通過ref去引用組件時,實際引用的是HTMLElement
        6. 函數式組件的props可以不用顯示聲明,所以沒有在props里面聲明的屬性都會被自動隱式解析為prop,而普通組件所有未聲明的屬性都被解析到$attrs里面,并自動掛載到組件根元素上面(可以通過inheritAttrs屬性禁止)

        4.我不想用JSX,能用函數式組件嗎?

        Vue2.5之前,使用函數式組件只能通過JSX的方式,在之后,可以通過模板語法來生命函數式組件

        <!--在template 上面添加 functional屬性-->
        <template functional>
          <img :data-original="props.avatar ? props.avatar : 'default-avatar.png'" />
        </template>
        <!--根據上一節第六條,可以省略聲明props-->
        

        結語:

        不要吹滅你的靈感和你的想象力; 不要成為你的模型的奴隸。 ——文森特?梵高

        掃碼關注,愿你遇到那個心疼你付出的人~

        微信公眾號宣傳圖.gif

        查看原文

        贊 48 收藏 32 評論 3

        浪里行舟 關注了用戶 · 2020-07-21

        子君 @zijun_5f156624be160

        微信公眾號: 前端有得玩
        微信賬號:snowzijun
        github倉庫: https://github.com/snowzijun
        寄語:不要吹滅你的靈感和你的想象力; 不要成為你的模型的奴隸。

        關注 562

        浪里行舟 關注了用戶 · 2020-07-17

        Linmi @linmi

        Segmentfault Community Support | Notion Pro


        一對一免費提供內容選題、寫作指導 → 微信 linmib

        關注 19

        浪里行舟 贊了文章 · 2020-07-15

        專業團隊支持、全網流量扶持,思否編程技術講師賦能計劃啟動 | 首期招募限 20 位

        很多技術人員都有一個三尺講臺夢,希望分享自己曾踩過的坑幫助年輕人。

        很多技術人員都在嘗試沉淀技術內容,建立技術興趣圈,互相促進交流,提升個人品牌。

        當經濟形勢持續惡化,越來越多職場人開始探索知識變現。關注高薪副業,在業余時間增加收入。

        996,項目不斷,人少事兒多……也許你一直蠢蠢欲動卻沒邁出第一步。

        其實你的忙碌,我們都懂!

        首頁banner.png

        思否編程針對技術人才推出了思否編程技術講師賦能計劃,專為技術人員晉升講師量身定制。首期僅限20位!?(立即申請

        我們將提供授課技巧、流量扶持等多維度賦能,專業課程策劃、編輯、運營團隊為你服務,確保每一位技術人員快速完成講師的華麗晉升。

        心動了?一起探索更多可能!

        ?無需課程經驗,只需具備:
        • 每周 2-3 小時的空閑時間
        • 工作經驗 5 年+,擅長某一技術方向,具備突出項目經驗者可適當放寬
        • 熱愛分享,愿意幫助更多的開發者
        • 技術博主/圖書作者/公眾號主等,思否社區用戶優先
        ??享受多維度運營支持,具體包括:
        • 流量支持:公眾號百萬粉絲矩陣,站內千萬級流量
        • 課程策劃:為你量身定制課程選題,與你一起打磨課程大綱
        • 課程編輯:專業視頻剪輯包裝,資深編輯團隊,與你一起優化教學材料
        • 課程包裝:結合講師和課程特點提供全方位課程包裝,并進行配套宣傳物料設計
        • 推廣方案:專屬售賣策略和講師打造方案
        • 個人包裝:SegmentFault 技術媒體訪談包裝,有機會被推薦為頂級技術大會講師、嘉賓評委,大大提升個人品牌影響力。(思否獨有福利 (^o^)/
        ?短期內快速提升自已,獲得豐厚收獲:
        • 授課技能:幫助你快速成為具備授課技能的講師,對工作中帶新人也將受益匪淺。
        • 學習成長:與平臺大咖講師交流切磋,與學員分享知識的同時,不斷自我總結成長。
        • 粉絲人氣:快速建立自己粉絲圈,提升業界的影響力。
        • 課程收入:爆款課程將有不菲收入,每月也將有穩定的課程分成。
        ?急需技術方向:
        • 前端框架、性能優化等前端技術
        • Python、Java 等后端技術
        • 算法、數據挖掘、機器學習等人工智能方向
        • 技術選型、代碼優化、網站架構、各類實戰項目等
        ?優秀課程案例:

        優秀案例.png

        課程1:凱威教你學 Python: 系列課程
        課程2:PHP 進階之路
        更多重磅課程,請訪問:https://ke.www.tvxinternet.com

        從業者就業壓力增大,在線培訓成為多數人快速提升技能的有效途徑之一。

        思否編程依托 SegmentFault 思否 8 年行業積累和每月數千萬精準流量,為開發者提供高質量的技術干貨課程,解決工作中遇到的問題,快速成長。

        課程將通過多樣化的營銷手段觸達到更多用戶,產生高額營收和廣泛影響力。

        6??0??天晉升技術講師,你準備好了嗎??(立即申請

        更多咨詢,請添加以下好友,等你呦?!

        image

        查看原文

        贊 8 收藏 0 評論 0

        浪里行舟 贊了文章 · 2020-07-14

        手寫一個Promise/A+,完美通過官方872個測試用例

        Promise幾乎是面試必考點,所以我們不能僅僅會用,還得知道他的底層原理,學習他原理的最好方法就是自己也實現一個Promise。所以本文會自己實現一個遵循Promise/A+規范的Promise。實現之后,我們還要用Promise/A+官方的測試工具來測試下我們的實現是否正確,這個工具總共有872個測試用例,全部通過才算是符合Promise/A+規范,下面是他們的鏈接:

        Promise/A+規范: https://github.com/promises-aplus/promises-spec

        Promise/A+測試工具: https://github.com/promises-aplus/promises-tests

        本文的完整代碼托管在GitHub上: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js

        Promise用法

        Promise的基本用法,網上有很多,我這里簡單提一下,我還是用三個相互依賴的網絡請求做例子,假如我們有三個網絡請求,請求2必須依賴請求1的結果,請求3必須依賴請求2的結果,如果用回調的話會有三層,會陷入“回調地獄”,用Promise就清晰多了:

        const request = require("request");
        
        // 我們先用Promise包裝下三個網絡請求
        // 請求成功時resolve這個Promise
        const request1 = function() {
          const promise = new Promise((resolve) => {
            request('https://www.baidu.com', function (error, response) {
              if (!error && response.statusCode == 200) {
                resolve('request1 success');
              }
            });
          });
        
          return promise;
        }
        
        const request2 = function() {
          const promise = new Promise((resolve) => {
            request('https://www.baidu.com', function (error, response) {
              if (!error && response.statusCode == 200) {
                resolve('request2 success');
              }
            });
          });
        
          return promise;
        }
        
        const request3 = function() {
          const promise = new Promise((resolve) => {
            request('https://www.baidu.com', function (error, response) {
              if (!error && response.statusCode == 200) {
                resolve('request3 success');
              }
            });
          });
        
          return promise;
        }
        
        
        // 先發起request1,等他resolve后再發起request2,
        // 然后是request3
        request1().then((data) => {
          console.log(data);
          return request2();
        })
        .then((data) => {
          console.log(data);
          return request3();
        })
        .then((data) => {
          console.log(data);
        })

        上面的例子里面,then是可以鏈式調用的,后面的then可以拿到前面resolve出來的數據,我們控制臺可以看到三個success依次打出來:

        image-20200324164123892

        Promises/A+規范

        通過上面的例子,其實我們已經知道了一個promise長什么樣子,Promises/A+規范其實就是對這個長相進一步進行了規范。下面我會對這個規范進行一些講解。

        術語

        1. promise:是一個擁有 then 方法的對象或函數,其行為符合本規范
        2. thenable:是一個定義了 then 方法的對象或函數。這個主要是用來兼容一些老的Promise實現,只要一個Promise實現是thenable,也就是擁有then方法的,就可以跟Promises/A+兼容。
        3. value:指reslove出來的值,可以是任何合法的JS值(包括 undefined , thenable 和 promise等)
        4. exception:異常,在Promise里面用throw拋出來的值
        5. reason:拒絕原因,是reject里面傳的參數,表示reject的原因

        Promise狀態

        Promise總共有三個狀態:

        1. pending: 一個promise在resolve或者reject前就處于這個狀態。
        2. fulfilled: 一個promise被resolve后就處于fulfilled狀態,這個狀態不能再改變,而且必須擁有一個不可變的值(value)。
        3. rejected: 一個promise被reject后就處于rejected狀態,這個狀態也不能再改變,而且必須擁有一個不可變的拒絕原因(reason)。

        注意這里的不可變指的是===,也就是說,如果value或者reason是對象,只要保證引用不變就行,規范沒有強制要求里面的屬性也不變。Promise狀態其實很簡單,畫張圖就是:

        image-20200324173555225

        then方法

        一個promise必須擁有一個then方法來訪問他的值或者拒絕原因。then方法有兩個參數:

        promise.then(onFulfilled, onRejected)

        參數可選

        onFulfilledonRejected 都是可選參數。

        • 如果 onFulfilled 不是函數,其必須被忽略
        • 如果 onRejected 不是函數,其必須被忽略

        onFulfilled 特性

        如果 onFulfilled 是函數:

        • promise 執行結束后其必須被調用,其第一個參數為 promise 的終值value
        • promise 執行結束前其不可被調用
        • 其調用次數不可超過一次

        onRejected 特性

        如果 onRejected 是函數:

        • promise 被拒絕執行后其必須被調用,其第一個參數為 promise 的據因reason
        • promise 被拒絕執行前其不可被調用
        • 其調用次數不可超過一次

        多次調用

        then 方法可以被同一個 promise 調用多次

        • promise 成功執行時,所有 onFulfilled 需按照其注冊順序依次回調
        • promise 被拒絕執行時,所有的 onRejected 需按照其注冊順序依次回調

        返回

        then 方法必須返回一個 promise 對象。

        promise2 = promise1.then(onFulfilled, onRejected); 
        • 如果 onFulfilled 或者 onRejected 返回一個值 x ,則運行 Promise 解決過程[[Resolve]](promise2, x)
        • 如果 onFulfilled 或者 onRejected 拋出一個異常 e ,則 promise2 必須拒絕執行,并返回拒因 e
        • 如果 onFulfilled 不是函數且 promise1 成功執行, promise2 必須成功執行并返回相同的值
        • 如果 onRejected 不是函數且 promise1 拒絕執行, promise2 必須拒絕執行并返回相同的據因

        規范里面還有很大一部分是講解Promise 解決過程的,光看規范,很空洞,前面這些規范已經可以指導我們開始寫一個自己的Promise了,Promise 解決過程會在我們后面寫到了再詳細講解。

        自己寫一個Promise

        我們自己要寫一個Promise,肯定需要知道有哪些工作需要做,我們先從Promise的使用來窺探下需要做啥:

        1. 新建Promise需要使用new關鍵字,那他肯定是作為面向對象的方式調用的,Promise是一個類。關于JS的面向對象更詳細的解釋可以看這篇文章。
        2. 我們new Promise(fn)的時候需要傳一個函數進去,說明Promise的參數是一個函數
        3. 構造函數傳進去的fn會收到resolvereject兩個函數,用來表示Promise成功和失敗,說明構造函數里面還需要resolvereject這兩個函數,這兩個函數的作用是改變Promise的狀態。
        4. 根據規范,promise有pending,fulfilled,rejected三個狀態,初始狀態為pending,調用resolve會將其改為fulfilled,調用reject會改為rejected。
        5. promise實例對象建好后可以調用then方法,而且是可以鏈式調用then方法,說明then是一個實例方法。鏈式調用的實現這篇有詳細解釋,我這里不再贅述。簡單的說就是then方法也必須返回一個帶then方法的對象,可以是this或者新的promise實例。

        構造函數

        為了更好的兼容性,本文就不用ES6了。

        // 先定義三個常量表示狀態
        var PENDING = 'pending';
        var FULFILLED = 'fulfilled';
        var REJECTED = 'rejected';
        
        function MyPromise(fn) {
          this.status = PENDING;    // 初始狀態為pending
          this.value = null;        // 初始化value
          this.reason = null;       // 初始化reason
        }

        resolvereject方法

        根據規范,resolve方法是將狀態改為fulfilled,reject是將狀態改為rejected。

        // 這兩個方法直接寫在構造函數里面
        function MyPromise(fn) {
          // ...省略前面代碼...
          
          // 存一下this,以便resolve和reject里面訪問
          var that = this;
          // resolve方法參數是value
          function resolve(value) {
            if(that.status === PENDING) {
              that.status = FULFILLED;
              that.value = value;
            }
          }
          
          // reject方法參數是reason
          function reject(reason) {
            if(that.status === PENDING) {
              that.status = REJECTED;
              that.reason = reason;
            }
          }
        }

        調用構造函數參數

        最后將resolvereject作為參數調用傳進來的參數,記得加上try,如果捕獲到錯誤就reject。

        function MyPromise(fn) {
          // ...省略前面代碼...
          
          try {
            fn(resolve, reject);
          } catch (error) {
            reject(error);
          }
        }

        then方法

        根據我們前面的分析,then方法可以鏈式調用,所以他是實例方法,而且規范中的API是promise.then(onFulfilled, onRejected),我們先把架子搭出來:

        MyPromise.prototype.then = function(onFulfilled, onRejected) {}

        then方法里面應該干什么呢,其實規范也告訴我們了,先檢查onFulfilledonRejected是不是函數,如果不是函數就忽略他們,所謂“忽略”并不是什么都不干,對于onFulfilled來說“忽略”就是將value原封不動的返回,對于onRejected來說就是返回reason,onRejected因為是錯誤分支,我們返回reason應該throw一個Error:

        MyPromise.prototype.then = function(onFulfilled, onRejected) {
          // 如果onFulfilled不是函數,給一個默認函數,返回value
          var realOnFulfilled = onFulfilled;
          if(typeof realOnFulfilled !== 'function') {
            realOnFulfilled = function (value) {
              return value;
            }
          }
        
          // 如果onRejected不是函數,給一個默認函數,返回reason的Error
          var realOnRejected = onRejected;
          if(typeof realOnRejected !== 'function') {
            realOnRejected = function (reason) {
              throw reason;
            }
          }
        }

        參數檢查完后就該干點真正的事情了,想想我們使用Promise的時候,如果promise操作成功了就會調用then里面的onFulfilled,如果他失敗了,就會調用onRejected。對應我們的代碼就應該檢查下promise的status,如果是FULFILLED,就調用onFulfilled,如果是REJECTED,就調用onRejected:

        MyPromise.prototype.then = function(onFulfilled, onRejected) {
          // ...省略前面代碼...
        
          if(this.status === FULFILLED) {
            onFulfilled(this.value)
          }
        
          if(this.status === REJECTED) {
            onRejected(this.reason);
          }
        }

        再想一下,我們新建一個promise的時候可能是直接這樣用的:

        new Promise(fn).then(onFulfilled, onRejected);

        上面代碼then是在實例對象一創建好就調用了,這時候fn里面的異步操作可能還沒結束呢,也就是說他的status還是PENDING,這怎么辦呢,這時候我們肯定不能立即調onFulfilled或者onRejected的,因為fn到底成功還是失敗還不知道呢。那什么時候知道fn成功還是失敗呢?答案是fn里面主動調resolve或者reject的時候。所以如果這時候status狀態還是PENDING,我們應該將onFulfilledonRejected兩個回調存起來,等到fn有了結論,resolve或者reject的時候再來調用對應的代碼。因為后面then還有鏈式調用,會有多個onFulfilledonRejected,我這里用兩個數組將他們存起來,等resolve或者reject的時候將數組里面的全部方法拿出來執行一遍

        // 構造函數
        function MyPromise(fn) {
          // ...省略其他代碼...
          
          // 構造函數里面添加兩個數組存儲成功和失敗的回調
          this.onFulfilledCallbacks = [];
          this.onRejectedCallbacks = [];
          
          function resolve(value) {
            if(that.status === PENDING) {
              // ...省略其他代碼...
              // resolve里面將所有成功的回調拿出來執行
              that.onFulfilledCallbacks.forEach(callback => {
                callback(that.value);
              });
            }
          }
          
          function reject(reason) {
            if(that.status === PENDING) {
              // ...省略其他代碼...
              // resolve里面將所有失敗的回調拿出來執行
              that.onRejectedCallbacks.forEach(callback => {
                callback(that.reason);
              });
            }
          }
        }
        
        // then方法
        MyPromise.prototype.then = function(onFulfilled, onRejected) {
          // ...省略其他代碼...
        
          // 如果還是PENDING狀態,將回調保存下來
          if(this.status === PENDING) {
            this.onFulfilledCallbacks.push(realOnFulfilled);
            this.onRejectedCallbacks.push(realOnRejected);
          }
        }

        上面這種暫時將回調保存下來,等條件滿足的時候再拿出來運行讓我想起了一種模式:訂閱發布模式。我們往回調數組里面push回調函數,其實就相當于往事件中心注冊事件了,resolve就相當于發布了一個成功事件,所有注冊了的事件,即onFulfilledCallbacks里面的所有方法都會拿出來執行,同理reject就相當于發布了一個失敗事件。更多訂閱發布模式的原理可以看這里。

        完成了一小步

        到這里為止,其實我們已經可以實現異步調用了,只是then的返回值還沒實現,還不能實現鏈式調用,我們先來玩一下:

        var request = require("request");
        var MyPromise = require('./MyPromise');
        
        var promise1 = new MyPromise((resolve) => {
          request('https://www.baidu.com', function (error, response) {
            if (!error && response.statusCode == 200) {
              resolve('request1 success');
            }
          });
        });
        
        promise1.then(function(value) {
          console.log(value);
        });
        
        var promise2 = new MyPromise((resolve, reject) => {
          request('https://www.baidu.com', function (error, response) {
            if (!error && response.statusCode == 200) {
              reject('request2 failed');
            }
          });
        });
        
        promise2.then(function(value) {
          console.log(value);
        }, function(reason) {
          console.log(reason);
        });

        上述代碼輸出如下圖,符合我們的預期,說明到目前為止,我們的代碼都沒問題:

        image-20200325172257655

        then的返回值

        根據規范then的返回值必須是一個promise,規范還定義了不同情況應該怎么處理,我們先來處理幾種比較簡單的情況:

        1. 如果 onFulfilled 或者 onRejected 拋出一個異常 e ,則 promise2 必須拒絕執行,并返回拒因 e。
        MyPromise.prototype.then = function(onFulfilled, onRejected) {
            // ... 省略其他代碼 ...
          
          // 有了這個要求,在RESOLVED和REJECTED的時候就不能簡單的運行onFulfilled和onRejected了。
          // 我們需要將他們用try...catch...包起來,如果有錯就reject。
          if(this.status === FULFILLED) {
            var promise2 = new MyPromise(function(resolve, reject) {
              try {
                realOnFulfilled(that.value);
              } catch (error) {
                reject(error);
              }
            });
          
            return promise2;
          }
        
          if(this.status === REJECTED) {
            var promise2 = new MyPromise(function(resolve, reject) {
              try {
                realOnRejected(that.reason);
              } catch (error) {
                reject(error);
              }
            });
          
            return promise2;
          }
          
          // 如果還是PENDING狀態,也不能直接保存回調方法了,需要包一層來捕獲錯誤
          if(this.status === PENDING) {
            var promise2 = new MyPromise(function(resolve, reject) {
              that.onFulfilledCallbacks.push(function() {
                try {
                  realOnFulfilled(that.value);
                } catch (error) {
                  reject(error);
                }
              });
              that.onRejectedCallbacks.push(function() {
                try {
                  realOnRejected(that.reason);
                } catch (error) {
                  reject(error);
                }
              });
            });
          
            return promise2;
          }
        }
        1. 如果 onFulfilled 不是函數且 promise1 成功執行, promise2 必須成功執行并返回相同的值
        // 我們就根據要求加個判斷,注意else里面是正常執行流程,需要resolve
        // 這是個例子,每個realOnFulfilled后面都要這樣寫
          if(this.status === FULFILLED) {
            var promise2 = new MyPromise(function(resolve, reject) {
              try {
                if (typeof onFulfilled !== 'function') {
                  resolve(that.value);
                } else {
                  realOnFulfilled(that.value);
                  resolve(that.value);
                }
              } catch (error) {
                reject(error);
              }
            });
          
            return promise2;
          }
        1. 如果 onRejected 不是函數且 promise1 拒絕執行, promise2 必須拒絕執行并返回相同的據因。這個要求其實在我們檢測 onRejected 不是函數的時候已經做到了,因為我們默認給的onRejected里面會throw一個Error,所以代碼肯定會走到catch里面去。但是我們為了更直觀,代碼還是跟規范一一對應吧。需要注意的是,如果promise1onRejected執行成功了,promise2應該被resolve。改造代碼如下:
          if(this.status === REJECTED) {
            var promise2 = new MyPromise(function(resolve, reject) {
              try {
                if(typeof onRejected !== 'function') {
                  reject(that.reason);
                } else {
                  realOnRejected(that.reason);
                  resolve();
                }
              } catch (error) {
                reject(error);
              }
            });
          
            return promise2;
          }
        1. 如果 onFulfilled 或者 onRejected 返回一個值 x ,則運行下面的 Promise 解決過程[[Resolve]](promise2, x)。這條其實才是規范的第一條,因為他比較麻煩,所以我將它放到了最后。前面我們代碼的實現,其實只要onRejected或者onFulfilled成功執行了,我們都要resolve promise2。多了這條,我們還需要對onRejected或者onFulfilled的返回值進行判斷,如果有返回值就要進行 Promise 解決過程。我們專門寫一個方法來進行Promise 解決過程。前面我們代碼的實現,其實只要onRejected或者onFulfilled成功執行了,我們都要resolve promise2,這個過程我們也放到這個方法里面去吧,所以代碼變為下面這樣,其他地方類似:
          if(this.status === FULFILLED) {
            var promise2 = new MyPromise(function(resolve, reject) {
              try {
                if (typeof onFulfilled !== 'function') {
                  resolve(that.value);
                } else {
                  var x = realOnFulfilled(that.value);
                  resolvePromise(promise2, x, resolve, reject);   // 調用Promise 解決過程
                }
              } catch (error) {
                reject(error);
              }
            });
          
            return promise2;
          }

        Promise 解決過程

        現在我們該來實現resolvePromise方法了,規范中這一部分較長,我就直接把規范作為注釋寫在代碼里面了。

        function resolvePromise(promise, x, resolve, reject) {
          // 如果 promise 和 x 指向同一對象,以 TypeError 為據因拒絕執行 promise
          // 這是為了防止死循環
          if (promise === x) {
            return reject(new TypeError('The promise and the return value are the same'));
          }
        
          if (x instanceof MyPromise) {
            // 如果 x 為 Promise ,則使 promise 接受 x 的狀態
            // 也就是繼續執行x,如果執行的時候拿到一個y,還要繼續解析y
            // 這個if跟下面判斷then然后拿到執行其實重復了,可有可無
            x.then(function (y) {
              resolvePromise(promise, y, resolve, reject);
            }, reject);
          }
          // 如果 x 為對象或者函數
          else if (typeof x === 'object' || typeof x === 'function') {
            // 這個坑是跑測試的時候發現的,如果x是null,應該直接resolve
            if (x === null) {
              return resolve(x);
            }
        
            try {
              // 把 x.then 賦值給 then 
              var then = x.then;
            } catch (error) {
              // 如果取 x.then 的值時拋出錯誤 e ,則以 e 為據因拒絕 promise
              return reject(error);
            }
        
            // 如果 then 是函數
            if (typeof then === 'function') {
              var called = false;
              // 將 x 作為函數的作用域 this 調用之
              // 傳遞兩個回調函數作為參數,第一個參數叫做 resolvePromise ,第二個參數叫做 rejectPromise
              // 名字重名了,我直接用匿名函數了
              try {
                then.call(
                  x,
                  // 如果 resolvePromise 以值 y 為參數被調用,則運行 [[Resolve]](promise, y)
                  function (y) {
                    // 如果 resolvePromise 和 rejectPromise 均被調用,
                    // 或者被同一參數調用了多次,則優先采用首次調用并忽略剩下的調用
                    // 實現這條需要前面加一個變量called
                    if (called) return;
                    called = true;
                    resolvePromise(promise, y, resolve, reject);
                  },
                  // 如果 rejectPromise 以據因 r 為參數被調用,則以據因 r 拒絕 promise
                  function (r) {
                    if (called) return;
                    called = true;
                    reject(r);
                  });
              } catch (error) {
                // 如果調用 then 方法拋出了異常 e:
                // 如果 resolvePromise 或 rejectPromise 已經被調用,則忽略之
                if (called) return;
        
                // 否則以 e 為據因拒絕 promise
                reject(error);
              }
            } else {
              // 如果 then 不是函數,以 x 為參數執行 promise
              resolve(x);
            }
          } else {
            // 如果 x 不為對象或者函數,以 x 為參數執行 promise
            resolve(x);
          }
        }

        onFulfilledonRejected 的執行時機

        在規范中還有一條:onFulfilledonRejected 只有在執行環境堆棧僅包含平臺代碼時才可被調用。這一條的意思是實踐中要確保 onFulfilledonRejected 方法異步執行,且應該在 then 方法被調用的那一輪事件循環之后的新執行棧中執行。所以在我們執行onFulfilledonRejected的時候都應該包到setTimeout里面去。

        // 這塊代碼在then里面
        if(this.status === FULFILLED) {
          var promise2 = new MyPromise(function(resolve, reject) {
            // 這里加setTimeout
            setTimeout(function() {
              try {
                if (typeof onFulfilled !== 'function') {
                  resolve(that.value);
                } else {
                  var x = realOnFulfilled(that.value);
                  resolvePromise(promise2, x, resolve, reject);
                }
              } catch (error) {
                reject(error);
              }
            }, 0);
          });
        
          return promise2;
        }
        
        if(this.status === REJECTED) {
          var promise2 = new MyPromise(function(resolve, reject) {
            // 這里加setTimeout
            setTimeout(function() {
              try {
                if(typeof onRejected !== 'function') {
                  reject(that.reason);
                } else {
                  var x = realOnRejected(that.reason);
                  resolvePromise(promise2, x, resolve, reject);
                }
              } catch (error) {
                reject(error);
              }
            }, 0);
          });
        
          return promise2;
        }
        
        if (this.status === PENDING) {
          var promise2 = new MyPromise(function (resolve, reject) {
            that.onFulfilledCallbacks.push(function () {
              // 這里加setTimeout
              setTimeout(function () {
                try {
                  if (typeof onFulfilled !== 'function') {
                    resolve(that.value);
                  } else {
                    var x = realOnFulfilled(that.value);
                    resolvePromise(promise2, x, resolve, reject);
                  }
                } catch (error) {
                  reject(error);
                }
              }, 0);
            });
            that.onRejectedCallbacks.push(function () {
              // 這里加setTimeout
              setTimeout(function () {
                try {
                  if (typeof onRejected !== 'function') {
                    reject(that.reason);
                  } else {
                    var x = realOnRejected(that.reason);
                    resolvePromise(promise2, x, resolve, reject);
                  }
                } catch (error) {
                  reject(error);
                }
              }, 0)
            });
          });
        
          return promise2;
        }

        測試我們的Promise

        我們使用Promise/A+官方的測試工具promises-aplus-tests來對我們的MyPromise進行測試,要使用這個工具我們必須實現一個靜態方法deferred,官方對這個方法的定義如下:

        deferred: 返回一個包含{ promise, resolve, reject }的對象

        ? promise 是一個處于pending狀態的promise

        ? resolve(value)value解決上面那個promise

        ? reject(reason)reason拒絕上面那個promise

        我們實現代碼如下:

        MyPromise.deferred = function() {
          var result = {};
          result.promise = new MyPromise(function(resolve, reject){
            result.resolve = resolve;
            result.reject = reject;
          });
        
          return result;
        }

        然后用npm將promises-aplus-tests下載下來,再配置下package.json就可以跑測試了:

        {
          "devDependencies": {
            "promises-aplus-tests": "^2.1.2"
          },
          "scripts": {
            "test": "promises-aplus-tests MyPromise"
          }
        }

        在跑測試的時候發現一個坑,在resolvePromise的時候,如果x是null,他的類型也是object,是應該直接用x來resolve的,之前的代碼會走到catch然后reject,所以需要檢測下null

        // 這個坑是跑測試的時候發現的,如果x是null,應該直接resolve
        if(x === null) {
          return resolve(x);
        }

        這個測試總共872用例,我們寫的Promise完美通過了所有用例:

        image-20200326214543894

        其他Promise方法

        在ES6的官方Promise還有很多API,比如:

        Promise.resolve

        Promise.reject

        Promise.all

        Promise.race

        Promise.prototype.catch

        Promise.prototype.finally

        Promise.allSettled

        雖然這些都不在Promise/A+里面,但是我們也來實現一下吧,加深理解。其實我們前面實現了Promise/A+再來實現這些已經是小菜一碟了,因為這些API全部是前面的封裝而已。

        Promise.resolve

        將現有對象轉為Promise對象,如果 Promise.resolve 方法的參數,不是具有 then 方法的對象(又稱 thenable 對象),則返回一個新的 Promise 對象,且它的狀態為fulfilled。

        MyPromise.resolve = function(parameter) {
          if(parameter instanceof MyPromise) {
            return parameter;
          }
        
          return new MyPromise(function(resolve) {
            resolve(parameter);
          });
        }

        Promise.reject

        返回一個新的Promise實例,該實例的狀態為rejected。Promise.reject方法的參數reason,會被傳遞給實例的回調函數。

        MyPromise.reject = function(reason) {
          return new MyPromise(function(resolve, reject) {
            reject(reason);
          });
        }

        Promise.all

        該方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例。

        const p = Promise.all([p1, p2, p3]);

        Promise.all()方法接受一個數組作為參數,p1、p2、p3都是 Promise 實例,如果不是,就會先調用Promise.resolve方法,將參數轉為 Promise 實例,再進一步處理。當p1, p2, p3全部resolve,大的promise才resolve,有任何一個reject,大的promise都reject。

        MyPromise.all = function(promiseList) {
          var resPromise = new MyPromise(function(resolve, reject) {
            var count = 0;
            var result = [];
            var length = promiseList.length;
        
            if(length === 0) {
              return resolve(result);
            }
        
            promiseList.forEach(function(promise, index) {
              MyPromise.resolve(promise).then(function(value){
                count++;
                result[index] = value;
                if(count === length) {
                  resolve(result);
                }
              }, function(reason){
                reject(reason);
              });
            });
          });
        
          return resPromise;
        }

        Promise.race

        用法:

        const p = Promise.race([p1, p2, p3]);

        該方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。上面代碼中,只要p1、p2、p3之中有一個實例率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。

        MyPromise.race = function(promiseList) {
          var resPromise = new MyPromise(function(resolve, reject) {
            var length = promiseList.length;
        
            if(length === 0) {
              return resolve();
            } else {
              for(var i = 0; i < length; i++) {
                MyPromise.resolve(promiseList[i]).then(function(value) {
                  return resolve(value);
                }, function(reason) {
                  return reject(reason);
                });
              }
            }
          });
        
          return resPromise;
        }

        Promise.prototype.catch

        Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的別名,用于指定發生錯誤時的回調函數。

        MyPromise.prototype.catch = function(onRejected) {
          this.then(null, onRejected);
        }

        Promise.prototype.finally

        finally方法用于指定不管 Promise 對象最后狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。

        MyPromise.prototype.finally = function(fn) {
          return this.then(function(value){
            return MyPromise.resolve(value).then(function(){
              return value;
            });
          }, function(error){
            return MyPromise.resolve(reason).then(function() {
              throw error
            });
          });
        }

        Promise.allSettled

        該方法接受一組 Promise 實例作為參數,包裝成一個新的 Promise 實例。只有等到所有這些參數實例都返回結果,不管是fulfilled還是rejected,包裝實例才會結束。該方法由 ES2020 引入。該方法返回的新的 Promise 實例,一旦結束,狀態總是fulfilled,不會變成rejected。狀態變成fulfilled后,Promise 的監聽函數接收到的參數是一個數組,每個成員對應一個傳入Promise.allSettled()的 Promise 實例的執行結果。

        MyPromise.allSettled = function(promiseList) {
          return new MyPromise(function(resolve){
            var length = promiseList.length;
            var result = [];
            var count = 0;
        
            if(length === 0) {
              return resolve(result);
            } else {
              for(var i = 0; i < length; i++) {
        
                (function(i){
                  var currentPromise = MyPromise.resolve(promiseList[i]);
        
                  currentPromise.then(function(value){
                    count++;
                    result[i] = {
                      status: 'fulfilled',
                      value: value
                    }
                    if(count === length) {
                      return resolve(result);
                    }
                  }, function(reason){
                    count++;
                    result[i] = {
                      status: 'rejected',
                      reason: reason
                    }
                    if(count === length) {
                      return resolve(result);
                    }
                  });
                })(i)
              }
            }
          });
        }

        完整代碼

        完全版的代碼較長,這里如果看不清楚的可以去我的GitHub上看:

        https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js

        // 先定義三個常量表示狀態
        var PENDING = 'pending';
        var FULFILLED = 'fulfilled';
        var REJECTED = 'rejected';
        
        function MyPromise(fn) {
          this.status = PENDING;    // 初始狀態為pending
          this.value = null;        // 初始化value
          this.reason = null;       // 初始化reason
        
          // 構造函數里面添加兩個數組存儲成功和失敗的回調
          this.onFulfilledCallbacks = [];
          this.onRejectedCallbacks = [];
        
          // 存一下this,以便resolve和reject里面訪問
          var that = this;
          // resolve方法參數是value
          function resolve(value) {
            if (that.status === PENDING) {
              that.status = FULFILLED;
              that.value = value;
        
              // resolve里面將所有成功的回調拿出來執行
              that.onFulfilledCallbacks.forEach(callback => {
                callback(that.value);
              });
            }
          }
        
          // reject方法參數是reason
          function reject(reason) {
            if (that.status === PENDING) {
              that.status = REJECTED;
              that.reason = reason;
        
              // resolve里面將所有失敗的回調拿出來執行
              that.onRejectedCallbacks.forEach(callback => {
                callback(that.reason);
              });
            }
          }
        
          try {
            fn(resolve, reject);
          } catch (error) {
            reject(error);
          }
        }
        
        function resolvePromise(promise, x, resolve, reject) {
          // 如果 promise 和 x 指向同一對象,以 TypeError 為據因拒絕執行 promise
          // 這是為了防止死循環
          if (promise === x) {
            return reject(new TypeError('The promise and the return value are the same'));
          }
        
          if (x instanceof MyPromise) {
            // 如果 x 為 Promise ,則使 promise 接受 x 的狀態
            // 也就是繼續執行x,如果執行的時候拿到一個y,還要繼續解析y
            // 這個if跟下面判斷then然后拿到執行其實重復了,可有可無
            x.then(function (y) {
              resolvePromise(promise, y, resolve, reject);
            }, reject);
          }
          // 如果 x 為對象或者函數
          else if (typeof x === 'object' || typeof x === 'function') {
            // 這個坑是跑測試的時候發現的,如果x是null,應該直接resolve
            if (x === null) {
              return resolve(x);
            }
        
            try {
              // 把 x.then 賦值給 then 
              var then = x.then;
            } catch (error) {
              // 如果取 x.then 的值時拋出錯誤 e ,則以 e 為據因拒絕 promise
              return reject(error);
            }
        
            // 如果 then 是函數
            if (typeof then === 'function') {
              var called = false;
              // 將 x 作為函數的作用域 this 調用之
              // 傳遞兩個回調函數作為參數,第一個參數叫做 resolvePromise ,第二個參數叫做 rejectPromise
              // 名字重名了,我直接用匿名函數了
              try {
                then.call(
                  x,
                  // 如果 resolvePromise 以值 y 為參數被調用,則運行 [[Resolve]](promise, y)
                  function (y) {
                    // 如果 resolvePromise 和 rejectPromise 均被調用,
                    // 或者被同一參數調用了多次,則優先采用首次調用并忽略剩下的調用
                    // 實現這條需要前面加一個變量called
                    if (called) return;
                    called = true;
                    resolvePromise(promise, y, resolve, reject);
                  },
                  // 如果 rejectPromise 以據因 r 為參數被調用,則以據因 r 拒絕 promise
                  function (r) {
                    if (called) return;
                    called = true;
                    reject(r);
                  });
              } catch (error) {
                // 如果調用 then 方法拋出了異常 e:
                // 如果 resolvePromise 或 rejectPromise 已經被調用,則忽略之
                if (called) return;
        
                // 否則以 e 為據因拒絕 promise
                reject(error);
              }
            } else {
              // 如果 then 不是函數,以 x 為參數執行 promise
              resolve(x);
            }
          } else {
            // 如果 x 不為對象或者函數,以 x 為參數執行 promise
            resolve(x);
          }
        }
        
        MyPromise.prototype.then = function (onFulfilled, onRejected) {
          // 如果onFulfilled不是函數,給一個默認函數,返回value
          // 后面返回新promise的時候也做了onFulfilled的參數檢查,這里可以刪除,暫時保留是為了跟規范一一對應,看得更直觀
          var realOnFulfilled = onFulfilled;
          if (typeof realOnFulfilled !== 'function') {
            realOnFulfilled = function (value) {
              return value;
            }
          }
        
          // 如果onRejected不是函數,給一個默認函數,返回reason的Error
          // 后面返回新promise的時候也做了onRejected的參數檢查,這里可以刪除,暫時保留是為了跟規范一一對應,看得更直觀
          var realOnRejected = onRejected;
          if (typeof realOnRejected !== 'function') {
            realOnRejected = function (reason) {
              throw reason;
            }
          }
        
          var that = this;   // 保存一下this
        
          if (this.status === FULFILLED) {
            var promise2 = new MyPromise(function (resolve, reject) {
              setTimeout(function () {
                try {
                  if (typeof onFulfilled !== 'function') {
                    resolve(that.value);
                  } else {
                    var x = realOnFulfilled(that.value);
                    resolvePromise(promise2, x, resolve, reject);
                  }
                } catch (error) {
                  reject(error);
                }
              }, 0);
            });
        
            return promise2;
          }
        
          if (this.status === REJECTED) {
            var promise2 = new MyPromise(function (resolve, reject) {
              setTimeout(function () {
                try {
                  if (typeof onRejected !== 'function') {
                    reject(that.reason);
                  } else {
                    var x = realOnRejected(that.reason);
                    resolvePromise(promise2, x, resolve, reject);
                  }
                } catch (error) {
                  reject(error);
                }
              }, 0);
            });
        
            return promise2;
          }
        
          // 如果還是PENDING狀態,將回調保存下來
          if (this.status === PENDING) {
            var promise2 = new MyPromise(function (resolve, reject) {
              that.onFulfilledCallbacks.push(function () {
                setTimeout(function () {
                  try {
                    if (typeof onFulfilled !== 'function') {
                      resolve(that.value);
                    } else {
                      var x = realOnFulfilled(that.value);
                      resolvePromise(promise2, x, resolve, reject);
                    }
                  } catch (error) {
                    reject(error);
                  }
                }, 0);
              });
              that.onRejectedCallbacks.push(function () {
                setTimeout(function () {
                  try {
                    if (typeof onRejected !== 'function') {
                      reject(that.reason);
                    } else {
                      var x = realOnRejected(that.reason);
                      resolvePromise(promise2, x, resolve, reject);
                    }
                  } catch (error) {
                    reject(error);
                  }
                }, 0)
              });
            });
        
            return promise2;
          }
        }
        
        MyPromise.deferred = function () {
          var result = {};
          result.promise = new MyPromise(function (resolve, reject) {
            result.resolve = resolve;
            result.reject = reject;
          });
        
          return result;
        }
        
        MyPromise.resolve = function (parameter) {
          if (parameter instanceof MyPromise) {
            return parameter;
          }
        
          return new MyPromise(function (resolve) {
            resolve(parameter);
          });
        }
        
        MyPromise.reject = function (reason) {
          return new MyPromise(function (resolve, reject) {
            reject(reason);
          });
        }
        
        MyPromise.all = function (promiseList) {
          var resPromise = new MyPromise(function (resolve, reject) {
            var count = 0;
            var result = [];
            var length = promiseList.length;
        
            if (length === 0) {
              return resolve(result);
            }
        
            promiseList.forEach(function (promise, index) {
              MyPromise.resolve(promise).then(function (value) {
                count++;
                result[index] = value;
                if (count === length) {
                  resolve(result);
                }
              }, function (reason) {
                reject(reason);
              });
            });
          });
        
          return resPromise;
        }
        
        MyPromise.race = function (promiseList) {
          var resPromise = new MyPromise(function (resolve, reject) {
            var length = promiseList.length;
        
            if (length === 0) {
              return resolve();
            } else {
              for (var i = 0; i < length; i++) {
                MyPromise.resolve(promiseList[i]).then(function (value) {
                  return resolve(value);
                }, function (reason) {
                  return reject(reason);
                });
              }
            }
          });
        
          return resPromise;
        }
        
        MyPromise.prototype.catch = function (onRejected) {
          this.then(null, onRejected);
        }
        
        MyPromise.prototype.finally = function (fn) {
          return this.then(function (value) {
            return MyPromise.resolve(fn()).then(function () {
              return value;
            });
          }, function (error) {
            return MyPromise.resolve(fn()).then(function () {
              throw error
            });
          });
        }
        
        MyPromise.allSettled = function (promiseList) {
          return new MyPromise(function (resolve) {
            var length = promiseList.length;
            var result = [];
            var count = 0;
        
            if (length === 0) {
              return resolve(result);
            } else {
              for (var i = 0; i < length; i++) {
        
                (function (i) {
                  var currentPromise = MyPromise.resolve(promiseList[i]);
        
                  currentPromise.then(function (value) {
                    count++;
                    result[i] = {
                      status: 'fulfilled',
                      value: value
                    }
                    if (count === length) {
                      return resolve(result);
                    }
                  }, function (reason) {
                    count++;
                    result[i] = {
                      status: 'rejected',
                      reason: reason
                    }
                    if (count === length) {
                      return resolve(result);
                    }
                  });
                })(i)
              }
            }
          });
        }
        
        module.exports = MyPromise;

        總結

        至此,我們的Promise就簡單實現了,只是我們不是原生代碼,不能做成微任務,如果一定要做成微任務的話,只能用其他微任務API模擬,比如MutaionObserver或者process.nextTick。下面再回顧下幾個要點:

        1. Promise其實是一個發布訂閱模式
        2. then方法對于還在pending的任務,其實是將回調函數onFilfilledonRejected塞入了兩個數組
        3. Promise構造函數里面的resolve方法會將數組onFilfilledCallbacks里面的方法全部拿出來執行,這里面是之前then方法塞進去的成功回調
        4. 同理,Promise構造函數里面的reject方法會將數組onRejectedCallbacks里面的方法全部拿出來執行,這里面是之前then方法塞進去的失敗回調
        5. then方法會返回一個新的Promise以便執行鏈式調用
        6. catchfinally這些實例方法都必須返回一個新的Promise實例以便實現鏈式調用

        文章的最后,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續創作的動力。

        歡迎關注我的公眾號進擊的大前端第一時間獲取高質量原創~

        “前端進階知識”系列文章源碼地址: https://github.com/dennis-jiang/Front-End-Knowledges

        1270_300二維碼_2.png

        查看原文

        贊 51 收藏 37 評論 2

        認證與成就

        • 獲得 6239 次點贊
        • 獲得 40 枚徽章 獲得 0 枚金徽章, 獲得 3 枚銀徽章, 獲得 37 枚銅徽章

        擅長技能
        編輯

        開源項目 & 著作
        編輯

        注冊于 2018-06-05
        個人主頁被 31.2k 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕