cobish / code-analysis

源码解读

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Promise 源码:实现一个简单的 Promise

cobish opened this issue · comments

前言

Promise 是 ES6 新增的一个内置对象, 它是用来避免回调地狱的一种解决方案。

从以前一直嵌套传回调函数,到使用 Promise 来链式异步回调。Promise 究竟是怎么实现,从而达到回调函数“扁平化”?

接下来就来一步步实现一个简单的 Promise。开始发车了...

执行步骤

先来看一个使用 Promise 的简单例子:

var p = new Promise(function a (resolve) {
  console.log(1);
  setTimeout(function () {
    resolve(2);
  }, 1000);
})

p.then(function b (val) {
  console.log(val);
});

代码执行,它会先执行函数 a,打印出 1。定时器 1 秒后执行 resolve,紧接着执行了函数 b。

更详细的步骤是这样子的:

  1. new Promise,执行 Promise 构造函数;
  2. 构造函数里,执行 a 函数;
  3. 执行 then 函数;
  4. 1 秒后,执行 resolve 函数;
  5. 执行 b 函数。

这里的一个思路就是,在 then 函数执行时用一个属性保存函数 b,然后在 resolve 执行时再将其执行。

开始封装

这里定义一个 MyPromise,它有 then 函数,还有一个 callback 属性用来保存上面的“b 函数”。

function MyPromise (fn) {
  var _this = this;
  
  // 用来保存 then 传入的回调函数
  this.callback = undefined;
  
  function resolve (val) {
    _this.callback && _this.callback(val);
  }
  
  fn(resolve);
}

MyPromise.prototype.then = function (cb) {
  this.callback = cb;
};

测试使用:

var p = new MyPromise(function (resolve) {
  console.log(1);
  setTimeout(function () {
    resolve(2);
  }, 1000);
});

p.then(function (val) {
  console.log(val);
});

代码执行时会马上打印出 1,1秒后打印出 2。毛问题。

多个 resolve 调用处理

上面已经实现了一个简单的 Promise,当然还有很多种情况需要考虑。

比如会有这么一种情况,调用了多个 resolve 函数:

var p = new MyPromise(function (resolve) {
  console.log(1);
  setTimeout(function () {
    resolve(2);
    resolve(3);
    resolve(4);
  }, 1000);
});

原生的 Promise 在调用了第一个 resolve 之后,后面的 resolve 都无效化,即后面的 resolve 都是没用的代码。

这里的处理方式是,给 MyPromise 再添加一个属性 isResolved,用来记录是否调用过 resolve 函数。如果调用过,用它标识一下。再有 resolve 的调用,则用它判断返回。

function MyPromise (fn) {
  var _this = this;

  this.callback = undefined;
  this.isResolved = false;
  
  function resolve (val) {
    if (_this.isResolved) return;
    _this.isResolved = true;
    _this.callback && _this.callback(val);
  }
  
  fn(resolve);
}

多个 then 处理

继续走,除了可以调用多个 resolve 函数,同样我们可以调用多个 then 函数。

var p = new MyPromise(function (resolve) {
  console.log(1);
  setTimeout(function () {
    resolve(2);
  }, 1000);
});

p.then(function (val) {
  console.log(val);
});

p.then(function (val) {
 console.log(val);
});

与 resolve 不同,这里的每一个传给 then 的回调函数都会在 1 秒后执行,即 then 函数都有效。代码执行,先打印出 1。1 秒后,打印两个 2。

所以 MyPromise 的 callback 属性需改成数组的格式,保存着每一个 then 的回调函数。

function MyPromise (fn) {
  var _this = this;

  this.callback = [];
  this.isResolved = false;
  
  function resolve (val) {
    if (_this.isResolved) return;
    _this.isResolved = true;
    
    if (_this.callback.length > 0) {
      _this.callback.forEach(function (func) {
        func && func(val);
      });
    }
  }
  
  fn(resolve);
}

MyPromise.prototype.then = function (cb) {
  this.callback.push(cb);
};

