前端客栈 - Promise书生 (一)

前文

“前端客栈?想必前方就是宁安城了,此地为宁安城的郊区,”书生忖道,“时间不早了,今日不如在此停顿休整一日。”

进得店里,书生吩咐店小二将马匹安排妥当。订罢客房,要了杯茶,便进房间休息。不到半个钟头,外面天色已黑,便下来吃晚饭。

入住的人并不多,十来张桌子都有些空旷。店小二靠墙边站着,大概其他客人下来得早。

书生找了靠角落的位置坐下,店小二随即跟来,递上食单。“客官您看看想吃什么,这上面除了排骨和鹅,其他的都有。”

书生望向店小二,心里暗道:“这店小二虽是敝巾旧服,但生得腰圆背厚,面阔口方,更兼剑眉星眼,料想不会是久困之人。”

“一份小白菜,葱拌豆腐,三两牛肉,一碗米饭,加一瓶杨梅汁。”

“好叻,您稍等片刻。”

书生点点头,随即掏出手机打开这个礼拜的JavaScript weekly,想看看有何新鲜。

无精彩之处。

不到一刻钟,店小二端来托盘,全部上齐。随即待在书生身后的墙边。

书生左手划手机,右手夹着小葱拌豆腐,不时喝一口杨梅汁,脑子里胡思乱想。想到今天一整天未曾好好说话,心生一种孤寂之感。想到前面的路似乎还很漫长,家乡还有人等着,书生叹了口气,收起了手机,断断续续吃着。

想说说话。

见店小二目不转睛,近似呆滞,书生便抬手示意,招来店小二。

“你们店为啥叫前端客栈呢?”

“客官,这店名啊,小的听说掌柜的以前是做前端的,后来转行了,做过许多行当,但最难忘的还是前端的日子,店名也跟着叫了前端客栈。说来也怪,我来这不久也渐渐对前端生了兴趣,目前自己也在学。只是基础不太行,学得相当吃力。”店小二说完不好意思地笑了笑。

“这倒有趣,我还以为是因为这地离宁安城不远。我对前端也略知一二,不知道你学得如何了?”

“吃力归吃力,但学得还算有头有尾。最近开始学es6的Promise了,进展有些慢,可能是小的比较笨吧。”

“Promise我也略知,不知你有何困惑之处?”

“小的还没弄清Promise要解决什么问题,只是链式调用吗?”

Promise要解决什么问题

“在Promise出现之前,浏览器中的回调就已经很常见了。一个回调还好,但如果回调函数嵌套多了,则容易出现被称为‘回调地狱’的情况。这你知道,对吧?”

店小二点点头,“回调地狱就是回调函数嵌套太多,如果逻辑的分支太多会让函数难以理解,调用顺序和代码顺序对应不上。慢慢地可能难以理解难以维护。”

“是的。为了缓解回调地狱的问题,大家做了不少尝试,比如尽量恰当地拆分逻辑,然后用函数表示每一个步骤。

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log(‘Got the final result: ‘ + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);
// 得益于函数变量提升,可以自由组织位置。
function doSomething (cb){
    ...
    cb && cb(value)
}
function doSomethingElse (input,cb){
    ...
    cb && cb(value)
}
function doThirdThing (input,cb){
    ...
    cb && cb(value)
}

这样逻辑上可看得清楚一些。

“是的。除此之外,回掉函数的错误处理也是一个问题。由于回调函数不可控,我们编码时需要主动假设回调可能出错,编码时也要有相应处理。nodejs的风格是将回调函数返回的第一个参数作为错误。但这意味着需要处理每一个错误,流程将非常繁琐,需要判断很多分支。

此外,在某些情况中,人们可能使用发布订阅模式对代码逻辑进行处理。”

“发布订阅模式也被称为观察者模式吗?”店小二问道。

