Promise源碼分析(附帶Promise的正確打開方式)

Promise源碼分析

Promise的底層是瀏覽器實現的,所以我只是實現了裏的功能,並非和源碼一摸一樣。下面爲部分源碼,完整源碼移駕到github中下載

https://github.com/young-monk/my-promise.git

我們首先來定義一個MyPromise類,我在這裏使用立即執行函數的方式,防止變量全局污染。

const MyPromise = (() => {
    //在這裏定義內部變量
    return class {
        //構造器
        constructor(executor) {
            //...
        }
})()

接下來,定義一些內部使用的變量:我這裏使用了Symbol,使用Symbol的原因是改變量只能內部使用

const MyPromise = (() => {
    //在這裏定義內部變量
    const PENDING = "pending"
    const RESOLVED = "resolved"
    const REJECTED = "rejected"
    //使用Symbol是爲了僅供內部使用
    const PromiseStatus = Symbol("PromiseStatus")//當前狀態
    const PromiseValue = Symbol("PromiseValue")//當前值
    return class {
        //構造器
        constructor(executor) {
            //...
        }
})()

我們都知道,當我們new Promise的時候,會有一個狀態,會有值,還會傳入兩個函數,resolvereject。那麼我們實現一下:

constructor(executor) {
    //初始化
    this[PromiseStatus] = PENDING //當前狀態
    this[PromiseValue] = undefined //當前值
    /**
     * 定義 resolve 函數
     * @param {*} data: 要返回的數據
     */
    const resolve = (data) => {
        //...
    }
    /**
     * 定義reject函數
     * @param {*} data: 要返回的數據
     */
    const reject = (data) => {
        //...
    }
    //執行
    executor(resolve, reject)
}

接下來,我們就要實現resolvereject函數的功能,其實這兩個函數的功能非常簡單,就是修改當前的狀態和值,並且如果有任務隊列,我們就執行任務隊列中的內容。我把這兩個函數的功能封裝一下:

/**
 * 改變狀態的函數
 * @param {*} data: 數據
 * @param {*} status: 要改變的狀態,resolve 或 reject
 * @param {*} queue: 任務隊列
 */
[changeStatus](data, status, queue) {
    //如果已經是已決狀態,那麼直接結束
    if (this[PromiseStatus] !== PENDING) return;
    this[PromiseStatus] = status //修改當前狀態
    this[PromiseValue] = data //修改值
    //變成已決階段後,執行相應的隊列函數
    queue.forEach(q => q(data))
}

//---- 在 resolve 函數中調用
this[changeStatus](data, RESOLVED, this[thenables])
//---- 在 reject 函數中調用
this[changeStatus](data, REJECTED, this[catchables])

接下來我們要實現thencatch這兩個後續處理函數,這兩個函數實現什麼功能想必大家都知道了,但我們要注意的是,如果調用then或者catch的時候,當前的狀態已經是已決狀態,那麼就要直接執行。如果不是,那麼就加入任務隊列中,我在構造器中已經聲明瞭 thenablescatchables 變量(thencatch函數的任務隊列)

 //settled then處理函數
then(thenable, catchable) {
    //每個then都要返回一個新的promise
    return this[linkPromise](thenable, catchable)
}
//settled catch處理函數
catch (catchable) {
    return this[linkPromise](undefined, catchable)
}

因爲功能相同,我進行了 2 次封裝,都是哪兩次封裝呢?這是第一次,也就是實現加入任務隊列的功能:

/**
 * then和catch的處理函數,分爲兩種情況,如果當前已經是已決狀態,
 * 那麼直接執行(此時直接執行也要加入事件隊列中,無法模擬微隊列,只能用宏隊列實現下),如果當前還是未決狀態, 
 * 那麼把當前的處理函數加入相應的任務隊列中
 * @param {*} handler 處理函數
 * @param {*} queue   處理隊列
 */
[settledHandler](handler, status, queue) {
    //如果不是函數,那麼直接返回
    if (typeof handler !== "function") return
    if (this[PromiseStatus] === status) {
        //如果已經是已決狀態,直接執行
        setTimeout(() => {
            handler(this[PromiseValue])
        }, 0);
    } else {
        //處於未決狀態,加入任務隊列
        queue.push(handler)
    }
}

還有一次封裝,也就是我們上面調用的 linkPromise 函數,爲什麼要調用這個函數?因爲thencatch後續處理函數,都要返回一個新的Promise,我們什麼時候知道,then 或 catch 函數執行了,該返回新的Promise呢?這裏是比較難的一部分,我們可以將 settledHandler 也就是執行處理函數的功能,反正創建一個新的 Promise中執行:

/**
 * 用於創建一個新的Promise, 當我們調用then和catch處理函數時, 會返回一個新的Promise
 * @param {*} thenable
 * @param {*} catchable 
 */
[linkPromise](thenable, catchable) {
    /**
     * 返回一個新的Promise的狀態處理,如果父級已經變爲已決狀態, 那麼新的Promise也是已決狀態
     * @param {*} data 
     * @param {*} handler 
     * @param {*} resolve 
     * @param {*} reject 
     */
    function exec(data, handler, resolve, reject) {
        try {
            //獲取返回值
            const res = handler(data)
            //如果返回的是一個Promise,此時我們直接處理一下就可以
            if (res instanceof MyPromise) {
                res.then(data => resolve(data), err => reject(err))
            } else {
                //改變狀態,和修改值
                resolve(res)
            }
        } catch (error) {
            reject(error)
        }
    }
    //返回新的Promise
    return new MyPromise((resolve, reject) => {
        //處理then的
        this[settledHandler](data => {
            //如果傳過來的thenable不是函數,那麼直接resolve下並結束
            if (typeof thenable !== "function") {
                resolve(data)
                return
            }
            //我們把操作相同的提取封裝一下
            exec(data, thenable, resolve, reject)
        }, RESOLVED, this[thenables])
        //處理catch的
        this[settledHandler](data => {
            //如果傳過來的thenable不是函數,那麼直接reject下並結束
            if (typeof catchable !== "function") {
                reject(data)
                return
            }
            //我們把操作相同的提取封裝一下
            exec(data, catchable, resolve, reject)
        }, REJECTED, this[catchables])
    })
}

如果你能看到這裏,說明你已經對Promise已經完全掌握了,下面實現的是幾個Promise的靜態成員:

all成員

/**
 * 當數組中的每一個值都變爲resolved時,返回新的promise的值resolve爲一個數組,數組的內容爲proms每個Promise的結果,如果有一個變爲rejected, 那麼直接結束
 * @param {*} proms 假定爲一個數組
 */
static all(proms) {
    return new MyPromise((resolve, reject) => {
        const results = proms.map(p => {
            var obj = {
                result: undefined,
                isResolved: false
            }
            //判斷是否已經全部完成
            p.then(data => {
                obj.result = data
                obj.isResolved = true
                //得到是否有非resolve狀態的
                const unResolved = results.filter(res => !res.isResolved)
                if (unResolved.length === 0) {//如果沒有 那麼我們返回新的Promise
                    resolve(results.map(res => res.result))
                }
                //如果有一個reject狀態,那麼直接返回
            }, err => reject(err))
            return obj
        })
        console.log(results)
    })
}

race成員

/**
 * 當數組中有一個處於已決狀態,那麼結束
 * @param {*} proms: 假定是一個數組 
 */
static race(proms) {
    return new MyPromise((resolve, reject) => {
        proms.forEach(p => {
            p.then(data => resolve(data), err => reject(err))
        })
    })
}

resolve和reject靜態成員

/**
 * 返回一個resolved狀態的promise
 * @param {*} data 
 */
static resolve(data) {
    //如果穿過來的是一個Promise,直接返回就可以
    if (data instanceof MyPromise) {
        return data
    }
    return new MyPromise(resolve => resolve(data))
}
/**
 * 返回一個rejected狀態的promise
 * @param {*} err 
 */
static reject(err) {
    return new MyPromise((resolve, reject) => {
        reject(err)
    })
}

Promise正確打開方式

1、異步處理的通用模型

ES6 將某一件可能發生異步操作的事情,分爲兩個階段:unsettledsettled

  • unsettled:未決階段,表示事情還在進行前期的處理,並沒有發生通向結果的那件事

  • settled:已決階段,事情已經有了一個結果,不管這個結果是好是壞,整件事情無法逆轉

異步操作總是從 未決階段 逐步發展到 已決階段的。並且,未決階段擁有控制何時通向已決階段的能力。

異步操作分成了三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)

  • pending:進行中,處於未決階段,則表示這件事情還在進行(最終的結果還沒出來)
  • fulfilled:已成功,已決階段的一種狀態,表示整件事情已經出現結果,並是一個可以按照正常邏輯進行下去的結果
  • rejected:已失敗,已決階段的一種狀態,表示整件事情已經出現結果,並是一個無法按照正常邏輯進行下去的結果,通常用於表示有一個錯誤

一旦這種狀態改變,就會固定,不會再改變。是不可逆的Promise狀態的改變只有兩種情況:

  • 1、從pending通過resolve()變爲fulfilled
  • 2、從pending通過reject()變爲rejected

參考流程圖

在這裏插入圖片描述

2、Promise及其基本使用

爲了解決地獄回調和異步操作之間的聯繫,ES6提出了一種異步編程大的解決方案Promise。但是Promise並沒有消除回調,只是讓回調變得可控。

Promise是一個對象,它可以獲取異步操作的消息。爲了方便和簡易,下面的resolved統指fulfilled狀態

const pro = new Promise((resolve, reject)=>{
    /*
        未決階段的處理
        通過調用resolve函數將Promise推向已決階段的resolved狀態
        通過調用reject函數將Promise推向已決階段的rejected狀態
        resolve和reject均可以傳遞最多一個參數,表示推向狀態的數據
    */ 
    if(true){
        resolve()
    }else{
        reject()
    }
    
})
pro.then(data=>{
    /*
        這是thenable函數,如果當前的Promise已經是resolved狀態,該函數會立即執行
        如果當前是未決階段,則會加入到作業隊列,等待到達resolved狀態後執行
        data爲狀態數據
    */
},err=>{
    /*
    	then函數也可以填catchable函數,也可以不填。我們最好是通過catch方法添加catchable函數
    */
})
pro.catch(err=>{
   /*
        這是catchable函數,如果當前的Promise已經是rejected狀態,該函數會立即執行
        如果當前是未決階段,則會加入到作業隊列,等待到達rejected狀態後執行
        err爲狀態數據
    */
})

注意

  • Promise創建後會立即執行。

  • thenablecatchable函數是異步的,就算是立即執行,也會加入到事件隊列中等待執行,加入的隊列是微隊列。

  • 在未決階段的處理函數中,如果發生未捕獲的錯誤,會將狀態推向rejected,並會被catchable捕獲。

  • 一旦狀態推向了已決階段,無法再對狀態做任何更改。

  • Promise並沒有消除回調,只是讓回調變得可控。

const pro = new Promise((resolve,reject)=>{
    console.log("a")
    setTimeout(() => {
        console.log("d")
    }, 0);
    resolve(1)
    console.log("b")
})
pro.then(data=>{
    console.log(data)
})
console.log("c")
//a b c 1 d

以上代碼,立即創建一個Promise函數,並且立即執行函數中的代碼,所以首先輸出a,隨後將setTimeout中代碼加入宏隊列,然後通過resolve()Promise的狀態推向已決狀態,但是resolve也是異步的,他會加入到微隊列中等同步代碼執行完畢後再執行。隨後輸出bPromise函數執行完後,又執行剩下的同步代碼輸出c,同步代碼執行完畢後,執行微隊列中的輸出resolve的結果值1,再執行宏隊列中的setTimeout,輸出b

3、Promise的方法then,catch,finally

(1)then()和catch()

then():註冊一個後續處理函數,當Promise爲resolved狀態時運行該函數

catch():註冊一個後續處理函數,當Promise爲rejected狀態時運行該函數

Promise對象中,無論是then方法還是catch方法,它們都具有返回值,返回的是一個全新的Promise對象,它的狀態滿足下面的規則:

  1. 如果當前的Promise是未決的,得到的新的Promise是進行中狀態
  2. 如果當前的Promise是已決的,會運行響應的後續處理函數,並將後續處理函數的結果(返回值)作爲resolved狀態數據,應用到新的Promise中;如果後續處理函數發生錯誤,則把返回值作爲rejected狀態數據,應用到新的Promise中。

注意:後續的Promise一定會等到前面的Promise有了後續處理結果後,纔會變成已決狀態

如果前面的Promise的後續處理,返回的是一個Promise,則返回的新的Promise狀態和後續處理返回的Promise狀態保持一致。

const pro1 = new Promise((resolve,reject)=>{
    resolve(1)
})
console.log(pro1)
// 異步調用,
const pro2 = pro1.then(result => result *2)
console.log(pro2)//pro2是一個Promise對象,狀態是pending 
pro2.then(result=>{console.log(result)},err=>console.log(err))

上述代碼,在執行console.log(pro2)的時候是同步執行, 此時pro1.then()還未執行完畢,所以promise還是pending狀態

const pro1 = new Promise((resolve,reject)=>{
    throw 1
})
const pro2 = pro1.then(result => result *2,err=> err*3)
pro2.then(result=>{console.log(result*2)},err=>console.log(err*3))
//6

上述代碼,對於串聯的Promise,then和catch均返回一個全新的Promise,所以在pro1的catch執行時返回的pro2執行的是正常的,並非拋出錯誤。所以執行的爲err * 3result * 2

const pro1 = new Promise((resolve,reject)=>{
    throw 1
})
const pro2 = pro1.then(result => result *2,err=> err*3)
pro1.catch(err=>err*2)
pro2.then(result=>{console.log(result*2)},err=>console.log(err*3))

上述代碼,每一次返回都是一個全新的Promise,所以pro1.catch(err=>err*2)並沒有變量接收。

const pro1 = new Promise((resolve,reject)=>{
    resolve(1)
})
const pro2 = new Promise((resolve,reject)=>{
    resolve(2)
})
const pro3 = pro1.then(result=>{
    console.log(`結果${result}`) //1
    return pro2
})
//pro3的狀態是pending
pro3.then(result=>{
    console.log(result)//2
})

上述代碼,第一個then方法指定的回調函數,返回的是另一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化後再執行

const pro1 = new Promise((resolve,reject)=>{
    resolve(1)
})
const pro2 = new Promise((resolve,reject)=>{
    setTimeout(() => {
        resolve(2)
    }, 3000);
})
pro1.then(result=>{
    console.log(`結果${result}`) //1
    return pro2
}).then(result=>{
    console.log(result)//2
}).then(result=>{
    console.log(result)//undefined
})

上面代碼,最後一個輸出undefined是因爲第二個then方法沒有返會值。

(2)finally()

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

const pro1 = new Promise((resolve,reject)=>{
    resolve(1)
})
pro1.then(result=>{
    console.log(`結果${result}`) //結果1
    return 3
}).then(result=>{
    console.log(result)//3
}).catch(error=>{
	console.log(error)
}).finally(()=>{
	console.log("我一定會執行的")//我一定會執行的
})

4、Promise的靜態成員

(1)resolve()

該方法返回一個resolved狀態的Promise,傳遞的數據作爲狀態數據。有時需要將現有對象轉爲 Promise 對象,Promise.resolve()方法就起到這個作用。

const pro = Promise.resolve(1)
//等同於
const pro = new Promise((resolve,reject)=>{
    resolve(1)
})

特殊情況:如果傳遞的數據是Promise,則直接返回傳遞的Promise對象

(2)reject()

該方法返回一個rejected狀態的Promise,傳遞的數據作爲狀態數據

const pro = Promise.reject(1)
//等同於
const pro = new Promise((resolve,reject)=>{
    reject(1)
})

(3)all()

Promise.all()方法的參數可以不是數組,但必須具有 Iterator 接口。這個方法返回一個新的Promise對象,

返回的新的Promise對象的狀態分成兩種情況:

  1. 當參數中所有的Promise對象的狀態都變成fulfilled,返回的Promise狀態纔會變成fulfilled。此時參數的返回值組成一個數組,傳遞給新Promise的回調函數。
  2. 當參數之中有一個被rejected,新Promise的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給新Promise的回調函數。
//p1, p2, p3均是promise對象
const pro = Promise.all([p1, p2, p3]);

上述代碼就是一個非常簡單的調用方法

function getRandom(min,max){
    return Math.random() * (max-min) + min
}
const proms = []
for(let i=0;i<10;i++){
    proms.push(new Promise((resolve,reject)=>{
        setTimeout(() => {
            if(Math.random()<0.1){
                reject(i)
            }else{
                resolve(i)
                console.log(i,"完成")
            }
        }, getRandom(1000,5000))
    }))
}
const pro = Promise.all(proms)
pro.then(res=>{
    console.log("全部完成",res)
},err=>{
    console.log(err,"錯誤")
})

上面代碼是產生10個Promise對象,每個Promise對象都延遲時間1~5s中的隨機時間後將狀態推向已決,調用all方法,當10個Promise對象全部完成後再輸出,或有一個錯誤的時候,也輸出。

(4)race()

Promise.race()方法同樣是將多個 Promise 對象,包裝成一個新的 Promise 對象。Promise.race()方法的參數與Promise.all()方法一樣。

當參數裏的任意一個Promise被成功或失敗後,新Promise馬上也會用參數中那個promise的成功返回值或失敗詳情作爲參數調用新promise綁定的相應句柄,並返回該promise對象

function getRandom(min,max){
    return Math.random() * (max-min) + min
}
const proms = []
for(let i=0;i<10;i++){
    proms.push(new Promise((resolve,reject)=>{
        setTimeout(() => {
            if(Math.random()<0.1){
                reject(i)
            }else{
                resolve(i)
                console.log(i,"完成")
            }
        }, getRandom(1000,5000))
    }))
}
const pro = Promise.race(proms)
pro.then(res=>{
    console.log("有完成的",res)
},err=>{
    console.log(err,"有錯誤")
})
console.log(proms)

上述代碼和all()例子一樣,只不過,當有一個完成或失敗的時候,就會執行prothencatch回調函數

例子:異步加載圖片

const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve();
    image.onerror = reject();
    image.src = path;
  });
};

