七分钟带你看懂Promise(含源码)

Promise出现的背景

本文已在掘金、博客园、CSDN多平台发布,转载需署名

  • 同步:需要等待一件事情完成后才能做下一件事。
  • 异步:一件事情在等待执行的过程中,我们可以继续做下一件事,不需要等待上一件事完成。
    常见的异步场景:定时器 setTimeout事件绑定回调函数数据请求 Ajax / 请求图片…

众所周知,js 执行的时候,一次只能执行一个任务,它会阻塞其他任务。由于这个缺陷导致js的所有网络操作浏览器事件,都必须是异步执行。而异步执行可以使用回调函数执行。

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

image.png

Promise的规范

Promise必须遵循一组特定的规则:

  • Promise 或者 "thenable" 必须是一个提供符合标准的 .then() 方法的对象
  • 一个 pendding 状态的 Promise 会转换为 fulfilled 状态或 rejected 状态
  • Promise 对象一旦变为 fulfilled 状态或 rejected 状态,则状态永远不会再次发生改变
  • 一旦 Promise 执行完成后,一定会产生一个值(这个值可能是 undefined)。并且当前值永远不会被更改。

这里的值修改不了
这里的值无法被修改

Promise的基本使用

ES6规定,Promise对象是一个构造函数,用来生成Promise实例.

console.dir(Promise)

image.png

我们可以发现,Promise是一个构造函数,有allrejectresolve这几个常用的方法,原型上有thencatch等同样很眼熟的方法。因此用Promise构造函数 new出来的实例对象就会拥有thencatch方法。


在了解promise的基本流程前,先要知道 promise的一些基本属性

1、promise 的状态⭐

promise 的状态是 promise实例对象中的一个属性 [PromiseState]

  • pending进行中
  • resolved / fulfilled 成功
  • rejected失败

状态只能由 Pending 变为 Fulfilled 或由 Pending 变为 Rejected ,且状态改变之后不会在发生变化,会一直保持这个状态。

(源码里通过判断PromiseState状态和PromiseRusult的值来决定then方法中回调函数和异步任务的执行顺序)


2、promise 对象的值

实例对象中的另一个属性 [PromiseResult]

保存着异步任务 [成功/失败] 的结果

  • resolve
  • reject

3、promise 的基本流程

image.png

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

var promise = new Promise(function(resolve,reject){
  if(/* 异步操作成功 */){
    resolve(value);
  }else{
    reject(error);
  }
});

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;

reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

resolve的使用

例如:

var p = new Promise(function(resolve, reject){
    // 模拟异步操作
    setTimeout(function(){
        console.log('异步任务执行完成了');
        resolve('ok');
    }, 3000);
});

在上面的代码中,我们用定时器setTimeout模拟了一个异步操作,3秒后,输出“异步任务执行完成了”,并且调用resolve方法。

有没有观察到p这个实例对象并没有执行p()去调用它,传入的函数却已经执行了呢?

注意Promise(fn):传递的fn函数是一个同步函数,会立刻执行,通过new关键字去调用Promise构造函数的时候就会自动立即调用fn函数(详情看源码)

因此我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:

function testAsync(){
    var p = new Promise(function(resolve, reject){
    // 模拟异步操作
    setTimeout(function(){
            console.log('异步任务执行完成了');
            resolve('ok');
        }, 3000);
    });
    return p;            
}
testAsync()

3秒后,输出“异步任务执行完成了”,并且在testAsync函数最后,会return出一个Promise对象,还记得Promise对象上有thencatch方法吧?

所以我们可以:

testAsync().then(function(res){
    //可以直接拿到返回的数据res
    console.log(res);
    //后面可以用传过来的数据res做些其他操作
    //......
});

显然,Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then链式调用来进行回调操作。避免了回调地狱的问题

回调地狱:

image.png

then链式调用:

functionsendRequest(url) {
    // url 为请求路径
    returnnewPromise((resolve, reject) => {
    // 定时器模拟网络请求
    setTimeout(() => {
      if (url.includes('user')) {
        resolve(url)     
       } 
       else {
        reject('请求错误')  
      }
    }, 1000);  
  })
}

  // 因为函数的返回值是Promise对象所以可采用链式调用
  sendRequest("/user").then(res=> {
    return sendRequest(`userlist ${res}`)
  }).then(res=> {
    returnsendRequest(`userdetail ${res}`)
  }).then(res=> {
    console.log(res)
  })

reject的使用

事实上,我们前面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把romise的状态从pending置为rejected,这样我们在then中就能捕获到,然后执行“失败”情况的回调。看下面的代码:

function getNumber(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            var num = Math.ceil(Math.random()*100); //生成1-100的随机数
            if(num<=50){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
        }, 2000);
    });
    return p;            
}
 
getNumber()
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason, data){
        console.log('rejected');
        console.log(reason);
    }
);

getNumber函数用来异步获取一个数字,2秒后执行完成,如果数字小于等于50,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。

运行getNumber并且在then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:

image.png
image.png

catch的使用

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
catch相当于then的第二个参数,用来指定reject的回调,用法如下:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
    console.log(somedata); //此处的somedata未定义
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

resolve的回调中,我们console.log(somedata);somedata这个变量是没有被定义的。如果我们不用Promise,代码运行到这里就直接在控制台报错了,不往下运行了。但是在这里,会得到这样的结果:

