老生常談-實現一個Promise

h雪痕發表於2018-12-10

前言

在寫這個promise之前,希望你已經對es6中的Promise很熟悉了,概念性和基礎的東西就不再講了,不懂的同學可以去看看阮一峰老師的es6教程. 我主要按以下5個步驟來一步一步實現,非同步的實現我放在了後面,所以前面幾步暫不考慮

  1. 實現一個基本的MyPromise
  2. 實現then的鏈式呼叫
  3. 處理reolve函式的引數是MyPromise例項的情況以及處理then方法中前一個回撥函式返回的也是一個MyPromise例項的情況
  4. 實現非同步的MyPromise
  5. 完善MyPromise的其它方法

1.實現一個基本的MyPromise

/*
 * 這裡我將promise的3個狀態分別定義為: pending, resolved, rejected
 * 其中fn必須是個函式, 必須通過new來使用
 */
function MyPromise(fn) {
  if (!(this instanceof MyPromise)) {
    throw new TypeError('MyPromise must be constructed via new');
  }
  if (typeof fn !== 'function') {
    throw new TypeError('MyPromise constructor argument is not a function');
  }
  this.state = 'pending';  // 出初始化狀態
  this.value = undefined;  // 初始化一個值, 用來儲存resolve或者reject的值
  // 執行 fn 方法
  executeFn(fn, this);
}

MyPromise.prototype.then = function(onFullfilled, onRejected) {
  var res = undefined;
  var cb = this.state === 'resolved' ? onFullfilled : onRejected;
  res = cb(this.value);
}

// 執行 fn 方法
function executeFn(fn, promise) {
  var done = false;     // 宣告一個變數, 防止resolve, reject連續呼叫
  try {
    fn(function _resolve(value) {
      if(done) return;
      done = true;
      resolve(promise, value);
    }, function _reject(reason) {
      if(done) return;
      done = true;
      reject(promise, reason);
    });
  }
  catch(err) {
    if(!done) {
      done = true;
      reject(promise, err);
    }
  }
}

function resolve(promise, value) {
  promise.state = 'resolved';
  promise.value = value;
}

function reject(promise, error) {
  promise.state = 'rejected';
  promise.value = error;
}
複製程式碼

這樣就實現了一個基本版的MyPromise,呼叫方法同Promise,如下:

new MyPromise((resolve, reject) => {
  // resolve('resolved');
  reject('rejected');
}).then(res => {
  console.log('>>> res', res);
}, err =>  {
  console.log('>>> err', err);
});
複製程式碼

2.實現then的鏈式呼叫

原生Promise支援鏈式呼叫,並且then方法會返回一個新的Promise例項,第一個回撥函式完成以後,會將返回結果作為引數,傳入第二個回撥函式,所以我們可以修改下then方法,其餘不變

MyPromise.prototype.then = function(onFullfilled, onRejected) {
  var self = this;
  var res = undefined;
  var cb = this.state === 'resolved' ? onFullfilled : onRejected;
  var newPromise = new MyPromise(function(_resolve, _reject) {
    try {
      res = cb(self.value);
      _resolve(res);
    }
    catch(err) {
      _reject(err);
    }
  });

  return newPromise;
}
複製程式碼

這樣的話,鏈式呼叫也就實現了,測試了下,沒啥問題

new MyPromise((resolve, reject) => {
  resolve('resolved');
  // reject('rejected');
}).then(res => {
  console.log('>>> res', res);
  return 'res1';
}, err => {
  console.log('>>> err', err);
  return 'err1';
}).then(res2 => {
  console.log('>>> res2', res2);
 }, err2 => {
  console.log('>>> err2', err2);
});
複製程式碼

3. 處理reolve函式的引數是MyPromise例項的情況以及處理then方法中前一個回撥函式返回的也是一個MyPromise例項的情況

resolve函式的引數除了正常的值以外,還可能是另一個MyPromise例項,如下,這個時候p1的狀態決定了p2的狀態

const p1 = new MyPromise(function(resolve, reject) {
  // ...
});

const p2 = new MyPromise(function(resolve, reject) {
  // ...
  resolve(p1);
});
複製程式碼

同樣,在then方法中,前一個回撥函式有可能返回的也是一個MyPromise物件,這時後一個回撥函式就會等待該MyPromise物件的狀態發生變化,才會被呼叫,例如:

const p1 = new MyPromise(function(resolve, reject) {
  // ...
});

const p2 = new MyPromise(function(resolve, reject) {
  // ...
  resolve('p2 resolved');
});
p2.then(res1 => {
  console.log('>>> res1', res1);
  return p1;
}, err1 => {
  console.log('>>> err1', err1);
}).then(res2 => {
  console.log('>>> res2', res2);
}, err2 => {
  console.log('>>> err2', err2);
})
複製程式碼

瞭解以上後,我們來實現它

function resolve(promise, value) {
  if(!handlePromise(promise, value)) return;  // 增加這行

  promise.state = 'resolved';
  promise.value = value;
}
/*
 * 增加一個函式用來處理返回值或者resolve的引數是promise的情況, 最後的返回值起個標識作用
 */
