初识js中的Promise()

news/2024/7/19 14:50:53 标签: js, javascript, es6

文章目录

  • 前言
  • 原理
  • 使用场景
  • Promise 的api
  • EventLoop注意点
      • 任务队列
      • 同步任务
      • 异步任务
      • 事件循环
      • 总结
  • 练习
      • 分析执行过程
      • 总结
      • 结果
  • 参考资料




前言

Promise 规范有很多,目前主要的标准有

  • Promise/A

  • Promise/B

  • Promise/D

  • Promise/A

  • Promise/A+

ES6 中,采用了 Promise/A+ 规范,所以接下来是按照《Promise/A+规范》来介绍的。




Promise正如其单词意思一样,“承诺”,即一旦从初始状态(pending)转变成其他状态,那么它就不能改变了。

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

javascript">//不使用Promise        
xhr.get('request_url', function (result) {
    //do something
    console.log(result.id);
});
javascript">//使用Promise
new Promise(function (resolve) {
    //异步请求
    http.get('request_url', function (result) {
        resolve(result.id)
    })
}).then(function (id) {
    //do something
    console.log(id);
})

传统的异步回调写法在多次请求的场景下会出现回调地狱的问题,这样在审计代码的时候非常不便。因此使用promise来编写异步代码,这样看起来就舒爽多了。

javascript">//不使用Promise        
http.get('some_url', function (id) {
    //do something
    http.get('getNameById', id, function (name) {
        //do something
        http.get('getCourseByName', name, function (course) {
            //dong something
            http.get('getCourseDetailByCourse', function (courseDetail) {
                //do something
            })
        })
    })
});

//使用Promise
function getUserId(url) {
    return new Promise(function (resolve) {
        //异步请求
        http.get(url, function (id) {
            resolve(id)
        })
    })
}
getUserId('some_url').then(function (id) {
    //do something
    return getNameById(id); // getNameById 是和 getUserId 一样的Promise封装。下同
}).then(function (name) {
    //do something
    return getCourseByName(name);
}).then(function (course) {
    //do something
    return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
    //do something
});



原理

  • Promise有三种状态,分别是pending、resolved(也可以叫fulfilled)、rejected。
  • pending表示Promise对象实例创建的时候初始状态。
  • resolved(fulfilled)表示为异步任务成功执行的状态,例如,请求状态码为200时。
  • rejected表示异步任务失败的状态,例如,请求状态码为404时,也有可能是抛出异常,这个可以自己设置。
  • 构造一个Promise实例需要给Promise构造函数传入一个函数,然后传入的函数需要有两个形参,两个参数都是函数类型的参数,分别是resolved和rejected。
  • Promise有then方法,then方法就是用来指定Promise对象的状态改变时需要执行的操作。当状态变为resolved时需执行第一个函数onResolved;当状态变为rejected时需执行第二个函数onRejected。
    在这里我们需要注意:pending状态只能变成resolved(fulfilled)成功状态或者pendin状态只能变成失败rejected状态这两种状态;不能resolved(fulfilled)成功状态变成pending状态或者失败状态变成pending状态或者resolved(fulfilled)成功状态和rejected失败状态之间各自相互转换。

可以把 Promise 看成一个有限状态机。初始是 pending 状态,可以通过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。

下图是我个人的一个理解,仅供参考。

img




使用场景

例如使用promise来创建xhr请求,获得json数据。

javascript">function getJson(type = 'GET', url = './test.json', data = {}) {  //设定默认值
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function () {
            if (this.readyState === 1) {
                this.responseType = 'json';
                this.setRequestHeader("Content-type", "application/json; charset=UTF-8");
            } else if (this.readyState === 4) {
                if (this.status === 200 || this.status == 201 || this.status === 304) {
                    resolve({
                        data: this.response,
                        msg: 'success'
                    })
                } else {
                    reject({
                        data: this.response,
                        msg: 'error'
                    })
                }
            }
        }
        xhr.open(type, url);
        xhr.send(JSON.stringify(data));
    })
}

// 模拟ajax
function ajax() {
    getJson('POST', 'http://jsonplaceholder.typicode.com/posts', {
        title: 'foo',
        body: 'bar',
        userId: 1
    }).then(res => {
        console.log(res);
    }).catch(err => {
        console.log(err)
    });
}

Promise 的api

具体使用方法请查阅官方资料。

  • Promise.resolve()
  • Promise.reject()
  • Promise.prototype.then()
  • Promise.prototype.catch()
  • Promise.all([promise1,promise2…]) 列表中所有的promise都有完成,相当于 且
  • Promise.race([promise1,promise2…]) 列表中promise完成一个即可,相当于 或

一些注意点

  • Promise.resolve()的作用将现有对象转为Promise对象resolved。

相当于Promise.resolve(‘test’)==new Promise(resolve=>resolve(‘test’))

  • Promise.reject()返回一个Promise对象,状态为rejected;

