Promise

Promise是什么?

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。

由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现:

$.ajax({
   url:'/xxx',
   success:()=>{},
   error: ()=>{}
})

这种方法可以清楚的让读代码的人明白那一部分是Ajax请求成功的回调函数和失败的回调函数。但是问题来了,当一次请求需要连续请求多个接口时,这段代码仿佛进入了一团乱麻中:

// 第一次 
$.ajax({
     url:'/xxx',
     success:()=>{
         // 第二次
         $.ajax({
             url:'/xxx',
             success:()=>{
               // 第三次
               $.ajax({
                  url:'/xxx',
                  success:()=>{
                   // 可能还会有
                  },
                  error: ()=>{}
                })
             },
             error: ()=>{}
        })
     },
     error: ()=>{}
})

再看一个例子:
每隔1秒钟输出递增的数字,如( 1, 2, 3 等 )

setTimeout(function () {
    console.log(1);
    setTimeout(function () {
        console.log(2);
        setTimeout(function () {
            console.log(3);
        }, 1000);
    }, 1000);
}, 1000);

这还是3层,如果4层,5层。。。。,理解这些代码会很简单,但是如果换成具体的业务逻辑需求的时候,这就产生了回调地狱,即代码层层嵌套,环环相扣,很明显,逻辑稍微复杂一些,这样的程序就会变得难以维护。

那es6的Promise它可以帮你非常灵活的调整。
它不是新的语法功能,而是一种新的写法,允许将回调函数的横向加载,改成纵向加载。

Promise是一种对异步操作的封装,主流的规范是Promise/A+。
Promise可以使得异步代码层次清晰,便于理解,且更加容易维护。

Promise提供一个then,来为异步提供回调函数:

$.ajax({
    url:'/xxx',
}).then( ()=>{
   // 成功的回调
}, ()=>{
  // 失败的回调 
})
function next( n ){
    return new Promise( function( resolve, reject ){
        setTimeout( function(){
            resolve( n );
        }, 1000 );
    } );
}
next( 1 ).then( function( res ){
    console.log( res );
    return next( 2 );
} ).then( function( res ){
    console.log( res );
    return next( 3 );
} ).then( function( res ){
    console.log( res );
} )

它的先进之处是,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。

Promise的用法
下面让我们看看Promise是怎么使用的。首先,Promise是一个对象,因此,我们使用new的方式创建一个。然后给它传一个函数作为参数,这个函数呢也有两个参数,一个叫resolve(决定),一个叫reject(拒绝),这两个参数也是函数。紧接着,我们使用then里调用这个Promise

const fn = new Promise(function (resolve, reject) {
  setTimeout(()=>{
    let num = Math.ceil(Math.random() * 10) // 假设num为7
    if (num > 5) {
      resolve(num) //返回7
    } else {
      reject(num)
    }
  },2000)
})
fn.then((res)=>{
  console.log(res) // 7
},(err)=>{
  console.log(err)
})

这就是最简单的Promise的使用。假设2s后生成随机数7,因此resolve回调函数运行,then走第一个函数,console.log(7)。假设2s后生成的随机数是3,此时reject回调函数运行,then走第二个函数,console.log(3)。

上面说了Promise的先进之处在于可以再then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作:

fn = new Promise(function (resolve, reject) {
  let num = Math.ceil(Math.random() * 10)
  if (num > 5) {
    resolve(num)
  } else {
    reject(num)
  }
})
// 第一次回调
fn.then((res)=>{
  console.log(`res==>${res}`)
  return new Promise((resolve,reject)=>{
    if(2*res>15){
      resolve(2*res)
    }else{
      reject(2*res)
    }
  })
},(err)=>{
  console.log(`err==>${err}`)
}).then((res)=>{ // 第二次回调
  console.log(res)
},(err)=>{
  console.log(`err==>${err}`)
})

这就可以代替了上面类似es5时代jQuery的success的嵌套式的回调地狱的产生,让代码清爽了许多。这里的resolve就相当于以前的success。