书生微微点头:“《headfirst设计模式》提到发布订阅模式只是观察者模式的别名,但现在人们往往认为有些区别。观察者模式的前提是被观察者有自己的发布机制,是一对多;而发布订阅模式中,发布者和订阅者都依赖一个中间人,全交由中间人处理,可以处理多对多。不过大体上它们是一样的,都是一种响应机制。”

“小的似懂非懂。”

但看到店小二的眼神仍旧不太明朗,书生打算慢慢地讲。

“除此之外,回调函数的执行没有一个统一的规范。当你想在某个函数中执行一个回调函数,那么就必须以来一个前提:这个函数定义好了形参,并且会在函数中调用。

乍看这并不是个问题,只是要定义好就行。但是否有似乎成了一种约定,在代码层面没法检测。如果有了统一的规范,统一的检测方式,事情是不是会变得更简单呢?”说完,书生定定地看着店小二。

“Promise就是那个规范,对吧?”

“是的,另外Promise还让异步调用变得更加可控。它不只是检测入参是否有回调并且调用,它同时可以传递这个回调到合适的时机去处理,这个是它最厉害的。如下面代码,cb在何时执行,就看resolveCallback要怎么处理。”

const supportCallback = function(cb){
    const resolveCallback() = ...
    resolveCallback(cb)
}

“小的听不太懂了,小的只知道Promise调用和那几个api。”

“那你给我讲讲怎么使用,如何?我看看你的了解多少。”

“好的,让小的直接敲代码吧。”说完从里屋拿出一台电脑,坐在书生旁边敲了起来。其他客人仍旧摆着原来的姿势,似游戏里npc一般,不曾打扰这两人。

Promise的使用

常规使用

//店小二在注释中写道:“以下是最基本的创建。“
const dianxiaoerPromise = new Promise(resolve => {
  setTimeout(() => {
    resolve(‘hello‘)
  }, 2000)
})
//创建之后用then
dianxiaoerPromise.then(res => {
  //1then
  console.log(res)
})

//可以分开的多次then,then里面的函数会按注册顺序执行
dianxiaoerPromise.then(res => {
  //2then
  console.log(res)
})

//还可以链式then, 链式then中传递各种类型数据,主要分为PromiseLike和非PromiseLike
//以下为非PromiseLike
dianxiaoerPromise.then(res => {
  console.log(val) //  参数val = ‘hello‘
  return ‘world‘
}).then(val => {
    console.log(val) // 参数val = ‘world‘
})

//PromiseLike
//如果有异步操作,则把操作放在Promise中,返回出去,外层的then则会在异步操作之后执行
dianxiaoerPromise.then(res => {
 return new Promise(resolve => {
      setTimeout(() => {
        resolve(‘world‘)
      }, 2000)
    })
}).then(val => {
    console.log(val) // 参数val = ‘world‘
})

//里面返回的里层的promise也可以then,并且顺序可控
dianxiaoerPromise.then(res => {
 return new Promise(resolve => {
      setTimeout(() => {
        resolve(‘world‘)
      }, 2000)
    }).then(res=>{
     return ‘调皮‘
 	})
}).then(val => {
    console.log(val) // 参数val = ‘调皮‘
})

// 但不能像下面这样,返回之前创建的Promise;这是个不合法的操作。
// 如果浏览器支持这么运行,后面的then永远无法执行
dianxiaoerPromise.then(res => {
 return dianxiaoerPromise
}).then(val => {
    console.log(val) // 参数val = ‘world‘
})

书生看着店小二敲代码,不时嗯两声,表示认可,同时细细地吃牛肉,喝杨梅汁。偶尔会一口吃上几种食物,白菜、豆腐和牛肉加酸梅汁,混在一起。

敲到这里,书生吃了6口杂烩,每一口都嚼了二十多下。

“不错不错,基本用法你掌握得相当准确了。你看你上面的写法已经缓解了回调地狱问题,异步也变得更更加可控。这就是我之前说的resolveCallback的功能:如果传递过来的还在执行的异步操作,则将自己的回调转交给那个异步操作去触发。”