function handlePromise(promise, value) {
  if(value === promise) {
    reject(promise, 'A promise cannot be resolved with itself');
    return;
  }
  if(value && (typeof value === 'object' || typeof value === 'function')) {
    var then = value.then;
    if(typeof then === 'function') {
      executeFn(then.bind(value), promise);
      return;
    }
  }
  return true;
}
// 同時then中增加一行程式碼
MyPromise.prototype.then = function(onFullfilled, onRejected) {
  var self = this;
  var res = undefined;
  var cb = this.state === 'resolved' ? onFullfilled : onRejected;
  var newPromise = new MyPromise(function(_resolve, _reject) {
    if(self.state === 'pending') return;  // 增加這行
    try {
      res = cb(self.value);
      _resolve(res);
    }
    catch(err) {
      _reject(err);
    }
  });

  return newPromise;
}
複製程式碼

到這裡,就解決了以上2個問題,測試一下

const p = new MyPromise((resolve, reject) => {
  // resolve('resolve a promise');
  reject('reject a promise');
});
// resolve引數是MyPromise例項
new MyPromise((resolve, reject) => {
  resolve(p);
  // reject('rejected');
}).then(res => {
  console.log('>>> res', res);
  return 'res1';
}, err => {
  console.log('>>> err', err);
  return 'err1';
}).then(res2 => {
  console.log('>>> res2', res2);
}, err2 => {
  console.log('>>> err2', err2);
});

// then第一個方法返回的是MyPromise例項
new MyPromise((resolve, reject) => {
  resolve('resolved');
  // reject('rejected');
}).then(res => {
  console.log('>>> res', res);
  return p;
}, err => {
  console.log('>>> err', err);
  return 'err1';
}).then(res2 => {
  console.log('>>> res2', res2);
}, err2 => {
  console.log('>>> err2', err2);
});
複製程式碼

4. 實現非同步的MyPromise

經過前面3步,MyPromise的已經實現了大半,接著我們來實現非同步的MyPromise. 既然是非同步,我們並不知道它什麼時候結束,但是我們可以將它的非同步回撥存入一個陣列,待它結束後執行它,好吧,其實就是觀察者模式了

首先在建構函式中加上一句

function MyPromise(fn) {
  // ...這裡省略,加上下面這行
  this.callbacks = [];     // 儲存非同步的回撥方法
  // 執行 fn 方法
  executeFn(fn, this);
}
複製程式碼

然後在resolve和reject方法中分別加上

function resolve(promise, value) {
  // ...這裡省略,加上下面幾句
  promise.callbacks.forEach(function(cb) {
    cb();
  });
}
function reject(promise, error) {
  // ...這裡省略,加上下面幾句
  promise.callbacks.forEach(function(cb) {
    cb();
  });
}
複製程式碼

最後在then方法中,將非同步的回撥發存入陣列中

MyPromise.prototype.then = function(onFullfilled, onRejected) {
  // ...這裡省略,不變
  var newPromise = new MyPromise(function(_resolve, _reject) {
    // 狀態為pending時,將回撥存入陣列,因為then中方法也是非同步執行
    // 所以用setTimeout,同時直接return
    if(self.state === 'pending') {
      self.callbacks.push(function() {
        setTimeout(function() {
          // 這裡需要再次判斷
          cb = self.state === 'resolved' ? onFullfilled : onRejected;
          try {
            res = cb(self.value);
            _resolve(res);
          }
          catch(err) {
            _reject(err);
          }
        });
      });
      return;
    }
    // then中是非同步執行
    setTimeout(function() {
      try {
        res = cb(self.value);
        _resolve(res);
      }
      catch(err) {
        _reject(err);
      }
    });
  });

  return newPromise;
}
複製程式碼

到這裡,非同步的MyPromise也就實現了,then方法程式碼有點亂,我們整理下

MyPromise.prototype.then = function(onFullfilled, onRejected) {
  // 這裡刪除了一部分程式碼
  var self = this;
  var newPromise = new MyPromise(function(_resolve, _reject) {
    if(self.state === 'pending') {
      self.callbacks.push(function() {
        // 同時將這部分的程式碼抽成了以下方法
        handleResolved(self, onFullfilled, onRejected, _resolve, _reject);
      });
      return;
    }
    handleResolved(self, onFullfilled, onRejected, _resolve, _reject);
  });

  return newPromise;
}
function handleResolved(promise, onFullfilled, onRejected, _resolve, _reject) {
  setTimeout(function() {
    var res = undefined;
    var cb = promise.state === 'resolved' ? onFullfilled : onRejected;
    // 需要對cb進行判斷
    if(typeof cb !== 'function') {
      if(promise.state === 'resolved') {
        _resolve(promise.value);
      }
      else {
        _reject(promise.value);
      }
      return;
    }
    try {
      res = cb(promise.value);
      _resolve(res);
    }
    catch(err) {
      _reject(err);
    }
  });
}
複製程式碼

測試如下, 當然沒啥問題了

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('resolve a promise');
    // reject('reject a promise');
  }, 2 * 1000);
});