Promise的原理
在Promise的内部,有一个状态管理器的存在,有三种状态:pending(进行中)、fulfilled(已完成)、rejected(已拒绝)。
1.Promise对象初始化状态为pending
2.当调用resolve(成功),会由pending => fulfilled
3.当调用reject(失败),会由pending => rejected

因此,看上面的代码中的resolve(num)其实是将promise的状态由pengding改为fulfilled,然后向then的成功回调函数传值,reject反之。但是需要记住的是注意promise状态只能由pending => fulfilled/rejected, 一旦修改就不能再变。

当状态为fulfilled(rejected反之)时,then的成功回调函数会被调用,并接受上面传来的num,进而进行操作。promise.then方法每次调用,都返回一个新的promise对象,所以可以链式写法(无论resolve还是reject都是这样)

Promise的几种方法
我们先用console.dir(Promise)打印出来看看:

从上图可以看出主要有以下几个方法:resolve,reject,then,catch,all,race。下面分别来说说这几个方法

resolve、reject
1.resolve:该方法可以使Promise对象的状态改变成成功,同时传递一个参数用于后续成功后的操作
2.reject: 该方法则是将Promise对象的状态改变为失败,同时将错误的信息传递到后续错误处理的操作。

Promise.resolve返回一个fulfilled状态的promise对象,Promise.reject返回一个rejected状态的promise对象。

Promise.resolve('hello').then(function(value){
    console.log(value);
});

Promise.resolve('hello');
// 相当于
const promise = new Promise(resolve => {
   resolve('hello');
});

// reject反之

then
then:所有的Promise对象实例都有一个then方法,它是用来跟这个Promise进行交互的,then方法主要传入两个方法作为参数,一个resolve函数,一个 reject函数,链式调用,上一个Promise对象变为resolved的时候,调用then中的Resolve方法,否则调用Reject方法。

then方法用于注册当状态变为fulfilled或者reject时的回调函数:

// onFulfilled 是用来接收promise成功的值
// onRejected 是用来接收promise失败的原因
promise.then(onFulfilled, onRejected);

需要注意的地方是then方法是异步执行的。

// resolve(成功) onFulfilled会被调用
const promise = new Promise((resolve, reject) => {
   resolve('fulfilled'); // 状态由 pending => fulfilled
});
promise.then(result => { // onFulfilled
    console.log(result); // 'fulfilled' 
}, reason => { // onRejected 不会被调用
})

// reject(失败) onRejected会被调用
const promise = new Promise((resolve, reject) => {
   reject('rejected'); // 状态由 pending => rejected
});
promise.then(result => { // onFulfilled 不会被调用
}, reason => { // onRejected 
    console.log(rejected); // 'rejected'
})

catch
catch在链式写法中可以捕获前面then中发送的异常。

catch: 该方法是then(onFulfilled,onRejected)方法当中onRejected函数的一个简单的写法,可以理解为promise.then(undefined, onRejected),但是用来捕获异常时,用catch更多便于理解。

fn = new Promise(function (resolve, reject) {
  let num = Math.ceil(Math.random() * 10)
  if (num > 5) {
    resolve(num)
  } else {
    reject(num)
  }
})
fn.then((res)=>{
  console.log(res)
}).catch((err)=>{
  console.log(`err==>${err}`)
})

其实,catch相当于then(null,onRejected),前者只是后者的语法糖而已。

all
从字面意思上理解,可能为一个状态全部怎么样的意思,让看下它的用法就可以明白这个静态方法:

var   p1 = Promise.resolve(1),
      p2 = Promise.reject(2),
      p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((res)=>{
    //then方法不会被执行
    console.log(results);
}).catch((err)=>{
    //catch方法将会被执行,输出结果为:2
    console.log(err);
});