店小二点点头。

“那么接下来你写写Promise中的错误处理吧。”书生又道。

店小二轻轻应了一声,继续敲代码。

错误处理

//Promise中的错误处理主要有两种方式
//用onReject函数
const p1 = new Promise(function(resolve, reject) {
  resolve(‘Success‘);
});
p1.then(function(value) {
  console.log(value); // "Success!"
  throw ‘oh, no!‘;
}).then(function(res){
  console.log(‘不会执行‘);
}, function (err) {  //onReject函数
  console.error(err); //‘oh, no!‘;
});

// 或者用catch,catch是在内部调用then(undefined, onRejected))
p1.then(function(value) {
  console.log(value); // "Success!"
  throw ‘oh, no!‘;
}).catch(function (err) {
  console.error(err); //‘oh, no!‘;
});

// 在异步函数中抛出的错误不会被catch捕获到
const p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw ‘Uncaught Exception!‘;
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // 不会执行
});

//错误处理会根据promise的状态处理,
//如果已经fullfilled,已经完成
Promise.resolve("calling next").catch(function (reason) {
    //这个方法永远不会调用
    console.log(reason);
});
//如果是失败状态
Promise.reject("calling next").catch(function (reason) {
   	//则会和then一样放在微观任务(浏览器执行机制)中,适时执行
    console.log(reason);
});

//只要抛出的错误或reject在某个环节catch住,后面的流程则回归正常
Promise.reject("calling next").catch(function (reason) {
   	//则会和then一样放在微观任务(浏览器执行机制)中,适时执行
    console.error(reason);
    return ‘错误已处理‘
}).then(res=>{
    //执行
	console.log(res) //‘错误已处理‘
},err=>{
	//后面的错误处理不会执行
})

//如果catch中出现抛出错误,则catch后面的catch才会起作用
Promise.reject("calling next").catch(function (reason) {
	throw "error"
}).then(res=>{
    //不会执行
	console.log(res) //
},err=>{
//这里才会执行
//用cacht也能执行
  	console.error(err)
})

//如果promise的reject不处理,则浏览器会触发一个unhandledrejection,一般触发源在window,也可以是Worker
//这时候进行事件监听
window.addEventListener("unhandledrejection", (event) => {
// 可能要在开发服务下才能获取到事件,直接打开文件获取不到
        console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});

“甚奇,当朝已出现电脑和手机这等精巧之物,为何老百姓仍依仗着马匹远行,”书生缓缓思索着。店小二两手一摊,舒展了下腰身。

房里穿过一阵风,带着房檐上的灯笼微微晃动。风声,蟋蟀叫,几位客人在小声说着什么。“此刻惬意即可,”书生脑中响起这句话。

“小的已经写完Promise的错误处理。”店小二把电脑稍稍移动,转向书生。

书生看了几眼:“非常不错,你对promise的掌握已经相当熟练。那么你可知道Promise的那些api?继续写吧。”

店小二写了起来。

白菜和小葱拌豆腐已吃大半,牛肉还剩不少,书生认真地看着店小二敲代码。

一些api

//Promise.all  的基本调用
//可以传入promise实例和非promise,非promise会被保留
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, ‘foo‘);
}); 

Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [3, 1337, "foo"] 
});

//当all中传入的可迭代对象为空时,all为同步
console.log("同步1");
const p1 = Promise.all([]);
console.log(p1); //resolved

console.log("异步");
const p2 = Promise.all([1, 3, 4, 5]);
console.log(p2); //pending

//如果在所有都完成前有个失败的,则all状态为失败,并且返回失败值
var p1 = new Promise((resolve, reject) => { 
  setTimeout(resolve, 1000, ‘one‘); 
}); 
var p2 = new Promise((resolve, reject) => {
  reject(‘reject‘);
});
Promise.all([p1, p2, ]).then(values => { 

},error=>{
	console.error(error) // reject
});