new MyPromise((resolve, reject) => {
  resolve(p);
  // reject('rejected');
}).then(res => {
  console.log('>>> res', res);
  return 'res1';
}, err => {
  console.log('>>> err', err);
  return 'err1';
}).then(res2 => {
  console.log('>>> res2', res2);
}, err2 => {
  console.log('>>> err2', err2);
});
複製程式碼

到這裡,一個大概的MyPromise也就基本實現完成了,整理後的完整程式碼如下

function MyPromise(fn) {
  if (!(this instanceof MyPromise)) {
    throw new TypeError('MyPromise must be constructed via new');
  }
  if (typeof fn !== 'function') {
    throw new TypeError('MyPromise constructor argument is not a function');
  }
  this.state = 'pending';  // 初始化狀態
  this.value = undefined;  // 初始化一個值, 用來儲存resolve或者reject的值
  this.callbacks = [];     // 儲存非同步的回撥方法
  // 執行 fn 方法
  executeFn(fn, this);
}

MyPromise.prototype.then = function(onFullfilled, onRejected) {
  var self = this;
  var newPromise = new MyPromise(function(_resolve, _reject) {
    if(self.state === 'pending') {
      self.callbacks.push(function() {
        handleResolved(self, onFullfilled, onRejected, _resolve, _reject);
      });
      return;
    }
    handleResolved(self, onFullfilled, onRejected, _resolve, _reject);
  });

  return newPromise;
}

function handleResolved(promise, onFullfilled, onRejected, _resolve, _reject) {
  setTimeout(function() {
    var res = undefined;
    var cb = promise.state === 'resolved' ? onFullfilled : onRejected;
    if(typeof cb !== 'function') {
      if(promise.state === 'resolved') {
        _resolve(promise.value);
      }
      else {
        _reject(promise.value);
      }
      return;
    }
    try {
      res = cb(promise.value);
      _resolve(res);
    }
    catch(err) {
      _reject(err);
    }
  });
}

// 執行 fn 方法
function executeFn(fn, promise) {
  var done = false;     // 宣告一個變數, 防止resolve, reject連續呼叫
  try {
    fn(function _resolve(value) {
      if(done) return;
      done = true;
      resolve(promise, value);
    }, function _reject(reason) {
      if(done) return;
      done = true;
      reject(promise, reason);
    });
  }
  catch(err) {
    if(!done) {
      done = true;
      reject(promise, err);
    }
  }
}

function resolve(promise, value) {
  if(!handlePromise(promise, value)) return;

  promise.state = 'resolved';
  promise.value = value;
  promise.callbacks.forEach(function(cb) {
    cb();
  });
}

function reject(promise, error) {
  promise.state = 'rejected';
  promise.value = error;
  promise.callbacks.forEach(function(cb) {
    cb();
  });
}

// 用來處理返回值或者resolve的引數是promise的情況, 最後的返回值起個標識作用
function handlePromise(promise, value) {
  if(value === promise) {
    reject(promise, 'A promise cannot be resolved with itself');
    return;
  }
  if(value && (typeof value === 'object' || typeof value === 'function')) {
    var then = value.then;
    if(typeof then === 'function') {
      executeFn(then.bind(value), promise);
      return;
    }
  }
  return true;
}
複製程式碼

5. 最後來實現下MyPromise的其它方法

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

// 只要不是pending狀態都會執行
MyPromise.prototype.finally = function(cb) {
  return this.then(
    function(value) {
      return MyPromise.resolve(cb()).then(function() {
        return value;
      });
    },
    function(err) {
      return MyPromise.resolve(cb()).then(function() {
        throw err;
      });
    }
  );
}

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

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

/*
 * all方法用於將多個 MyPromise 例項,包裝成一個新的 MyPromise 例項
 * 只有全部例項都resolved,才會resolve; 只要其中一個rejected,就會reject
 * 引數可以不是陣列,但必須具有 Iterator 介面, 同時裡面的值可能也不是promise例項
 */
MyPromise.all = function(promiseArr) {
  var args = [].slice.call(promiseArr);

  return new MyPromise(function(resolve, reject) {
    var arr = [];
    var resolveCount = 0;
    var argsLen = args.length;
    for(var i = 0; i < argsLen; i++) {
      handle(i, args[i]);
    }
    function handle(index, val) {
      MyPromise.resolve(val).then(function(value) {
        arr[index] = value;
        if(++resolveCount === argsLen) {
          resolve(arr);
        }
      }, reject);
    }  
  });
}

/*
 * race方法與all方法類似,只要其中一個例項狀態發生改變resolved / rejected即可
 * 引數可以不是陣列,但必須具有 Iterator 介面, 同時裡面的值可能也不是promise例項
 */
MyPromise.race = function(promiseArr) {
  var args = [].slice.call(promiseArr);
  return new MyPromise(function(resolve, reject) {
    for(var i = 0; i < args.length; i++) {
      MyPromise.resolve(args[i]).then(resolve, reject);
    }
  });
}
複製程式碼

至此Promise的實現就算完成了,完整程式碼的地址點這裡

相關文章