Promise/Generator/async 《异步编程的解决方案 》

Promise 基础认识

用 Promise 主要是换种写法而已 (通过callBack的思想把异步编程变成同步的一种写法) 英文是“承诺”的意思,表示其它手段无法改变。

Promise 的原理?(面试题)

一句话总结promise原理:resolve, reject两个回调函数控制promise内部状态,promise.then()根据状态执行不同的逻辑,实现异步。并返回已固定状态的新 promise,完成 then 链。

  1. Promise 的状态
    在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。    
    (1) promise 对象初始化状态为 pending。    
    (2) 当调用resolve(成功),会由pending => fulfilled。    
    (3) 当调用reject(失败),会由pending => rejected。  
    需要记住的是注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变。
  2. then 链的实现原理
    实例每一次 .then() 之后会重现返回一个状态已固定的 Promise ,所有可以继续使用新的 Promise 原型上的 .then() 方法。
  3. 哪里可以拿到失败状态的值
    .then() 的第二个回调函数的参数,或者 在 .catch() 的第一个回调函数的参数中
    (下文有详细说明)

Promise 的私有属性:

  1. [[PromiseState]]: pending 状态
    三种状态:pending(准备状态 默认)、fulfilled(成功状态)和 rejected(失败状态),只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是Promise这个名字的由来。

Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected,只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型),如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果,这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

  1. [[PromiseResult]]: undefined
    默认是未定义,当状态改为成功,存储的是成功的结果,反之是失败的原因

注意:只有 Promise 体内的代码是同步进行的。

如何改变状态

new Promise([executor])
  • executor(执行者)必须是一个函数,不传递函数则报错
  • executor函数中会有两个形参:resolve & reject
    • resolve 和 reject 的值都是个函数
  • new Promise 的时候,会把传递进来的executor函数立即执行
    • 函数中一般用来管理一个异步编程的代码
executor函数中的形参执行
  • resolve(value):把实例状态修改为fulfilled,值改为value
  • reject(reason):把实例状态修改为rejected,值改为reason
  • executor执行报错,也会把实例的状态改为rejected,值是报错的原因
    一但状态被修改为成功或者失败后,则不能再更改它的状态

实例.then(onfulfilled,onrejected)

  • 如果此时实例的状态是fulfilled,则把onfulfilled执行,并且把实例的值传递给形参value
  • 如果此时实例的状态是rejected,则把onrejected执行,并且把实例的值传递给形参reason
  • 如果此时还不知道实例的状态{状态还是pending},则先把onfulfilled、onrejected都存储起来,等到后期实例的状态被更改后,再拿出对应的方法执行!!
  • 同时一个实例可以多次调用then方法,当实例状态确定后,会把每一次基于then传递的onfulfilled或者onrejected都去执行「这种写法不常用」
  • then链机制:每一次执行then方法,都会返回一个全新的promise实例「p2/p3」
  • then的“穿透/顺延”机制:如果onfulfilled或者onrejected没有传递,则Promise内部会自动补全对应的方法,实现穿透的效果 “顺延到下一个then相同状态应该执行的方法上”

创建promise实例的办法

  • new Promise([executor])
    实例的状态和值由executor函数执行是否报错 & resolve/reject函数执行 来决定
  • .then(onfulfilled,onrejected)
    实例的状态和值和onfulfilled或者onrejected的方法执行有关系「不论哪个方法执行,都是按照以下规则来影响新返回实例的状态和值」
    @1 看方法执行是否会报错,如果报错了,则新实例的状态是rejected,值是报错的原因
    @2 如果没有报错,则再看方法执行的返回值,是否为一个新的promsie实例「@NEW」,如果是,则@NEW的状态和值,直接决定了.then返回的P2实例的状态和值
    @3 如果方法返回值不是新的实例,则P2的状态就是fulfilled,值就是函数的返回值
  • Promise.resolve([value]):直接创建一个状态是成功,值是[value]的实例
  • Promise.reject([reason]):直接创建一个状态是失败,值是[reason]的实例
  • Promise.all([promise1,promise2,...]):监听传递进来的数组中,每一个promise实例的状态,当所有实状态是成功的时候,整体返回实例状态也是成功,值是一个数组,包含所有实例的结果;如果其中有一个实例是失的,整体返回的实例就是失败的,值是失败的那个promise的值!!
  • Promise.race([p1, p2, ...])
  • ...