//Promise.race正常调用
const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, ‘one‘);
});
const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, ‘two‘);
});
Promise.race([promise1, promise2]).then((value) => {
  console.log(value);
  // Both resolve, but promise2 is faster
});

//如果为race传入空的可迭代对象,则实例一直pending
const p = Promise.race([]);
console.log(p); //pending
setTimeout(() => {
   console.log(p); //pending
}, 0);

//设x为race的参数中所能找到的第一个非pending状态promise的值,如果存在x,则返回x
//如果不存在x,则返回第一个非pending的promise的返回值
const p1 = new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(1);
        }, 500);
      });
const p2 = Promise.resovle(2);
const race = Promise.race([p1, p2,3]).then((res) => {
        console.log(res); //2
      });
const race = Promise.race([p1, 3, p2]).then((res) => {
        console.log(res); //3
      });

//Promise.resolve正常调用
Promise.resolve("Success").then(function(value) {
  console.log(value); // "Success"
}, function(value) {
  // 不会被调用
});

//resolve一个promise,
//值得一提的时,当值为promise时,resolve返回的promise的状态会跟随传入的promise
//就是说,它的状态不一定是resolved
//而reject方法则一定是reject
const p1 = new Promise(function (resolve, reject) {
   setTimeout(() => {
     resolve(15);
   }, 1000);
    //  或者reject(‘error‘)
});
Promise.resolve(p1).then(function (value) {
    console.log("value: " + value);
},e=>{
	console.error(e)
});

//resolve一个thenable对象也可以调用对象中的then
var thenable = {
   then: function (resolve) {
       resolve("Resolving");
   },
};
Promise.resolve(thenable).then((res) => {
   console.log(res);
});

//then中报错也能处理
var thenable = {
   then: function (resolve) {
       throw new TypeError("Throwing");
   },
};
Promise.resolve(thenable).catch((e) => {
   console.error(e);
});

//reject正常调用,
const p = Promise.reject(Promise.resolve())
console.log(p) //状态永远是rejected
p.then(function() {
  // not called
}, function(error) {
  console.error(error); // Stacktrace
});

“小的已写完,”店小二停下来说道。

书生的白菜和豆腐吃得差不多了,还剩下许多牛肉。从小便如此,喜欢的东西往往要留在后面。

“精彩,看得出来基础非常扎实。那么,你是打算深入理解Promise?”书生夹着牛肉说道。

“正是。小的听闻Promise并不只是浏览器的原生对象,可以通过js代码来实现,但小的琢磨不透它该如何实现。尝试看了Promise规范,却于事无补,终究理解不能。”

“正好,当年我从西域的一个网站上找到一份Promise源码,潜心修炼之后已大概融合其心法。源码先传授给你,你先自己过一遍,我们再交流,如何?”

店小二心领神会:“愿先生赐教!”

“那好,你先看着吧,我先回房休息。”丢下源码,吃完几口牛肉,灌了杨梅汁,书生便回房休息了。

灯火比刚才又暗了一些。店小二还坐在老位子,眉头多半紧缩,偶尔舒放。

作为一个店小二,他确实有些蹊跷,但在故事之外他仍然是个一本正经的店小二。至于其他客人,则都是寻常旅人,各个生得平平无奇,吃饭、休息,然后上路,没有丝毫冲突值得诉诸笔墨。

Promise源码

class MyPromise {
  constructor(exector) {
    this.status = MyPromise.PENDING;
    // 1.3 “value” is any legal JavaScript value (including undefined, a thenable, or a promise).
    this.value = null;
    // 1.5 “reason” is a value that indicates why a promise was rejected.
    this.reason = null;
    this.resolved = false;
    /**
     * 2.2.6 then may be called multiple times on the same promise
     *  2.2.6.1 If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then
     *  2.2.6.2 If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
     */

    this.onFulfilledCallback = [];
    this.onRejectedCallback = [];
    this.initBind();
    this.init(exector);
  }
  initBind() {
    this.resolve = this.resolve.bind(this);
    this.reject = this.reject.bind(this);
  }
  init(exector) {
    try {
      exector(this.resolve, this.reject);
    } catch (err) {
      this.reject(err);
    }
  }