all: 该方法可以接收一个元素为Promise对象的数组作为参数,当这个数组里面所有的Promise对象都变为resolve时,该方法才会返回。就是全部都执行完了才接着往下执行。

当这几个作为参数的函数的返回状态为fulfilled时,至于输出的时间就要看谁跑的慢了:

let p1 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('1s') //1s后输出
    resolve(1)
  },1000)
})
let p10 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('10s') //10s后输出
    resolve(10)
  },10000)
})
let p5 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('5s') //5s后输出
    resolve(5)
  },5000)
})
Promise.all([p1, p10, p5]).then((res)=>{
    console.log(res); // 最后输出
})

这段代码运行时,根据看谁跑的慢的原则,则会在10s之后输出[1,10,5]。

race
promise.race()方法也可以处理一个promise实例数组但它和promise.all()不同,从字面意思上理解就是竞速,那么理解起来上就简单多了,也就是说在数组中的元素实例那个率先改变状态,就向下传递谁的状态和异步结果。但是,其余的还是会继续进行的。

race: 竞速,类似all方法,它同样接收一个数组,不同的是只要该数组中的Promise对象的状态发生变化(无论是resolve还是reject)该方法都会返回。就是只要某一个执行完了就接着往下执行。

let p1 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('1s') //1s后输出
    resolve(1)
  },1000)
})
let p10 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('10s') //10s后输出
    resolve(10) //不传递
  },10000)
})
let p5 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('5s') //5s后输出
    resolve(5) //不传递
  },5000)
})
Promise.race([p1, p10, p5]).then((res)=>{
    console.log(res); // 最后输出
})

运行结果:

我们可以根据race这个属性做超时的操作:

//请求某个图片资源
let requestImg = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
    });
//延时函数,用于给请求计时
let timeOut = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('图片请求超时');
        }, 5000);
    });

Promise.race([requestImg, timeout]).then((res)=>{
    console.log(res);
}).catch((err)=>{
    console.log(err);
});

Promise相关的面试题

const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
});
promise.then(() => {
    console.log(3);
});
console.log(4);
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
    reject('error')
  }, 1000)
})
promise.then((res)=>{
  console.log(res)
},(err)=>{
  console.log(err)
})
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)
setTimeout(()=>{
  console.log('setTimeout')
})
let p1 = new Promise((resolve)=>{
  console.log('Promise1')
  resolve('Promise2')
})
p1.then((res)=>{
  console.log(res)
})
console.log(1)
Promise.resolve(1)
    .then((res) => {
        console.log(res);
        return 2;
    })
    .catch((err) => {
        return 3;
    })
    .then((res) => {
        console.log(res);
    });
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('开始');
resolve('success');
}, 5000);
});
 
const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});
 
promise.then((res) => {
console.log(res, Date.now() - start);
});
let p1 = new Promise((resolve,reject)=>{
  let num = 6
  if(num<5){
    console.log('resolve1')
    resolve(num)
  }else{
    console.log('reject1')
    reject(num)
  }
})
p1.then((res)=>{
  console.log('resolve2')
  console.log(res)
},(rej)=>{
  console.log('reject2')
  let p2 = new Promise((resolve,reject)=>{
    if(rej*2>10){
      console.log('resolve3')
      resolve(rej*2)
    }else{
      console.log('reject3')
      reject(rej*2)
    }
  })
  return p2
}).then((res)=>{
  console.log('resolve4')
  console.log(res)
},(rej)=>{
  console.log('reject4')
  console.log(rej)
})

总结