一. new Promise((resolve,reject)=>{...})

let str = 10;
// p是 <promise> 对象 可以进行链式调用,链式调用的返回值是 this (这里指向p,所以可以一直使用 .then( ) 的方法)
let p = new Promise(function (resolve, reject) {
  setTimeout(function () {
    str = 20;
    resolve(str)
  }, 100);
});

p.then((str) => { //这里拿到的 str 是 new 里面的 resolve(str) 里的 str 相当于这里的函数,在上面被调用,并传实参 str
  console.log(str) //20
  return 5; // 虽然写了 return,但返回的依然是一个新的 promise 对象,而这里的 return 是给下一个.then 传参。
}).then((a) => {
  console.log(a) //5
});

p.then((xxx) => { //这里拿到的 xxx 是 new 里面的 resolve(str) 里的 str 相当于这里的函数,在上面被调用,并传实参 xxx
  console.log(xxx) //20
})
// 先打印 11行的 20, 再输出 18行的 20, 最后输出 14行的 5, 因为 11行 和 18行, 都是最先被执行的 promise, 而14行是一个新的 promise,属于新的微任务,进入异步队列

二. 哪里可以拿到 reject 中的内容

1. reject后的东西,一定会进入then中的第二个回调,如果then中没有写第二个回调,则进入catch,没有 then 也会进入 catch

var p1=new Promise((resolve,reject) => {
   console.log('没有resolve')
   reject('失败了') // 或者 throw new Error('手动返回错误')
});

/*
p1的状态是成功fulfilled还是失败rejected,决定了本次.then 
是执行onfulfilled(第一个回调函数)还是onrejected(第二个回调函数)
*/
p1.then(data =>{
  // 因为p1状态是rejected,所有本次.then的onfulfilled(第一个回调函数)不会执行
  console.log('data::',data);
}, err=> {
  console.log('err::',err); // 本次.then的onrejected(第二个回调函数)被执行并拿到失败的原因
  return 'err2';
}).then(data =>{
/*
1.上一个.then被执行的那个回调函数不管return了什么,都会返回一个新的Promise,而这个新的Promise
的状态,又决定了本次.then是执行onfulfilled(第一个回调函数)还是onrejected(第二个回调函数)

2.如果上一个.then被执行的那个回调函数没有return一个失败状态的Promise或者代码报错,
就执行本次.then的第一个回调函数,值是return的内容。
*/
  console.log('data2::',data); 
  throw new Error('手动返回错误');
  // return Promise.reject("给catch的值");
}, err=> {
//本次的onrejected不会执行,因为上一个.then被执行的那个回调函数返回的新Promise状态是fulfilled(成功状态)
  console.log('err2::',err)
}).catch(
/*
如果.then的onfulfilled或onrejected方法执行了,并且在被执行的方法中return一个失败状态的Promise或者代码报错,
那么.catch就会执行其onrejected,值是'手动返回错误'或"给catch的值"。
*/ 
  res => {
  console.log('catch data::', res)
});
/*
 ————————运行结果————————
  没有resolve
  err:: 失败了
  data2:: err2
  catch data:: Error: 手动返回错误
*/

.then中没有第二个回调的情况

var p1=new Promise((resolve,reject) => {
  console.log('没有resolve')
  // throw new Error('手动返回错误')
  reject('失败了')
})
 
p1.then(data =>{ // 不会被执行
  console.log('data::',data);
}).then(value => { // 不会被执行
  console.log('请求成功:', value);
  throw 'xxx';
 }).catch(res => { // 失败状态顺延到.catch的onrejected函数,并最终执行
  console.log('catch data::', res)
})
/*
  ————————运行结果————————
  没有resolve
  catch data:: 失败了
  同理:resolve的东西,一定会进入then的第一个回调,肯定不会进入catch
*/

