Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promise #34

Open
XXHolic opened this issue Jun 9, 2019 · 0 comments
Open

Promise #34

XXHolic opened this issue Jun 9, 2019 · 0 comments

Comments

@XXHolic
Copy link
Owner

XXHolic commented Jun 9, 2019

目录

引子

在回顾异步编程相关的知识时,发现 Promise 里面相关的东西,比实际使用 API 要多的多。在网上也查询了一些相关的资料,虽然标准里面都已经写好了,但每篇给人的感觉还是不一样,所以整理一篇符合自己感觉的总结。

异步

在 JavaScript 中异步机制还是很常用的,比如请求使用的 ajaxsetTimeout 延时处理、打印日志的 console 。在使用异步时,表现出来的是现在运行的一部分和将来运行的一部分之间的关系。在“现在”和“将来”之间的这段空隙,在很多的程序中都需要考虑如何管理。在 JavaScript 中使用了“事件循环”的机制来处理异步。

所有异步任务会进入到“任务队列”,一旦有事件需要运行,事件循环就会运行,直到队列清空。用户交互、IO 和定时器会向事件队列中加入事件。任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接的引发一个或多个后续事件。

回调

在处理异步逻辑时,最常用的方式是回调。回调在异步中的使用有两个主要缺陷:缺乏顺序性和可信任性。

缺乏顺序性

工作过一段时间的人,很有可能遇到这样的代码:

ajax('https://url1',function(data) {
  doSomething1();
  if (data === 200) {
    doSomething3();
    ajax('https://url2',function(data2) {
      if (data2 === 200) {
        ajax('https://url3',function() {
          doSomething4();
        });
      } else {
        doSomething5();
      }
    });
  } else {
    doSomething2();
  }
})

这种代码常被称为回调地狱,在维护和阅读时,都需要花费相当的精力。

缺乏可信任性

看下面一个例子:

doSomethingA();

ajax('https://url',function() {
  doSomethingC()
});

doSomethingB();

在这段程序中,doSomethingAdoSomethingB 发生在现在,在主程序的控制下,而 doSomethingC 发生在将来,在第三方的控制下。这种把自己程序一部分的执行控制交给某个第三方的情况,被称为控制反转。一段时间过后,在某一天,有人反馈 doSomethingC 重复执行了 3 次。为了防止这种情况,进行了下面的处理:

var isDone = false;

ajax('https://url',function() {
  if (!isDone) {
    isDone = true;
    doSomethingC()
  }
});

这样似乎就解决了问题,但再想一想,就会发现还可能出现下面的问题:

  • 没有调用;
  • 调用超时;
  • 调用过早;
  • 等等

为此,我们需要构建对应的机制,针对这些情况进行处理,不管是自己控制下的代码,还是第三方的代码。这些看似合理的处理,表现出来的是回调缺乏可信任性。

如果我们把控制反转再反转回来,希望第三方给我们提供了解任务何时结束的能力,然后由我们自己的代码决定接下来要做的事情,那将会是样?这就是 Promise 要做的事情。

Promise

Promise 是一个对象,是用来保存延期(可能是异步)计算结果的一个容器。

任何 Promise 对象拥有下面三种状态中的一种:

  • fulfilled:已成功
  • rejected:已失败
  • pending:进行中
const promise = new Promise(function(resolve, reject) {
  if (/* 异步操作成功 */) {
    resolve("resolve value");
  } else {
    reject("reject value");
  }
});

console.info('promise:', promise);

Promise 构造函数接收一个函数作为参数,该函数的第一个参数用于标识 Promise 已经完成(从 pending 变为 fulfilled )。第二个参数用于标识 Promise 被拒绝(从 pending 变为 rejected )。下面假设异步操作成功,看下返回的是什么。

34-resolved

可以看到,返回对象中有个存储目前 Promise 状态的 PromiseStatus 字段,还有存放值的字段 PromiseValue

Promise 缺点

  • 一旦创建了一个 Promise 并为其注册完成或拒绝函数,无法取消程序的执行。
  • 如果不设置回调函数,Promise 内部报错不会反应到外部。
  • 当处于 pending 状态时,无法得知目前进展到那一个阶段。

Properties of the Promise Prototype Object

在上面打印的值中,还可以发现 __proto__ 上的几个方法:then、catch、finally 。

