1. 程式人生 > 前端設計 >一起拿下非同步

一起拿下非同步

拿下非同步

寫在前面

今天來回顧一下非同步,在如今的前端。非同步已經是非常重要的東西了,甚至可以說是不懂非同步,什麼都做不了了

今天從幾個方面來總結

  • 總結一下常見的非同步解決方案
  • 手寫一下promise

非同步經歷了以下四個階段

回撥函式 —> Promise —> Generator —> async/await。

回顧一下開始容易感到困惑的問題

問:首先我們都清楚js是一個單執行緒的語言,單又可以開一個非同步任務的執行緒是否與它的單執行緒執行模式自相矛盾呢?

答:js的執行時單執行緒的,但是瀏覽器是多執行緒的。即js的非同步是由js執行執行緒和瀏覽器的事件觸發執行緒等共同實現的

(結合Electron開發的經驗理解瀏覽中的多執行緒操作)。程序之間不太容易實現交流,執行緒可是沒有這個憂慮的。js執行執行緒即遇到一個非同步程式碼,會向瀏覽器請求援助。瀏覽器會新開一個執行緒處理,事件監聽執行緒會監聽處理的結果

步入正題(常見的非同步解決方案)

1. 回撥函式

直接看栗子(讀取一個文字):

const fs = require("fs");
const path = require("path")


fs.readFile(path.join(__dirname,"./test.txt"),"utf8",(err,data) => {
    console.log(data);

})
複製程式碼

2. 釋出訂閱

寫一個“訊號中心”。當一個非同步任務執行結束之後向內釋出“訊號”,則訂閱者的回撥就可開始執行(釋出訂閱模式)

class Event {
    constructor() {
        this.subs = [];
    }
    on(fn) {
        this.subs.push(fn)
    }
    emit(data) {
        this.subs.forEach(fn => fn(data));
    }
}

const fs = require("fs");
const path = require("path")
let
e = new Event(); e.on((val) => { console.log(val); }) fs.readFile(path.join(__dirname,data) => { if (err) return; e.emit(data); }) 複製程式碼

注意:有一些文件說釋出訂閱模式就是觀察者模式,這是不對的。釋出訂閱更像是觀察者的子集(日後總結設計模式的時候再展開吧)

3. 事件監聽

這個東西,寫個前端的都寫過了

demo.onclick=function(){}
複製程式碼

4. promise

為了解決回撥地獄的問題,promise出來了

const fs = require("fs");
const path = require("path")
const p = new Promise((resolve,reject) => {
    fs.readFile(path.join(__dirname,data) => {
        if (err) {
            reject(err);
        } else {
            resolve(data)
        }


    })
})
p.then(data => {
    console.log(data);

})
複製程式碼

注意promise的使用和原理是一個重點,筆者在下面會總結

5. generator+co

利用generator可以中斷可以喚醒的執行機制。

為什麼需要co?我們都知道generator是一個迭代器生成器,它呼叫一下才是一個迭代器,同時因為要喚醒我們也要不斷則執行next()方法。這便很繁瑣了,co就是為我們封裝了這麼一個自動執行的東西。

這裡為了栗子的簡便,筆者沒有使用co 。主要要了解它做了什麼

const fs = require("fs");
const path = require("path");
const ioPromise = new Promise((resolve,data) => {
        if (err) {
            reject(err);
        } else {
            resolve(data)
        }
    })
});

function* read() {
    const data = yield ioPromise;
}
const it = read();
it.next().value.then(data => {
    console.log(data);
});
複製程式碼

6. async/awiat(generator+co的語法糖)

這個就是我們現在常用的啦

async function getData() {
    const res = await this.$aixos("...")
}
複製程式碼

第二階段 promise的相關知識

1. 常用方法