p.then(null, onrejected) 等价于 p.catch(onrejected)

2. 用 try{}catch(error){} **注意写法,需要使用 async await **

通常 try catch 是无法捕捉异步报错的,所以用它直接包裹 promise 是无法捕捉到错误信息的。

    let fn = async () => {
      try {
        let p = await new Promise((res, rej) => {
          rej('失败了')
        })
      } catch (err) {
        console.log("try catch 捕获到 promise 的失败信息:", err);
      };
    };
    fn();

三. Promise.all ( [ p1 , p2 , ...... ] )

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。(all会将传入的数组中的所有promise全部决议以后,将决议值以数组的形式传入到观察回调中,任何一个promise决议为拒绝,那么就会调用拒绝回调。)

let p1 = new Promise((resolve, reject) => {
  resolve('成功了')
});

let p2 = new Promise((resolve, reject) => {
  resolve('success')
});
 
let p3 = new Promise((resolve, reject) => {
  reject('失败')
});

Promise.all([p1, p2]).then((result) => {
  console.log(result); // ['成功了', 'success']
}).catch((error) => {
  console.log(error);
});

Promise.all([p1,p3,p2]).then((result) => {
  console.log(result);
}).catch((error) => {
  console.log(error); // 失败了,打出 '失败'
});

Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。

代码模拟:

let wake = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`${time / 1000}秒后醒来`)
    }, time)
  })
}

let p1 = wake(3000)
let p2 = wake(2000)

Promise.all([p1, p2]).then((result) => {
  console.log(result)       // [ '3秒后醒来', '2秒后醒来' ]
}).catch((error) => {
  console.log(error)
})

需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。
这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。

四. Promise.race的使用

顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  },1000)
})

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('failed')
  }, 500)
})

Promise.race([p1, p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)  // 打开的是 'failed'
})

五. Promise是如何捕获异常的?与传统的try/catch相比有什么优势?

传统的 try / catch 捕获异常方式是无法捕获异步的异常的。

而对于 Promise 对象来说,构造 Promise 实例时的代码如果出错,则会被认为是一个拒绝的决议,并会向观察回调中传递异常信息。所以即使是一个异步的请求,Promise 也是可以捕获异常的。此外,Promise 还可以通过 catch 回调来捕获回调中的异常。

六.如果向Promise.all()和Promise.race()传递空数组,运行结果会有什么不同?

all会立即决议,决议结果是fullfilled,值是undefined

race会永远都不决议,程序卡住……

七. 扩展一个 Promise.finally()

扩展一个 Promise.finally(),finally方法用于指定不管Promise对象最后状态如何,都会执行的操作.

它与done方法的最大区别在于,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

//添加finally方法
Promise.prototype.finally=function (callback) {
   var p=this.constructor;
   return this.then(//只要是promise对象就可以调用then方法
     value => p.resolve(callback()).then(() => value),
     reason => p.resolve(callback()).then(() => {throw reason})
   );
}

对finally方法的理解:

  1. p.resolve(callback())这句函数callback已经执行
  2. finally方法return的是一个promise对象,所以还可以继续链式调用其他方法
  3. 对于Promise.resolve方法:Promise.resolve('foo') 等价于 new Promise(resolve => resolve('foo'));所以可以通过then方法的回调函数 接受 实例对象返回的参数。比如:Promise.resolve(function(){console.log(2);}).then(function(cb){cb()})
  4. p.resolve(callback()).then(() => value)调用then的目的是给promise实例即this添加成功和失败的回调函数

八. 扩展一个 Promise.done()

Promise 对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

  Promise.prototype.done = function (onFulfilled, onRejected) {
    this
      .then(onFulfilled, onRejected)
      .catch(function (reason) {
        // 抛出一个全局错误
        setTimeout(() => {
          throw reason
        }, 0)
      })
  }

从上面代码可见,done方法的使用,可以像then方法那样用,提供fulfilled和rejected状态的回调函数,也可以不提供任何参数。但不管怎样,done都会捕捉到任何可能出现的错误,并向全局抛出。