相当于Promise.reject(‘test’)==new Promise((resolve, reject)=>reject(‘test’))

  • then方法上边已经做介绍,这里就不再介绍。

  • catch():发生错误的回调函数。

  • Promise.race()的作用是同时执行多个实例,只要有一个实例改变状态,Promise就改为那个实例所改变的状态。

  • Promise.all()适合用于所有的结果都完成了才去执行then()成功的操作。例如:

javascript">// 有一个请求需要同时使用学号/姓名/班级三个参数,而这三个参数的请求分别不同,需要分别请求
let p1 =new Promise(function(resolve,reject){
            resolve(1);
        });
        let p2 = new Promise(function(resolve,reject){
            resolve(2);
        });
        let p3 = new Promise(function(resolve,reject){
            resolve(3);
        });

// 学号/姓名/班级三个参数都请求完成且成功后
Promise.all([p1, p2, p3]).then(function (results) {
            console.log('success:'+results);
        }).catch(function(r){
            console.log("error");
            console.log(r);
        });

EventLoop注意点

要想真正弄明白Promise的运行机制,最好把EventLoop这块吃透。

任务队列

  • 由于js是一门单线程的语言,这就意味着js中所有任务需要排队,前一个任务结束,才会执行后一个任务。

  • 如果前一个任务耗时很长,后一个任务就不得不一直等着。

  • js中所有任务可以分成两种,一种是同步任务,另一种是异步任务。

同步任务

  • 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
  • 所有同步任务都在主线程上执行,形成一个执行栈

异步任务

  • 异步任务是不进入主线程,而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

  • 当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。

img

事件循环

主线程在运行的时候,产生堆和栈。只有栈中的代码执行完毕,主线程才会去读取“任务队列”中的回调函数依次执行。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

img

总结

  • macrotask的执行:是在evenloop的每次循环过程,取出macrotask queue中可执行的第一个(注意不一定是第一个,例如setTimeout可以指定任务被执行的最少延迟时间,当前macrotask queue的首位保存的任务可能还没有到执行时间,所以queue只是代表callback插入的顺序,不代表执行时也要按照这个顺序)。
  • microtask的执行:在evenloop的每次循环过程之后,如果当前的执行栈(call stack)为空,那么执行microtask queue中所有可执行的任务。



练习

javascript">//使用while循环同步阻塞xx毫秒
function sleep(delay) {
    var start = (new Date()).getTime();
    while((new Date()).getTime() - start < delay) {
        continue;
    }
}

console.log((new Date()).getTime());

setTimeout(() => {
    console.log((new Date()).getTime());
    console.log('0');
}, 15000)

new Promise((resolve, reject) => {
    // 异步代码
    setTimeout(function(){
        sleep(5000);
    }, 3000)
    
    sleep(10000);
    console.log('1');
    resolve();
}).then(() => {
    console.log('2');
    new Promise((resolve, reject) => {
        console.log('3');
        resolve()
    }).then(() => {
        console.log('4');
    }).then(() => {
        console.log((new Date()).getTime());
        console.log('5');
    })
}).then(() => {
    console.log('6');
})

new Promise((resolve, reject) => {
    console.log('7');
    resolve();
}).then(() => {
    console.log('8');
})
console.log('9');

分析执行过程

1、首先将定时器中的回调添加到宏任务队列中

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[]**
  • 已打印[]

2、执行到第一个Promise处,在异步任务table中注册了一个阻塞5秒的异步宏任务,然后再同步地阻塞10000毫秒,接下来执行同步代码打印**1,然后状态立马变成resolve,其中的异步回调函数打印代码加入到微队列中[2]**。

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[2]**
  • 已打印[1]

3、往下走,到下一个Promise中的代码,打印同步代码7,然后立即变为resolve状态,并将异步回调打印8的代码放入微队列中。

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[2,8]**
  • 已打印[1,7]

4、往下走,遇到同步代码打印9,至此,主线程执行完了同步任务,此时任务队列为空,异步任务table开始计时,按照对应计时结束后往异步队列中注册任务。

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[2,8]**
  • 已打印[1,7,9]

5、先把微队列中的代码都取出执行完,才去执行后面的代码以及宏队列的代码。

所以先取出2,即打印2, 所以现在的微队列只有一个任务[8]

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[8]**
  • 已打印[1,7,9,2]

6、打印完2后,往下走,又new了一个Promise对象,里面有同步代码打印3,然后立即变为resolve状态,因此将4放入微队列[8,4]

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[8,4]**
  • 已打印[1,7,9,2,3]

7、接下来注意:未打印4的时候,是不会把后面then方法中的5放入微队列中的。因为then返回的还是一个promise对象。在本次EventLoop中,会先将外层Promise中的then中的6放入微队列,因为内层的Promise已经执行完最后一个then方法了,因此现在的微队列是[8,4,6]

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[8,4,6]**
  • 已打印[1,7,9,2,3]

