確認過眼神,你就是個人Promise~~

小哥哥、小姐姐,大家好,請把手伸出來,我給大家點東西。javascript

一、JavaScript異步編程

  • 同步與異步
  • 回調函數
  • promise
  • generator
  • async+await

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

一、JavaScript異步編程

1-一、同步與異步

咱們都知道js是單線程語言,這就致使了會有同步異步的概念。所謂同步,就是指腳本解釋器在解析代碼時,從上往下一行一行解釋,第一行解釋不完,就不去解釋第二行。所謂異步,就是指,當解釋到某一行中,發現有異步方法(好比settimeout、ajax、DOM點擊事件等等),解釋器不會去等待異步方法執行完,再往下解釋。而是,將異步任務放到任務隊列,當全部的同步代碼所有執行完,也就是主線程沒有可執行的代碼了,就回去任務隊列拿出以前遇到的異步任務,執行,執行完畢後再去任務隊列調取下一個任務,如此循環。java

一圖勝千言node

同步
一次只能服務一我的,請排好隊。誰事兒多也沒辦法,後面只能等着。
異步
三我的同時吃飯,這是異步。若是是同步的話,只能一我的吃完下一我的再吃。

1-二、回調函數

相信你們對回調函數已經不陌生了,函數A做爲參數被傳遞到函數B裏,那麼函數A就是回調函數。有什麼用呢?請看代碼ajax

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

聲明瞭一個doSomething函數,並做爲第一個參數傳遞給了setTimeout函數,setTimeout函數會在合適的時機執行它。達到了異步編程的目的。這種方式用處有不少,node.js有大部分api都是經過回調來實現異步編程的。編程

1-三、promise 寫法

回調函數這種形式有一個缺點,那就是若是異步任務比較多的話,而且多任務執行有前後順序,那麼回調函數很容易就造成多層嵌套。以下:api

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。promise

1-四、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函數有另一個功能,那就是能夠暫停,不像普通函數,只要一執行,那就會一口氣執行完。bash

1-五、async + await 寫法

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

是否是發現,這種寫法更加簡潔,就像在寫同步代碼同樣。異步

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

2-一、實現最簡單的一個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-二、 實現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-三、 實現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-四、實現鏈式調用

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

規範第一句就代表, then方法必須返回一個 promise,以實現鏈式調用。如今咱們須要關注的是,調用 then方法時,傳入的第一個參數( onFulfilled)的返回值問題。若是是一個普通值,那咱們就把它繼續傳遞下去,傳遞給then方法返回的 promise裏;若是是一個新的 promise的話,那就須要將新的 promisethen方法返回的 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-五、實現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-六、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);
});
複製代碼

說了不少,但願大家理解了,若是文中有錯誤或者你們有不懂的地方,歡迎留言。

相關文章
相關標籤/搜索