基于 Promise 之后又有 Generator ,和它门的语法糖:async

Generator

Generator 函数是协程在ES6的实现,最大的特点就是可以交出函数的执行权(即暂停执行),交给 yield 后面的 语句,等此语句执行完,再执行下面的代码。

Generator 有两个区分于普通函数的部分:

  • 一是在 function 后面,函数名之前有个 * ;
  • 函数内部有 yield 表达式。

协程:
  第一步:协程 A 开始执行。
  第二步:协程 A 执行到一半,进入暂停,执行权转移到协程 B。
  第三步:(一段时间后)协程 B 交还执行权
  第四步:协程 A 恢复执行。

也就是说 Promise 和 Generator 一个明显不同的特点是,前者只在Promise体内进行异步代码的同步执行,(不会阻断主任务)

而后者是阻断(主任务)的执行,等到异步的代码执行完毕之后再执行被阻断的(主任务)代码。

例子一:

function* func(){
  console.log("one");
  yield '1';
  console.log("two");
  yield '2'; 
  console.log("three");
  return '3';
};
let f = func();
f.next();
// one
// {value: "1", done: false}
f.next();
// two
// {value: "2", done: false}
f.next();
// three
// {value: "3", done: true}
f.next();
// {value: undefined, done: true}

例子二:
一般情况下,next 方法不传入参数的时候,yield 表达式的返回值是 undefined 。当 next 传入参数的时候,该参数会作为上一步yield的返回值。

function* sendParameter() {
  console.log("start");
  var x = yield '2';
  console.log("one:" + x);
  var y = yield '3';
  console.log("two:" + y);
};
// next()不传参
var sendp1 = sendParameter();
sendp1.next();
// start
// {value: "2", done: false}
sendp1.next();
// one:undefined
// {value: "3", done: false}
sendp1.next();
// two:undefined
// {value: undefined, done: true}

// next()传参
var sendp2 = sendParameter();
sendp2.next();
// start
// {value: "2", done: false}
sendp2.next(10);
// one:10
// {value: "3", done: false}
sendp2.next(20);
// two:20
// {value: undefined, done: true}

例子三:练习一下

function* gen(x){
//这个函数体内任何地方console.log(y)都是undefined
 var y = yield x + 2 ;
 var z = yield x + y ;
};
let y = gen(5);
// y.next() => {value: 7, done: false},value的值是第一个yield后面的 x + 2
y.next(y.next().value) // next中可以传入数据(传入的数据为上一次yield表达式的返回值(就是这里的y)),为了下一步操作的运算。

async(**现在最常用)

async & await:是promise+generator的语法糖,简化promsie的代码编写
async:用来修饰函数的,让函数执行的返回值是一个promsie实例「不常用的」
+ 函数执行报错,返回的实例状态是rejected,值是报错原因
+ 如果没报错,看函数执行是否自己返回了promise实例,如果返回了,则以自己返回的为主
+ 如果没返回,则默认返回一个状态是fulfilled、值是返回值的实例

真实项目中,使用async修饰函数,一般都是因为函数中想要使用await「因为:函数中想使用await,则当前函数必须基于async修饰」

await:等待promise处理的结果;当实例状态是成功,把实例的值赋值给value,同时再执行当前上下文await下面的代码;如果实例的状态是失败的,则下面代码不会去执行!!let value = await promise;

await下面的代码何时执行?

  • await 后面如果是微任务(一个promise),它下面的代码就会等待这个微任务执行完,紧跟着执行。
  • await 后面如果是主任务(比如一个函数或一个表达式),其下面的代码就会等待这一批主任务执行完,最后执行。
function fn2(){
  return new Promise(function(resolve){
    setTimeout(function(){
      resolve(50);
    }, 1000);
  })
};

async function fn(){
  let value = await fn2();
  console.log(value);
};

fn();// 调用后,输出 50 。要 等待 fn2执行完(将异步转成同步的感觉)
posted @ 2018-12-10 13:08  真的想不出来  阅读(875)  评论(0编辑  收藏  举报