then

then 方法的作用是为 Promise 实例添加状态改变时的回调函数。then 方法的第一个参数是 fulfilled 状态的回调函数,第二个参数是 rejected 状态的回调函数。下面看一个例子。

  var getJSONData = function(url) {
    const promise = new Promise(function(resolve, reject) {
      var xhr = new XMLHttpRequest();
      xhr.onreadystatechange = function() {
        if (xhr.readyState !== 4) {
          return;
        }

        if (xhr.status === 200) {
          resolve(JSON.parse(xhr.responseText));
        } else {
          reject(new Error(xhr.statusText));
        }
      };

      xhr.open("GET", url);
      xhr.send();
    });

    return promise;
  };

  var thenResult = getJSONData("./data.json").then(function(json) {
    console.info("jsonData:",json);
  },function(error){
    console.error('出错了',error);
  });

  console.info("thenResult:",thenResult);

可以发现,then 方法返回了一个新的 Promise 实例,因此可采用链式写法。

getJSONData("./data.json").then(function(json) {
  return json.isOk
}).then(function(isOk) {
  console.info(isOK);
})

上面的代码指定了两个回调。第一个回调函数完成后,会将结果作为参数,传入第二个回调函数里面。

使用 then 方法链式调用减轻了回调地狱的问题,但仍然有大量重复样板代码。

catch

catch 方法用于指定发生错误时的回调函数。

  getJSONData("./data.json").then(function(json) {
    console.info("jsonData:",json);
  }).catch(function(error) {
    console.log('error');
  });

在上面的的代码中,如果异步操作抛出错误,状态就会变成 rejected ,就会调用 catch 方法指定的回调函数。如果 then 方法指定的回调函数,如果在运行中抛出错误,也会被 catch 方法捕获。

const promise = new Promise(function(resolve,rejected) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.info(error);
});

// Error: test

无效的错误

如果 Promise 状态已经变成 fulfilled ,再抛出错误无效。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

const promise = new Promise(function(resolve,rejected) {
  resolve('ok');
  throw new Error('test');
});
promise.then(function(value) {
  console.info(value);
}).catch(function(error) {
  console.info(error);
});
// ok

被吃掉的错误

如果没有使用 catch 方法指定错误处理程序,Promise 对象抛出的错误不会传递到外层代码。

const doAsyncThing = function() {
  return new Promise(function(resolve,rejected) {
    resolve(x);
  });
};

doAsyncThing().then(function(value) {
  console.info(value);
});

setTimeout(() => {console.info(222);},1000);

// Uncaught (in promise) ReferenceError: x is not defined
// 222

可发现,出现了语法错误,浏览器打印出了错误提示,但并没有退出程序或中止脚本执行,后面还是打印了 222。也就是说 Promise 内部的错误不会影响到外部的代码。

推荐写法

  1. 不要在 then 里面定义 reject 状态的回到函数,总是使用 catch
  // bad
  promise.then(
    function(data) {
      // success
    },
    function(err) {
      // error
    }
  );

  // good
  promise
    .then(function(data) {
      //cb
      // success
    })
    .catch(function(err) {
      // error
    });
  1. Promise 对象后面要有 catch 方法,这样可以处理内部发生的错误。

finally

finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

Promise.resolve(2).finally(() => {console.info('resolve finally');});
Promise.reject(3).finally(() => {console.info('reject finally');});
// resolve finally
// reject finally

Properties of the Promise Constructor

__proto__ 对应值的属性 constructor 中有几个方法:all、race、reject、resolve 。

all

在经典的编程术语中,有一种机制叫门(gate),要等待两个或更多并行/并发的任务都完成才能继续。完成的顺序并不重要,但必须都完成,门才能打开让流控制继续,在 Promise API 中,这种模式成为 allall 方法接收一个参数,是一个数组,数组中都是 Promise 实例。

var promise1 = new Promise((resolve,reject)=>{
  resolve("promise1 resolve");
});

var promise2 = new Promise((resolve,reject)=>{
  reject("promise2 reject");
});

Promise.all([promise1,promise2]).then(result => console.info(result));

34-promise-all

如果参数中某个 Promise 实例 A 定义了自己的 catch 方法,当 A 被 rejected,不会出触发 Promise.allcatch 方法。