  resolve(value) {
    if (this.status === MyPromise.PENDING && this.resolved === false) {
      this.resolved = true;
      setTimeout(() => {
        this.status = MyPromise.FULFILLED;
        this.value = value;
        this.onFulfilledCallback.forEach((cb) => cb(this.value));
      });
    }
  }

  reject(reason) {
    if (this.status === MyPromise.PENDING) {
      setTimeout(() => {
        this.status = MyPromise.REJECTED;
        this.reason = reason;
        this.onRejectedCallback.forEach((cb) => cb(this.reason));
      });
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) => value;

    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) => {
            throw reason;
          };
    let promise2;
    if (this.status === MyPromise.FULFILLED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            MyPromise.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }

    if (this.status === MyPromise.REJECTED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            MyPromise.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }

    if (this.status === MyPromise.PENDING) {
      return (promise2 = new MyPromise((resolve, reject) => {
        this.onFulfilledCallback.push((value) => {
          try {
            const x = onFulfilled(value);
            MyPromise.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });

        this.onRejectedCallback.push((reason) => {
          try {
            const x = onRejected(reason);
            MyPromise.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
  }
}

// 2.1 A promise must be in one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending";
MyPromise.FULFILLED = "fulfilled";
MyPromise.REJECTED = "rejected";

MyPromise.resolvePromise = (promise2, x, resolve, reject) => {
  let called = false;
  /**
   * 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
   */

  if (promise2 === x) {
    const err = new TypeError(
      "cannot return the same promise object from onfulfilled or on rejected callback."
    );
    console.error(err);
    return reject(err);
  }

  if (x instanceof MyPromise) {
    // 处理返回值是 Promise 对象的情况
    /**
     * new MyPromise(resolve => {
     *  resolve("Success")
     * }).then(data => {
     *  return new MyPromise(resolve => {
     *    resolve("Success2")
     *  })
     * })
     */
    if (x.status === MyPromise.PENDING) {
      /**
       * 2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.
       */
      x.then(
        (y) => {
          MyPromise.resolvePromise(promise2, y, resolve, reject);
        },
        (reason) => {
          reject(reason);
        }
      );
    } else {
      /**
       * 2.3 If x is a thenable, it attempts to make promise adopt the state of x,
       * under the assumption that x behaves at least somewhat like a promise.
       *
       * 2.3.2 If x is a promise, adopt its state [3.4]:
       * 2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.
       * 2.3.2.4 If/when x is rejected, reject promise with the same reason.
       */
      x.then(resolve, reject);
    }
    /**
     * 2.3.3 Otherwise, if x is an object or function,
     */
  } else if ((x !== null && typeof x === "object") || typeof x === "function") {
    /**
     * 2.3.3.1 Let then be x.then.
     * 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
     */
    try {
      // then 方法可能设置了访问限制(setter),因此这里进行了错误捕获处理
      const then = x.then;
      if (typeof then === "function") {
        /**
         * 2.3.3.2 If retrieving the property x.then results in a thrown exception e,
         * reject promise with e as the reason.
         */

        /**
         * 2.3.3.3.1 If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
         * 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
         */

        then.call(
          x,
          (y) => {
            /**
             * If both resolvePromise and rejectPromise are called,
             * or multiple calls to the same argument are made,
             * the first call takes precedence, and any further calls are ignored.
             */
            if (called) return;
            called = true;
            MyPromise.resolvePromise(promise2, y, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      /**
       * 2.3.3.3.4 If calling then throws an exception e,
       * 2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
       * 2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
       */

      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // If x is not an object or function, fulfill promise with x.
    resolve(x);
  }
};

相关推荐