原型上

  1. then():接收兩個回撥引數,後一個回撥是可選引數,then(function(){...},function(){....})

    它的返回值需要注意一下(直接看mdn,這裡很值得注意,因為手寫時這裡的邏輯很重要)

    • 返回了一個值,那麼 then 返回的 Promise 將會成為接受狀態,並且將返回的值作為接受狀態的回撥函式的引數值。
    • 沒有返回任何值,那麼 then 返回的 Promise 將會成為接受狀態,並且該接受狀態的回撥函式的引數值為 undefined
    • 丟擲一個錯誤,那麼 then 返回的 Promise 將會成為拒絕狀態,並且將丟擲的錯誤作為拒絕狀態的回撥函式的引數值。
    • 返回一個已經是接受狀態的 Promise,那麼 then 返回的 Promise 也會成為接受狀態,並且將那個 Promise 的接受狀態的回撥函式的引數值作為該被返回的Promise的接受狀態回撥函式的引數值。
    • 返回一個已經是拒絕狀態的 Promise,那麼 then 返回的 Promise 也會成為拒絕狀態,並且將那個 Promise 的拒絕狀態的回撥函式的引數值作為該被返回的Promise的拒絕狀態回撥函式的引數值。
    • 返回一個未定狀態(pending)的 Promise,那麼 then 返回 Promise 的狀態也是未定的,並且它的終態與那個 Promise 的終態相同;同時,它變為終態時呼叫的回撥函式引數與那個 Promise 變為終態時的回撥函式的引數是相同的。
  2. catch():then(null,function(){})的別名。promise狀態失敗了走它。這裡要注意的是在resolve之後的錯誤是不會捕獲的,同時promise的錯誤具有“冒泡的性質”,它會一直向後傳遞直到被捕獲。故建議將catch寫到最後

    • 返回值:一個promise
  3. finally():不管promise的狀態是成功還失敗都走一下這個方法

因為then的返回值也均是promise,故為promise的鏈式呼叫提供了可能

靜態方法

  • resolve: 返回一個狀態由給定value決定的Promise物件。如果該值是thenable(即,帶有then方法的物件),返回的Promise物件的最終狀態由then方法執行決定;否則的話(該value為空,基本型別或者不帶then方法的物件),返回的Promise物件狀態為fulfilled,並且將該value傳遞給對應的then方法。通常而言,如果你不知道一個值是否是Promise物件,使用Promise.resolve(value) 來返回一個Promise物件,這樣就能將該value以Promise物件形式使用。

  • reject: 返回一個狀態為失敗的Promise物件,並將給定的失敗資訊傳遞給對應的處理方法

  • all:引數(iterable)一般是元素是promise的陣列,即[p1,p2,p3]。幾個promise全resolve,all的返回值也是一個已完成的promise。有一個reject,則走失敗

  • race:與all相似,[p1,p3]哪一個promise最先狀態改變它就認哪個。

2. 手寫promise

**Promise** 物件是一個代理物件(代理一個值),被代理的值在Promise物件建立時可能是未知的。它允許你為非同步操作的成功和失敗分別繫結相應的處理方法(handlers)。 這讓非同步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的promise物件

一個 Promise有以下幾種狀態:

  • pending: 初始狀態,既不是成功,也不是失敗狀態。
  • fulfilled: 意味著操作成功完成。
  • rejected: 意味著操作失敗。

手寫之前,回想一下promise是怎麼使用的。我們new的時候穿了一個執行函式,且當裡面非同步邏輯成功時執行resolve,失敗時執行reject

即開始一個promise的狀態是 pending,執行了resolve它就變成了fulfilled,又或者執行了reject它就變成了rejected

並且promise的狀態是發生了改變便不會再變動了,成功就是成功。不會從成功態又轉到失敗態

1 來構造這個類(這個已經是最簡版了)

const PENDING = "PENDING"; //執行態
const SUCCESS = "SUCCESS"; //成功態
const FUL = "FUL"; //失敗態
class MyPromise {
    constructor(exector) {
            this.status = PENDING;
            this.res = undefined; //失敗原因
            this.val = undefined; //成功值

            let resolve = (val) => {
                if (this.status != PENDING) return;
                this.val = val;
                this.status = SUCCESS;

            }
            let reject = (err) => {
                if (this.status != PENDING) return;
                this.res = err;
                this.status = FUL;

            }

            try {
                exector(resolve,reject);
            } catch (error) {
                reject(error);
            }
           
        }
        then(onfulfilled,omrejected) {
            if (this.status === SUCCESSS) {
                onfulfilled(this.val)
            }
            if (this.status === FUl) {
                omrejected(this.res);
            }
        
        }
    }


複製程式碼

2. 增加非同步解決