首先,Promise是一个对象,如同其字面意思一样,代表了未来某时间才会知道结果的时间,不受外界因素的印象。Promise一旦触发,其状态只能变为fulfilled或者rejected,并且已经改变不可逆转。Promise的构造函数接受一个函数作为参数,该参数函数的两个参数分别为resolve和reject,其作用分别是将Promise的状态由pending转化为fulfilled或者rejected,并且将成功或者失败的返回值传递出去。then有两个函数作为Promise状态改变时的回调函数,当Promise状态改变时接受传递来的参数并调用相应的函数。then中的回调的过程为异步操作。catch方法是对.then(null,rejectFn)的封装(语法糖),用于指定发生错误时的回掉函数。一般来说,建议不要再then中定义rejected状态的回调函数,应该使用catch方法代替。all和race都是竞速函数,all结束的时间取决于最慢的那个,其作为参数的Promise函数一旦有一个状态为rejected,则总的Promise的状态就为rejected;而race结束的时间取决于最快的那个,一旦最快的那个Promise状态发生改变,那个其总的Promise的状态就变成相应的状态,其余的参数Promise还是会继续进行的。

扩展
在es7时代,也出现了await/async的异步方案
await/async来说是基于promise的,
async-await是promise和generator的语法糖。只是为了让我们书写代码时更加流畅,当然也增强了代码的可读性。简单来说:async-await 是建立在 promise机制之上的,并不能取代其地位。

老朋友Ajax

// 获取产品数据
ajax('products.json', (products) => {
    console.log('AJAX/products >>>', JSON.parse(products));
 
    // 获取用户数据
    ajax('users.json', (users) => {
        console.log('AJAX/users >>>', JSON.parse(users));
 
        // 获取评论数据
        ajax('products.json', (comments) => {
            console.log('AJAX/comments >>>', JSON.parse(comments));
        });
    });
});

不算新的朋友promise

// Promise
// 封装 Ajax,返回一个 Promise
function requestP(url) {
    return new Promise(function(resolve, reject) {
        ajax(url, (response) => {
            resolve(JSON.parse(response));
        });
    });
}
 
// 获取产品数据
requestP('products.json').then((products) => {
    console.log('Promises/products >>>', products);
    // 获取用户数据
    return requestP('users.json');
}).then((users) => {
    console.log('Promises/users >>>', users);
    // 获取评论数据
    return requestP('comments.json');
}).then((comments) => {
    console.log('Promises/comments >>>', comments);
});

碉堡的朋友 await/async

// 封装 Ajax,返回一个 Promise
function requestP(url) {
    return new Promise(function(resolve, reject) {
        ajax(url, (response) => {
            resolve(JSON.parse(response));
        });
    });
}
 
(async () => {
    // 获取产品数据
    let data = await requestP('products.json');
 
     // 获取用户数据
    let users = await requestP('users.json');
 
     // 获取评论数据
    let products = await requestP('comments.json');
 
    console.log('ES7 Async/products >>>', products);
    console.log('ES7 Async/users >>>', users);
    console.log('ES7 Async/comments >>>', comments);
}());

与Fetch Api相结合使用

(async () => {
// Async/await using the fetch API
    try {
 
         // 获取产品数据
        let products = await fetch('products.json');
 
        // Parsing products
        let parsedProducts = await products.json();
 
        // 获取用户数据
        let users = await fetch('users.json');
 
        // Parsing users
        let parsedUsers = await users.json();
 
        // 获取评论数据
        let comments = await fetch('comments.json');
 
        // Parsing comments
        let parsedComments = await comments.json();
 
 
        console.log('ES7 Async+fetch/products >>>', parsedProducts);
        console.log('ES7 Async+fetch/users >>>', parsedUsers);
        console.log('ES7 Async+fetch/comments >>>', parsedComments);
 
 
    } catch (error) {
        console.log(error);
    }
}());

再次结合Fetch

(async () => {
    let parallelDataFetch = await* [
        (await fetch('products.json')).json(),
        (await fetch('users.json')).json(),
        (await fetch('comments.json')).json()
    ];
    console.log('Async parallel+fetch >>>', parallelDataFetch);
}());

使用 await/async 用同步的思维去解决异步的代码

详细了解可参考:
http://www.ruanyifeng.com/blog/2015/05/async.html

posted @ 2022-11-25 15:16  SultanST  阅读(14)  评论(0编辑  收藏  举报