確認過眼神,你就是我的Promise~~

花樣前端發表於2018-05-15

小哥哥、小姐姐,你們好,請把手伸出來,我給你們點東西。

1、JavaScript非同步程式設計

  • 同步與非同步
  • 回撥函式
  • promise
  • generator
  • async+await

2、寫一個符合規範的promise庫

1、JavaScript非同步程式設計

1-1、同步與非同步

我們都知道js是單執行緒語言,這就導致了會有同步非同步的概念。所謂同步,就是指指令碼直譯器在解析程式碼時,從上往下一行一行解釋,第一行解釋不完,就不去解釋第二行。所謂非同步,就是指,當解釋到某一行中,發現有非同步方法(比如settimeout、ajax、DOM點選事件等等),直譯器不會去等待非同步方法執行完,再往下解釋。而是,將非同步任務放到任務佇列,當所有的同步程式碼全部執行完,也就是主執行緒沒有可執行的程式碼了,就回去任務佇列拿出之前遇到的非同步任務,執行,執行完畢後再去任務佇列調取下一個任務,如此迴圈。

一圖勝千言

同步
一次只能服務一個人,請排好隊。誰事兒多也沒辦法,後面只能等著。
非同步
三個人同時吃飯,這是非同步。如果是同步的話,只能一個人吃完下一個人再吃。

1-2、回撥函式

相信大家對回撥函式已經不陌生了,函式A作為引數被傳遞到函式B裡,那麼函式A就是回撥函式。有什麼用呢?請看程式碼

let doSomething = () => { console.log('do something') }
setTimeout(doSomething, 500);
console.log('a');
複製程式碼

宣告瞭一個doSomething函式,並作為第一個引數傳遞給了setTimeout函式,setTimeout函式會在合適的時機執行它。達到了非同步程式設計的目的。這種方式用處有很多,node.js有大部分api都是通過回撥來實現非同步程式設計的。

1-3、promise 寫法

回撥函式這種形式有一個缺點,那就是如果非同步任務比較多的話,並且多工執行有先後順序,那麼回撥函式很容易就形成多層巢狀。如下:

function doA() { }
function doB() { }
function doC() { }
function doD() { }

doA(function () {
    doB(function () {
        doC(function () {
            doD(function () {

            })
        })
    })
})
複製程式碼

當改用promise後,瞬間清爽了許多。

new Promise(function(resolve,reject){
    resolve();//在合適的時機出發resolve
})
.then(doA,null)
.then(doB,null)
.then(doC,null)
.then(doD,null)
複製程式碼

這也是這篇文章的重點講解內容,一會我會一步一步按照規範編寫一個promise庫。徹底搞懂promise。

1-4、generator 寫法

function* gen() {
    let a = yield doA();
    let b = yield doB();
    let c = yield doC();
    let d = yield doD();
}
let it = gen();//it是一個迭代器
it.next();//{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: true }
複製程式碼

可以看到,generator函式有另外一個功能,那就是可以暫停,不像普通函式,只要一執行,那就會一口氣執行完。

1-5、async + await 寫法

async function doSomething() {
    await doA();
    await doB();
    await doC();
    await doD();
}
doSomething();
複製程式碼

是不是發現,這種寫法更加簡潔,就像在寫同步程式碼一樣。

2、寫一個符合規範的promise庫

2-1、實現最簡單的一個Promise類

先來看Promise的用法,然後根據用法一步步編寫Promise

let p1 = new Promise(function (resolve, reject) {

})
p1.then(function (data) {
    console.log(data)
}, function (err) {
    console.log(err)
})
複製程式碼

在例項化一個Promise時,傳入一個函式作為引數,該函式接受兩個引數,分別為resolve,reject,然後按照Promise/A+規範一個Promise類應該包含如下狀態

  • status
  • value
  • reason
  • onResolvedCallbacks
  • onRejectedCallbacks 我們很容易就寫出了Promise的原型。程式碼如下:
function Promise(executor) {
    let self = this;
    self.status = 'pending';
    self.value = undefined;
    self.reason = undefined;
    self.onResolvedCallbacks = [];
    self.onRejectedCallbacks = [];
    function resolve() { }
    function reject() { }
    executor(resolve, reject);
}
Promise.prototype.then = function (onFulfilled, onRejected) {

}
複製程式碼