上面的實現,若是一個非同步任務。走到then時的狀態還是PENDING走不通。

利用釋出訂閱模式,還是在非同步結果出來之釋出訊號。then中實現訂閱即可

class MyPromise {
    constructor(exector) {
        this.status = PENDING;
        this.res = undefined;
        this.val = undefined;
        //裝then成功的回撥
        this.onfulfilledCb = [];
        //裝then失敗的回撥
        this.onrejectedCb = [];
        let resolve = (val) => {
            if (this.status != PENDING) return;
            this.val = val;
            this.status = SUCCESS;
            //成功結果出來執行已訂閱的的回撥
            this.onfulfilledCb.forEach(fn => fn(val))
        }
        let reject = (err) => {
            if (this.status != PENDING) return;
            this.res = err;
            this.status = FUL;
            //失敗結果出來執行已訂閱的的回撥
            this.onrejectedCb.forEach(fn => fn(err));
        }

        try {
            exector(resolve,reject);
        } catch (error) {
            reject(error);
        }
    }
    then(onfulfilled,onrejected) {


        if (this.status === SUCCESS) {
            onfulfilled(this.val);
        }
        if (this.status === FUL) {
            onrejected(this.res);

        }

        // 非同步
        if (this.status === PENDING) {
            this.onfulfilledCb.push(onfulfilled);
            this.onrejectedCb.push(onrejected);
        }

    }
}
複製程式碼

3. 為了保證鏈式呼叫then必須是返回一個promise

注意上面常用方法中then的返回值型別,用setTimeout包一下是因為promise的規範是明確說明:then方法是非同步執行的,故這裡使用setTimeout模擬了一下

const resolvePromise = (promise2,x,resolve,reject) => {

    if (typeof x === "object" || typeof x === "function") {
        let then = x.then;
        // 有then方法預設它就是promise
        if (typeof then === "function") {
            then.call(x,y => {
                // 可能返回的還是一個promise,故遞迴處理
                resolvePromise(promise2,y,reject)
            },z => {
                reject(z);
            })
        }
    } else {
        // 簡單型別的值直接resolve出去
        resolve(x);
    }
}
const PENDING = "PENDING";
const SUCCESS = "SUCCESS";
const FUL = "FUL";
class MyPromise {
    constructor(exector) {
        this.status = PENDING;
        this.res = undefined;
        this.val = undefined;
        this.onfulfilledCb = [];
        this.onrejectedCb = [];
        let resolve = (val) => {
            if (this.status != PENDING) return;
            this.val = val;
            this.status = SUCCESS;
            this.onfulfilledCb.forEach(fn => fn())
        }
        let reject = (err) => {
            if (this.status != PENDING) return;
            this.res = err;
            this.status = FUL;
            this.onrejectedCb.forEach(fn => fn());
        }

        try {
            exector(resolve,onrejected) {

        let promise2 = new MyPromise((resolve,reject) => {
            if (this.status === SUCCESS) {
                setTimeout(() => {
                    let x = onfulfilled(this.val);
                    resolvePromise(promise2,reject)
                },0)
            }
            if (this.status === FUL) {
                setTimeout(() => {
                    let x = onrejected(this.res);
                    resolvePromise(promise2,0)
            }

            // 非同步
            if (this.status === PENDING) {
                this.onfulfilledCb.push(() => {
                    setTimeout(() => {
                        let x = onfulfilled(this.val);
                        resolvePromise(promise2,reject)
                    },0)
                });
                this.onrejectedCb.push(() => {
                    setTimeout(() => {
                        let x = onrejected(this.res);
                        resolvePromise(promise2,0)
                });
            }
        })

        return promise2;
    }
}

複製程式碼

4 全部程式碼

const resolvePromise = (promise2,0)
                });
            }
        })

        return promise2;
    }
}


let p = new MyPromise((resolve,reject) => {
    setTimeout(() => {
        resolve(1000)
    },1000)
});
p.then(data => {
    console.log(data);
    return new MyPromise((resolve,reject) => {
        setTimeout(() => {
            resolve(1000)
        },1000)
    })

}).then(data => {
    console.log(data);

})
複製程式碼

非同步的問題可仍然沒有結束,故本次的腦圖總結放到下一次的部落格中