在多次调用 then 时,MyPromise 通过属性 callback 来保存多个回调函数。在 resolve 执行后,再去遍历 callback,将它保存的回调函数逐个执行。

支持 then 链式调用

Promise 相对于回调地狱而言,它的优势在于可以进行 then 的链式调用,从而将回调函数“扁平化”。

比如我有一个异步操作需要在另一个异步操作后才能执行,只需要继续调用 then 就能够下一步回调。

var p = new MyPromise(function (resolve) {
  console.log(1);
  setTimeout(function () {
    resolve(2);
  }, 1000);
});

p.then(function (val) {
  console.log(val);
}).then(function (val) {
  console.log(val);
});

既然要能够 then 链式调用,那我在执行完 then 函数后返回 this 不就可以啦。但这不就跟刚刚的代码一样了吗?

p.then(function (val) {
  console.log(val);
});

p.then(function (val) {
  console.log(val);
});

这样就会在调用 resolve 函数之后同时执行,而不是执行完第一个 then 后,再执行第二个。所以直接返回 this 的方案是不行的。

我们再想,还有什么可以返回的,并且带有 then 函数的。答案就是 new 一个新的 MyPromise 并返回。我们需重写一个 then 函数的实现。

MyPromise.prototype.then = function (cb) {
  var _this = this;

  return new MyPromise(function (resolve) {
    _this.callback.push({
      cb: cb,
      resolve: resolve
    });
  });
};

这里 callback 重新改写了一下,保存的是 then 的回调函数,和新 new 的 MyPromise 的 resolve 函数。保存的 resolve 函数先不执行,因为我们知道,它一旦执行了,就会触发传入 then 的回调函数的执行。

同时,MyPromise 构造函数里的 resolve 也需要调整一下:

function MyPromise (fn) {
  var _this = this;

  this.callback = [];
  this.isResolved = false;
  
  function resolve (val) {
    if (_this.isResolved) return;
    _this.isResolved = true;

    if (_this.callback.length > 0) {
       _this.callback.forEach(function (item) {
       var res;
       var cb = item.cb;
       var resolve = item.resolve;
       
       cb && (res = cb(val));
       resolve && resolve(res);
     });
    }
  }
  
  fn(resolve);
}

在执行第一个 then 的回调函数时,将其执行完返回的值,作为保存的 resolve 的参数传入。

p.then(function (val) {
  console.log(val);
  return val + 1;
}).then(function(val) {
  console.log(val);
});

这样子,就能够链式的 then 调用。先别急,我们实现的只是 then 的同步链式调用,而我们最终要的是异步的链式调用。

我们需要这样子的:

p.then(function (val) {
  console.log(val);
  
  return new MyPromise(function (resolve) {
    setTimeout(function () {
      resolve(val + 1);
    }, 1000);  
  });
}).then(function(val) {
  console.log(val);
});

先打印出 1,1秒后打印出第一个 then 里的 2,再过多一秒,打印出第二个 then 的 3。

所以,我们需要在取出原来保存的 cb 返回的值进行判断。如果该值是一个 MyPromise 对象,则调用它的 then,否则跟原来一样调用。

function MyPromise (fn) {
  var _this = this;

  this.callback = [];
  this.isResolved = false;
  
  function resolve (val) {
    if (_this.isResolved) return;
    _this.isResolved = true;

    if (_this.callback.length > 0) {
      _this.callback.forEach(function (item) {
        var res;
        var cb = item.cb;
        var resolve = item.resolve;
        
        cb && (res = cb(val));
        if (typeof res === 'object' && res.then) {
          res.then(resolve);
        } else {
          resolve && resolve(res);
        }
      });
    }
  }
  
  fn(resolve);
}

最后

在我们实现的 MyPromise 里,有两个属性,分别是 isResolvedcallback。isResolved 是一个标识,用来防止多次调用 resolve。callback 是一个数组,用来保存回调函数。

MyPromise 还有一个 then 函数,用来处理异步后的回调,能够链式异步调用。

这样子就实现了一个简单的 Promise,完整的代码戳 这里