8、取出微队列中的任务进行执行,将执行打印8的代码。打印完8后面没有其余代码,因此继续取出打印4的任务再打印4,当打印完4,之后将后面then中的打印5的异步任务放入微队列,因此现在的微队列是[6,5]

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[6, 5]**
  • 已打印[1,7,9,2,3, 8 , 4 ]

9、往下走,将微队列中剩余的任务中剩余的任务取出执行。

  • 宏任务队列中的任务**[0]**
  • 微任务队列中的任务**[]**
  • 已打印[1,7,9, 2,3,8,4,6,5]

10、直到微队列为空,宏队列中的任务才得以被取出执行。注意,不是现在才开始给宏任务计时,而是在第一次主线程栈空的时候,往子任务队列要任务的时候,就开始计时了。

  • 宏任务队列中的任务**[]**
  • 微任务队列中的任务**[]**
  • 已打印[1,7,9, 2,3,8,4,6,5,0]

总结

  • setTimeout是在第一次主线程栈空的时候,向子任务队列要任务的时候,就开始计时了。

  • new Promise(…).then()返回的也是一个promise对象,注意,是一个新的Promise实例。

  • 在一次EventLoop中,宏队列要等微队列为空的时候才开始取任务。

结果

img

参考资料

《图解 Promise 实现原理》

《ES6入门 之 Promise 对象》

《Promise/A+规范》

《Promise/A+规范中文版》

js promise看这篇就够了》


http://www.niftyadmin.cn/n/555330.html

相关文章

串口控制计算机重启,电脑开机无限重启解决方法

电脑开机无限重启怎么办?下面一起来看看电脑开机无限重启解决方法。1.病毒[冲击波"病毒发作时还会提示系统将在60秒后自动启动.木马程序从远程控制你计算机的一切活动.包括让你的计算机重新启动.清除病毒.木马.或重装系统.2.系统文件损坏系统文件被破坏.如Win2K下的KERNE…

.Net Core小技巧 - 使用Swagger上传文件

前言 随着前后端分离开发模式的普及&#xff0c;后端人员更多是编写服务端API接口。调用接口实现文件上传是一个常见的功能&#xff0c;同时也需要一个选择文件上传的界面&#xff0c;可以编写前端界面上传&#xff0c;可以使用Postman、curl来模拟上传请求。上述的方式多多少少…

js中实现防抖与节流

文章目录防抖节流是怎么一回事&#xff1f;场景&#xff1a;输入框防抖版本版本一 一旦触发keyup就请求思路分析版本二 使用防抖装饰器初体验思路分析版本三 能够保证this和event能正常捕捉的防抖思路分析节流版本版本一 节流之初体验思路分析版本二 我希望它可以在需要时马上触…

[bzoj3244] [洛谷P1232] [Noi2013] 树的计数

Description 我们知道一棵有根树可以进行深度优先遍历&#xff08;DFS&#xff09;以及广度优先遍历&#xff08;BFS&#xff09;来生成这棵树的DFS序以及BFS序。两棵不同的树的DFS序有可能相同&#xff0c;并且它们的BFS序也有可能相同&#xff0c;例如下面两棵树的DFS序都是1…

文件夹连接服务器,Win8 连接服务器+传输文件/文件夹

这几天写的代码&#xff0c;经常把笔记本跑的卡死&#xff0c;每次都要强制关机老师就给了我们借了服务器一开始我是想用虚拟机连接&#xff0c;但是我在虚拟机中有个加强安装安装不了&#xff0c;弃于是直接使用Win8连接&#xff0c;发送文件&#xff0c;亲测有效&#xff0c;…

js实现无限级目录树

文章目录原理N叉树的遍历显示/隐藏功能防止冒泡完整代码参考资料原理 N叉树的遍历 每个data的数据格式如下&#xff1a; var oneChoice {name: A,child: [{name: A-1child:[{...},{...}]},{}] }最终拼接成的html格式如下。 <li><span>-</span>AAA<ul…

linux网络编程中的超时设置

1 下面是在网上找到的资料&#xff0c;先非常的感谢。 用setsockopt()来控制recv()与send()的超时 在send(),recv()过程中有时由于网络状况等原因&#xff0c;收发不能预期进行,而设置收发超时控制&#xff1a; 在Linux下需要注意的是时间的控制结构是struct timeval而并不是某…

iis无法显示页面 因为发生内部服务器错误,HTTP Error 500.0。无法显示页面,因为发生内部服务器错误。...

在asp.net开发中&#xff0c;经常遇到500错误。500错误的原因&#xff0c;基本山都可以归结为代码问题或者配置问题&#xff0c;这种问题往往在服务器本地环境才可以看到报错详情。这是基于系统安全考虑的&#xff0c;不暴露系统结构隐私&#xff0c;可以防止被人猜解系统结构。…