接下來我們一個一個方法去攻破。

2-2、 實現resolvereject 方法

resolve方法需要完成的事情是:

  • 將Promise的狀態置為resolved
  • 更改self.value的值
  • 通知self.onResolvedCallbacks,並一一執行。

reject方法需要完成的事情是

  • 將Promise的狀態置為rejectd
  • 更改self.reason的值
  • 通知self.onRejectedCallbacks,並一一執行。 程式碼如下:
 function resolve(value) {
        self.status = 'resolved';
        self.value = value;
        self.onResolvedCallbacks.forEach(item => item(value))
    }
function reject(reason) {
        self.status = 'rejected';
        self.reason = reason;
        self.onRejectedCallbacks.forEach(item => item(reason))
    }
複製程式碼

2-3、 實現then方法

then方法的作用是收集到成功、失敗的回撥函式,將他們分別新增到成功和失敗的陣列中。也就是程式碼中,我們需要將onFulfilled新增到self.onResolvedCallbacks裡,將onRejected新增到self.onRejectedCallbacks裡。

Promise.prototype.then = function (onFulfilled, onRejected) {
    this.onResolvedCallbacks.push(onFulfilled);
    this.onRejectedCallbacks.push(onRejected);
}
複製程式碼

寫到這裡,這個Promise其實已經可以用了,不過還有一個潛在的問題。那就是,當resolve方法被同步呼叫時,通過then方法加入到佇列的函式沒有被執行。只有resolve被非同步呼叫時才會被執行,為什麼呢。因為這裡的then是同步的,resolve也被同步呼叫的話,那肯定是,先執行resolve後執行then,換句話說就是,先執行回撥,後新增回撥,這不是我們想看到的,要達到先新增回撥,後執行回撥的效果,我們稍作修改。

function resolve(value) {
        setTimeout(() => {
            if (self.status === 'pending') {
                self.status = 'resolved';
                self.value = value;
                self.onResolvedCallbacks.forEach(item => item(value))
            }
        });
    }
function reject(reason) {
        setTimeout(function () {
            if (self.status == 'pending') {
                self.value = value;
                self.status = 'rejected';
                self.onRejectedCallbacks.forEach(item => item(value));
            }
        });
    }
複製程式碼

這裡加入了狀態判斷,因為當Promise的狀態一旦確定,就不能更改,所以狀態只能是pending時,resolvereject才生效。

2-4、實現鏈式呼叫

先看一下Promise/A+規範中對then方法的返回值描述。

確認過眼神,你就是我的Promise~~
規範第一句就表明,then方法必須返回一個promise,以實現鏈式呼叫。現在我們需要關注的是,呼叫then方法時,傳入的第一個引數(onFulfilled)的返回值問題。如果是一個普通值,那我們就把它繼續傳遞下去,傳遞給then方法返回的promise裡;如果是一個新的promise的話,那就需要將新的promisethen方法返回的promise關聯起來。 具體如何關聯,我們們慢慢來,這裡有點繞,我先上張圖看圖說話。

確認過眼神,你就是我的Promise~~
圖中有部分程式碼只需要注意三個promisep1xp2,為了不造成混淆,這三個東西我一次標到了圖的左部分,他們是對應的。請大家先明白一句話,然後我們開始說。

呼叫promise的resolve方法,會執行該promise的then函式的第一個引數

(這裡我那resolve舉例,reject道理一樣,就不贅述了。)

看圖,請看圖。

呼叫p1的resolve方法,那麼a就會被執行。

呼叫p2的resolve方法,那麼c就會被執行。

也就是說,你想執行a或者b或者c或者d,那麼你得找到它屬於哪個promise,例如:圖中,a b 屬於p1,c d屬於p2.這個關係必須要明確。

假設你已經理解了上面的話,現在我們面臨的問題來了。

c d 本來是屬於p2的,執行還是不執行也得看p2調不呼叫resolve、reject。 現在要讓c d執不執行不看p2了,得看x。為什麼要看x,因為x這個回撥函式是使用者傳遞的,使用者的意思是:我讓這個回撥返回一個promise,然後繼續使用then方法新增成功或失敗的回撥,而且這兩個回撥啥時候執行,得看我返回的那個promise。 反應到圖中就是這個意思:c d何時執行,看x何時呼叫resolve、reject。