一旦圖片加載完成,就會Promise狀態就會發生變化。

5、async 和 await

async 和 await 是 ES2016 新增兩個關鍵字,它們借鑑了 ES2015 中生成器在實際開發中的應用,目的是簡化 Promise api 的使用,並非是替代 Promise。實際上就是生成器函數的一個語法糖。目的是簡化在函數的返回值中對Promise的創建。

(1)async

async函數返回一個 Promise 對象,可以使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操作完成,再接着執行函數體內後面的語句。

async 用於修飾函數(無論是函數字面量還是函數表達式),放置在函數最開始的位置,被修飾函數的返回結果一定是 Promise 對象。

async function test(){
    console.log(1);
    return 2;//完成時狀態數據
}
//等同於
function test(){
    return new Promise((resolve, reject)=>{
        console.log(1);
        resolve(2);
    })
}
async function test(){
    console.log(1)
    return 2
}
const pro = test()
console.log(pro)//promise對象 promiseValue是2

注意async函數返回的 Promise 對象,必須等到內部所有await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操作執行完,纔會執行then方法指定的回調函數。

(2)await

正常情況下,await命令後面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值。await類似於生成器的yield,當遇到await的時候,就會等待await後面的Promise對象執行完畢後再繼續執行下面代碼。

async function test(){
    const namePro = await getName();//異步ajax
    const passwordPro = await getPassword();
}

test()函數執行過程中,會先等待getName()執行完畢後,再執行getPassword()

注意await關鍵字必須出現在async函數中

如果多個await命令後面的異步操作,如果不存在繼發關係,最好讓它們同時觸發。

async function test(){
    let namePro = getName();
    let passwordPro = getPassword();
    let name = await namePro;
    let password = await passwordPro;
}

先讓getName()getPassword()執行,然後再等待結果

await用在某個表達式之前,如果表達式是一個Promise,則得到的是thenable中的狀態數據。

async function test1(){
    console.log(1);
    return 2;
}

async function test2(){
    const result = await test1();
    console.log(result);
}

test2();
//等同於
function test1(){
    return new Promise((resolve, reject)=>{
        console.log(1);
        resolve(2);
    })
}
function test2(){
    return new Promise((resolve, reject)=>{
        test1().then(data => {
            const result = data;
            console.log(result);
            resolve();
        })
    })
}
test2();

如果await的表達式不是Promise,則會將其使用Promise.resolve包裝後按照規則運行

async function test(){
    const result = await 1
    console.log(result)
}
//---等同
function test(){
    return new Promise((resolve,reject)=>{
        Promise.resolve(1).then(data =>{
            const result = data
            console.log(result)
            resolve()
        })
    })
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章