resolved
4
rejected
ReferenceError:somedata is not defined(...)

也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。

finally的使用

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的Promise状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

all的使用

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

Promiseall方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用定义好的testAsync1、testAsync2、testAsync3这三个函数,看下面的例子:

function testAsync1(){
    var p = new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log('1异步任务执行完成了');
            resolve('数据1');
        }, 1000);
    });
    return p;            
}
function testAsync2(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('2异步任务执行完成了');
            resolve('数据2');
        }, 2000);
    });
    return p;            
}
function tsetAsync3(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('3异步任务执行完成了');
            resolve('数据3');
        }, 2000);
    });
    return p;            
}
 
Promise
.all([testAsync1(), testAsync2(), testAsync3()])
.then(function(results){
    console.log(results);
});

Promise.all来执行,all接收一个数组参数,里面的值最终都是返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。所以上面代码的输出结果就是:

image.png


promise 实践练习-AJAX请求

// 原生
const btn = document.querySelector('#btn');
btn.addEventListener("click", () => {
    // 创建对象
    const xhr = new XMLHttpRequest();
    // 初始化
    xhr.open('GET', 'https://api.apiopen.top/getTime');
    // 发送
    xhr.send();
    // 处理响应结果
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            // 判断响应状态码
            if (xhr.status >= 200 && xhr.status < 300) {
                // 控制台输出响应体
                console.log(xhr.response);
            } else {
                // 控制台输出状态码
                console.log(xhr.status);
            }
        }
    }
})

// promise 封装
btn.addEventListener("click", () => {
    // 创建 Promise
    const promise = new Promise((resolve, reject) => {
        // 创建对象
        const xhr = new XMLHttpRequest();
        // 初始化
        xhr.open('GET', 'https://api.apiopen.top/getTime');
        // 发送
        xhr.send();
        // 处理响应结果
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                // 判断响应状态码
                if (xhr.status >= 200 && xhr.status < 300) {
                    // 控制台输出响应体
                    resolve(xhr.response);
                } else {
                    // 控制台输出状态码
                    reject(xhr.status);
                }
            }
        }
    })

    promise.then(result => {
        console.log(result);
    }).catch(err => {
        console.log(err);
    })
})


手写Promise源码

1、定义整体结构

创建两个文件 index.htmlpromise.js

promise.js写最基本的 promise 结构

function Promise(executor) {
   // new调用的时候就会立刻执行同步函数executor
}

// 添加 then 方法
Promise.prototype.then = function(onResolved, onRejected) {
   
}

index.html里引入 我们刚写的 promise.js

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- 引入 promise -->
    <script src="./promise.js"></script>
</head>
<body>
    <script>
        let p = new Promise((resolve, reject) => {
            resolve('Ok');
        })

        p.then(res => {
            console.log(res);
        }, reason => {
            console.log(reason);
        })
    </script>
</body>
</html>

2、封装 resolve 和 reject 结构

promise.js 里的 代码

function Promise(executor) {

    // resolve 函数
    function resolve(data) {

    }

    // reject 函数
    function reject(data) {
        
    }

    // 同步调用 [执行器函数]
    executor(resolve, reject);

}

3、resolve 和 reject 代码的实现

function Promise(executor) {

    // 添加属性
    this.PromiseState = 'pending';
    this.PromiseResult = null;
    // 保存实例对象的 this 的值
    const that = this;
    // resolve 函数
    function resolve(data) {
        // 1. 修改对象的状态 (promiseState)
        that.PromiseState = 'fulfilled';
        // 2. 设置对象结果值 (promiseResult)
        that.PromiseResult = data;
    }

    // reject 函数
    function reject(data) {
        // 1. 修改对象的状态 (promiseState)
        that.PromiseState = 'rejected';
        // 2. 设置对象结果值 (promiseResult)
        that.PromiseResult = data;
    }

    // 同步调用 [执行器函数]
    executor(resolve, reject);

}

// 添加 then 方法
Promise.prototype.then = function(onResolved, onRejected) {

}

4、throw 抛出异常改变状态

try {
    // 同步调用 [执行器函数]
    executor(resolve, reject);
} catch (error) {
    // 修改 promise 对象状态为  失败
    reject(error)
}

5、promise 对象状态只能修改一次

// resolve 函数
function resolve(data) {
    // 来个if判断一下就行
    if (that.PromiseState !== 'pending') return;
    // 1. 修改对象的状态 (promiseState)
    that.PromiseState = 'fulfilled';
    // 2. 设置对象结果值 (promiseResult)
    that.PromiseResult = data;
}

// reject 函数
function reject(data) {
    // 判断状态
    if (that.PromiseState !== 'pending') return;
    // 1. 修改对象的状态 (promiseState)
    that.PromiseState = 'rejected';
    // 2. 设置对象结果值 (promiseResult)
    that.PromiseResult = data;
}

6、then方法执行回调

// 添加 then 方法
Promise.prototype.then = function(onResolved, onRejected) {
    // 调用回调函数 PromiseState
    if (this.PromiseState === 'fulfilled') {
        onResolved(this.PromiseResult);
    }
    if (this.PromiseState === 'rejected') {
        onRejected(this.PromiseResult);
    }
}

待更新…

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真的很上进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值