希望你理解了。...繼續

破解方法:將p2的resolve、reject放入到x的then方法裡。 解釋一下:x的resolve、reject是暴露給使用者的,也就是說,這兩個方法的執行權在使用者手裡,當使用者執行resolve時,其實就執行了x的then方法的第一個引數,而x的then方法的第一個引數正好是p2的resolve,p2的resolve就被執行了,p2的resolve一執行,那麼c就被執行了。就實現了x的resolve、reject控制著c d的執行與否。

說了這麼多,上程式碼吧還是,改造後的then方法如下,加入了狀態判斷錯誤捕獲

Promise.prototype.then = function (onFulfilled, onRejected) {
    let promise2;
    let self = this;
    if (self.status === 'resolve') {
        promise2 = Promise(function (resolve, reject) {
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        })
    }
    if (self.satus === 'rejected') {
        promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    if (self.status === 'pending') {
        promise2 = new Promise(function (resolve, reject) {
            self.onResolvedCallbacks.push(function (value) {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
            self.onRejectedCallbacks.push(function (reason) {
                try {
                    let x = onRejected(reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    return promise2;
}
複製程式碼

再把resolvePromise方法寫一下,因為多個地方用到了,所以就單獨封裝了。

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('迴圈引用'));
  }
  let then, called;

  if (x != null && ((typeof x == 'object' || typeof x == 'function'))) {
    try {
      then = x.then;
      if (typeof then == 'function') {
        then.call(x, function (y) {
          if (called) return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, function (r) {
          if (called) return;
          called = true;
          reject(r);
        });
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}
複製程式碼

到這裡,我們寫的promise已經支援鏈式呼叫了。我希望閱讀本文的你,先去讀懂那張圖,一定要看懂,知道自己在幹什麼,就是思路要清晰,然後再去寫程式碼。我剛接觸的時候,就是一步步去捋思路,然後輔助畫圖去理解。用了好幾天才弄懂。

2-5、實現catchallracePromise.resolve()Promise.reject()

相比較then方法,這幾個方法就輕鬆許多了。直接上程式碼了,我會把註釋寫到程式碼裡邊 catch

Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);//原來這麼簡單
}
複製程式碼

all

Promise.all = function (promises) {
  return new Promise(function (resolve, reject) {
    let result = [];//結果集
    let count = 0;//計數器,用來記錄promise有沒有執行完
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(function (data) {
        result[i] = data;
        if (++count == promises.length) {
          resolve(result);//計數器滿足條件時,觸發resolve
        }
      }, function (err) {
        reject(err);
      });
    }
  });
}
複製程式碼

race

// 只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了
Promise.race = function (promises) {
  return new Promise(function (resolve, reject) {
      for (var i = 0; i < promises.length; i++) {
          promises[i].then(resolve,reject)
      }
  })
}
複製程式碼

Promise.resolve()Promise.reject()

// 生成一個成功的promise
Promise.resolve = function (value) {
  return new Promise(function (resolve, reject) {
    resolve(value);
  })
}
// 生成一個失敗的promise
Promise.reject = function (reason) {
  return new Promise(function (resolve, reject) {
    reject(reason);
  })
}
複製程式碼

2-6、Promise的語法糖

Promise.deferred = Promise.defer = function () {
  var defer = {};
  defer.promise = new Promise(function (resolve, reject) {
    defer.resolve = resolve;
    defer.reject = reject;
  })
  return defer;
}
複製程式碼

看一個例子

let fs = require('fs');
let Promise = require('./promise');
function read() {
   // 好處就是解決巢狀問題
   // 壞處錯誤處理不方便了
    let defer = Promise.defer();
    fs.readFile('./2.promise.js/a.txt','utf8',(err,data)=>{
      if(err)defer.reject(err);
      defer.resolve(data)
    });
    return defer.promise;
}
read().then(data=>{
  console.log(data);
});
複製程式碼

說了很多,希望你們理解了,如果文中有錯誤或者大家有不懂的地方,歡迎留言。

相關文章