var promise1 = new Promise((resolve,reject)=>{
  resolve("promise1 resolve");
}).catch(e => console.info('promise1 catch',e));

var promise2 = new Promise((resolve,reject)=>{
  throw new Error('promise2 error');
}).catch(e => console.info('promise2 catch',e));

Promise.all([promise1,promise2]).then(result => console.info(result)).catch(e => console.info('Promise.all catch',e));

34-promise-all-catch

race

race 方法接收一个参数,是一个数组。

const promiseRace = Promise.race([p1,p2,p3]);

只要 p1、p2、p3 之中有一个实例率先改变状态, promiseRace 的状态就会跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 promiseRace 的回调函数。

resolve

resolve 常用来创建一个已完成的 Promise

Promise resolve('p');
// 等价于
new Promise(resolve => resolve('p'))

resolve 方法的参数有以下情况:

  1. 不带任何参数
    直接返回一个 fulfilled 状态的 Promise 对象。

  2. 参数是一个 Promise 实例
    不做任何改动,直接返回。

  3. 参数是一个 thenable 对象
    thenable 对象只具有 then 方法的对象。resolve 方法会将这个对象转化为 Promise 对象,然后立即执行对象的 then 方法。

let thenable = {
  then: (resolve,reject) => {resolve(22);}
};
let promise = Promise.resolve(thenable);
promise.then((value) => {console.info(value);});

// 22

4.参数不具有 then 方法的对象,或者不是对象
这种情况,resolve 方法会返回一个新的 Promise 对象,状态为 fulfilled

const p = Promise.resolve(123);
p.then((value)=>{
  console.info(value);
});
// 123

由于数字 123 不属于异步操作,返回的 Promise 实例从一生成就是 fulfilled,所以回调函数立即执行。

reject

reject 方法也会返回一个新的 Promise 实例,该实例的状态为 rejectedreject 方法的参数,会原封不动地作为 reject 的理由,变成后续方法的参数。这一点与 resolve 方法不一致。

let thenable = {
  then: (resolve,reject) => {reject('error');}
};
Promise.reject(thenable).catch(e => {
  console.info(e === thenable);
});

//  true

问题

then 中数据传递

如果一个 Promise 实例后面有多个 then 方法调用,resolve 和 reject 的结果会一直传递到后面的 then 方法中吗?

const promiseResolve = new Promise(function(resolve, reject) {
  resolve("resolve value");
});

promiseResolve.then((data)=>{
  console.info('first then data:',data);
}).then((data)=>{
  console.info('second then data:',data);
}).then((data)=>{
  console.info('third then data:',data);
});
结果
// first then data: resolve value
// second then data: undefined
// third then data: undefined
const promiseReject = new Promise(function(resolve, reject) {
  reject("reject value");
});

promiseReject.then((data)=>{
  console.info('first then data:',data);
},(reject) => {
  console.info('first then reject:',reject);
}).then((data)=>{
  console.info('second then data:',data);
}, (reject) => {
  console.info('second then reject:',reject);

}).then((data)=>{
  console.info('third then data:',data);
}, (reject) => {
  console.info('third then reject:',reject);
}).catch((err) => {
  console.info('catch:',err);
})
结果
// first then reject: reject value
// second then data: undefined
// third then data: undefined

中断 then

当有多个 then 时,如果前面的 then 逻辑不走通就不执行后面 then 逻辑,如何实现?

const promiseBreak = new Promise(function(resolve, reject) {
  resolve('ok');
});

promiseBreak.then(() => {
  throw new Error('first then error');
}).then(() => {
  throw new Error('second then error');
}).catch((err) => {
  console.info('err',err);
});

// err Error: first then error

使用 return

在函数中使用 return 就会退出函数,在 Promise 中使用 return 结果会如何?

const promiseReturn = new Promise(function(resolve, reject) {
  return resolve("ok");
  resolve("yes");
});

promiseReturn.then((data)=>{
  console.info('first then data:',data);
}).then((data)=>{
  console.info('second then data:',data);
  return 'second data';
}).then((data)=>{
  console.info('third then data:',data);
});
结果
// first then data: ok
// second then data: undefined
// third then data: second data

参考资料

This was referenced Jun 10, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant