什么是Promise

它是一个ES6提出一个新语法,用来优化异步代码的写法。

Promise的基本使用

Promise是承诺的意思,es6中提出一个新api。
用来优化异步代码。它的经典使用方式如下:

var p1 = new Promise(function(resolve,reject){
     //异步操作 resolve(obj1)  或者 reject(obj2)
});
p1.then(function(rs){
    // 如果p1的状态是resolved,则then中的函数
    //会执行,且obj1的值会传给rs
}).catch(function(rs){
    // 如果p1的状态是reject,则catch中的函数
    //    会执行,且obj2的值会传给rs
}).finally(function(){
    // 一定会执行的函数
})

构造器

// const obj = new Object()
// const arr = new Array()
const p1 = new Promise(function(resolve,reject){
  // 执行异步代码
  // 调用resolve,或者reject
});
console.dir(p1)

要点:

● 构造器必须要给定一个参数,如果不给就是会报错。例如,new Promise() 报错的信息是: Promise resolver undefined is not a function
● 构造器的实参是一个函数,这个函数的特殊之处在于它有两个形参(resolve,reject),这两个形参也是函数。在格式上,也可以采用箭头函数来改写。例如:var p1 = new Promise((resolve,reject)=>{})。
● 在函数体的内部, 一般会执行异步代码,然后根据情况来调用resolve()或者是reject() 。调用resolve或者是reject后会产生什么样的后果,在后面小节介绍。 当然了,再次强调一下resolve和reject只是形参名,可以改写成其它的。

三种状态和值

一个Promise对象的状态可能是如下三种之一:pending,resolved,rejected.

初始态pending

pending。它的意思是 "待定的,将发生的",相当于是一个初始状态。 创建Promise对象时,且没有调用resolve或者是reject方法,相当于是初始状态。这个初始状态会随着你调用resolve,或者是reject函数而切换到另一种状态。

var p = new Promise((resolve,rejectok,err)=>{ console.info('发呆.....' )})
console.dir(p)

成功态resolved

也有叫fulfilled的。
resolved。表示解决了,就是说这个承诺实现了。 要实现从pending到resolved的转变,需要在 创建Promise对象时,在函数体中调用了resolve方法(即第一个参数)。

var p = new Promise((resolve,reject)=>{ resolved();})
console.dir(p)

注意
注意,上面的resolve和reject仅是形参名而已。

失败态rejected

rejected。拒绝,失败。表示这个承诺没有做到,失败了。要实现从pending到rejected的转换,只需要在创建Promise对象时,调用reject函数。

var p = new Promise((resolve,reject)=>{ reject()} )
console.dir(p)

三种状态小结

1.状态是可转化。
最初创建promise对象时,默认状态是pending,如果在函数体内部调用了第一个参数对应的函数,则状态变成了resolved;如果调用了第二个参数对应的函数,则状态变成了rejected

 pending -----  resolve() --> resolved;
 pending -----  reject()  --> rejected;

2.状态转换是不可逆的。
一旦从pending ---> resolved(或者是rejected),就不可能再回到pending,也不能由resolved变成rejected。

promise的值

一个promise对象除了状态之外,还有promiseValue。在构造器中,这个值在调用resolve和reject方法时传入
例如

var p = new Promise( (resolve,reject) => { resolve(123); } );
//  此时,prommise对象p的状态是 resolved,值是123。
console.dir(p)
var p = new Promise( (resolve,reject) => { reject(123); } );
//  此时,prommise对象p的状态是 rejected,值是123
console.dir(p)

在构造器的函数体中,一旦状态发生了变化,就会进行then,或者是catch中去,同时把promiseValue传入对应的函数。

具体来说:

  • 状态从pending变成resolved,进入then中,调用函数,并传入此时的promiseValue(就是调用resolve时传入的实参)
  • 状态从pending变成rejected,进入catch中,调用函数,并传入此时的promiseValue(就是调用reject时传入的实参)
  • 根据实际情况的需要,也可以不加上finally()。

使用promise改造回调函数

基本套路

第一步:建立模板。这里的模板是指固定的套路:写一个空函数,在函数体中的创建一个promise对象,并返回

function fnName(){
    var p = new Promise((resolve,reject)=>{  })
    return p;
}

当然, 如果使用箭头函数的话,还可以简化。
第二步:把异步功能写入构造器中,根据实际来决定是否调用resolve,reject。

function fnName(){
    var p = new Promise((resolve,reject)=>{
        // 这里写具体的代码,并在某个恰当的时机去调用resolve和reject函数。
    })
    return p;
}

第三步:调用函数。
通过fnName().then().catch() 结构来调用这个函数

then的格式及执行逻辑

作用
then方法的作用是为Promise对象添加状态改变时的回调函数

then的格式

它有两个参数,每个参数都是函数。 第二个参数是可选的。
如下:

// p 是一个promise对象
p.then(函数1[,函数2])

它的两个参数都是函数。

  • 第一个参数是resolved状态的回调函数。当p的状态从pending变成了resolved,函数1会执行。
  • 第二个参数是rejected状态的回调函数。 当p的状态从pending变成了rejected,函数2会执行。

其中第二个参数是可选的,如果只写一个参数的话就是如下:

promise对象.then(函数1)

执行逻辑

var p = new Promise((resolve,reject)=>{
   //  resolve(val1);
   reject(val2)
})

p.then(
      okVal=>{
        console.info("成功");
        console.log(okValue);
        }, 
      errVal=>{
        console.info("失败");
        console.log(errValue);
    }
)

它的两个参数都是函数,其执行逻辑是:

  • 如果promise对象的状态是resolved,则then()会执行第一个函数,并传入当前的PromiseValue(即上面的val1);
  • 如果promise对象的状态是rejected,则then()会执行第二个函数,并传入当前的PromiseValue(即上面的val2);
  • 特别地,如果promise对象的状态是rejected,且此时then方法并没有设置第二个参数,就会向外抛出一个错误,错误的提示大约是Uncaught (in promise)。

示例

var p = new Promise((resolve,reject)=>{
    resolve(1)//主动调用resolve,并传入
})
// 此时,P的状态是resolved,且值promiseValue 是1.
p.then((res)=>{
    // 因为p的状态是resolved,所以自动执行then的第一个参数,并且把promisevalue传进来。
    console.log("then,ok",res)
})

执行输出结果是:then,ok,1

then的返回值

then()方法的返回值也是一个promise对象,所以它支持链式写法。但是要注意的是它的返回值是一个新的promise对象,与调用then方法的并不是同一个对象

示例

var p1 = new Promise(()=>{});
var p2 = p1.then(function f_ok(){}, function f_err(){}); 
// p2也是一个promise对象。

console.log(p1 === p2); // false

如上代码可以说明p1.then()的结果是一个与p1不同的promise对象。换句话说,then()会封装一个全新的promise对象p2。那既然 p2也是一个promise对象,那么,p2的状态(promiseStatus)和值(promiseValue)分别是什么?

p2的状态及promiseValue如何确定?

p2的状态及promiseValue按如下规则来确定

● 如果p1的状态是pending,则p2的状态也是pending。
● 如果p1的状态是resolved,then()会去执行f_ok,则p2的状态由f_ok的返回值决定。
○ 如果f_ok返回值不是promise对象,则p2的状态是resolved,且p2的promiseValue就是f_ok函数的return值。
○ 如果f_ok返回值是一个promise对象,则p2的状态及promiseValue以这个promise对象为准。
○ 如果f_ok这个函数内部发生了错误(或者是用户主动抛出错误),则p2的状态是rejected,且p2的promiseValue就是这个错误对象。
● 如果p1的状态是rejected,then()会去执行f_err,则p2的状态由f_err的返回值决定。
○ 如果f_err返回值不是promise对象,则p2的状态是resolved,且p2的promiseValue就是f_err函数的return值。
○ 如果f_err返回值是一个promise对象,则p2的状态及promiseValue以这个promise对象为准。
○ 如果f_err这个函数内部发生了错误(或者是用户主动抛出错误),则p2的状态是rejected,且p2的promiseValue就是这个错误对象。

示例练习

var p1 = new Promise(()=>{});
var p2 = p1.then(function f_ok(){}, function f_err(){}); // p2也是一个promise对象。
console.dir(p1); // pending
console.dir(p2); // pending
var p1 = new Promise((resolve,reject)=>{ resolve()});
var p2 = p1.then(function f_ok(){
    return 1
}, function f_err(){}); // p2也是一个promise对象。
console.dir(p1);    // resolved, undefined
console.dir(p2); // resolved, 1

特殊地,如果f_ok()中并没有return语句,则相当于是 return undefined

catch的格式及用法

catch的格式及用法

catch()的格式及用法

Promise.prototype.catch 是 Promise.prototype.then(null, reject)的别名,用于指定当promise对象的状态从pending变成rejected的回调函数 。

var p1 = new Promise((resolve,reject)=>{
    reject('s')
});

p1.catch(function(err){
    console.log(err);
})

// 与下面的代码等价
p1.then(null, function(err){
    console.log(err);
})

单独使用catch没有什么意义,它一般与then一块使用。如下

new Promise((resolve,reject)=>{

}).then(function(result){
    // 如果promise对象的状态是resolved的,就会到这里来,并且result会接收promiseValue的值
}).catch(function(err){
    // 如果promise对象的状态是rejected的,就会到这里来,并且err会接收promiseValue的值
})

// 上面的代码如何拆分来写的话,等价于:
var p1 = new Promise((resolve,reject){

});
var p2 = p1.then(function(result){

});
var p3 = p2.catch(function(err){

})

catch的返回值

catch的返回值仍是一个promise对象,确定它的值的方式与then(null,(errVal)=>{ })的方式一致。

promise的链式调用

链式调用: $().attr().val()

p.then().then().then()...

示例

function do1() {
    console.log("任务1");
}
function do2() {
    console.log("任务2");
}
function do3() {
     console.log("任务3");
}
function do4() {
    console.log("任务4");
}

var p = new Promise((resolve,reject)=>{ resolve()})
p.then(do1)
 .then(do2)
 .then(do3)
 .then(do4);

结果输出是:任务1任务2任务3任务4
分析

var p1 = p.then(do1);
var p2 = p1.then(do2)
var p3 = p2.then(do3)
var p4 = p3.then(do4)

第一步:由于p的状态是resolved,所以p.then(do1)中,do1函数会执行。输出任务1。

第二步:确定p1的状态。按前面关于then的部分的介绍,p1的状态由do1()来决定。因为do1并没有明确指定返回值,则返回值就是undefined. p1的状态就是resolved。

第三步:由于p1的状态是resolved,所以p1.then(do2)会继续执行do2。输出任务2 ,且p2的状态由do2来决定。与第二步的分析相同,p2的状态仍是resolved。

第四步:接下来看p3。由于p2的状态是resolved,所以p2.then(do3)会继续执行do3。输出任务2 ,且p3的状态由do3来定,仍是resolved。

最后:p3.then(do4)。由于p3的状态是resolved,所以执行do4。输出任务4 。

小小一道面试题

代码填空,并完成指定功能。

function sleep(time){
    // 请写出你的代码
}

sleep(2000).then(()=>{
    console.log("后续操作")
})
console.log(2);

目标 是让sleep 的功能与setTimeout一样:就是等2000毫秒之后再执行后续操作。

参考代码如下:

function sleep(time){
    return new Promise(function(resolve,reject){
        // 异步操作,根据执行结果,决定是否调用 resolve,reject
        setTimeout(function(){
            resolve()
        }, time)
    })
}

async-await语法

async,await 是es7中新增的语法,用来进一步改进异步代码的写法,是promise升级版!

async

async函数返回一个 Promise 对象。

async函数内部return语句返回的值是Promise 对象的值

function f1 () {
  return 1
}
async function f2 () {
  return 1
}
async function f3 () {}
const r1 = f1()
const r2 = f2()
const r3 = f3()
console.log(r1) // 1
console.log(r2) // Promise, resolved(1)
console.log(r3) // Promise, resolved(undefined)
r2.then(res => { console.log(res) })
r3.then(res => { console.log(res) })

await 命令

await的外层函数必须有一个async.

正常情况下,await命令后面是一个 Promise 对象,返回该promise的值。如果不是 Promise 对象,就直接返回对应的值。

<script>
    async function f() {
        const p = new Promise((resolve,reject)=>{
            resolve(100)
        })
        const a = 1
    
        // await 等待promise的状态变化完成(pending->resolved, pending->rejected)
        // 取出promise的值
        const b = await p 

        console.log(a,b)
    }

    f()
</script>

async函数内部的执行流程

在执行async函数(设名为asyncF)时,进入函数内部

  • 按序执行同步代码
  • 遇到await,跳出asyncF函数,

继续执行后续代码。

当await后的异步代码执行完成之后,接着执行asyncF中的后续代码。

const p1 = new Promise((resolve, reject) => {
  setTimeout(()=>resolve(100), 3000)
})

async function f () {
  console.log('开始执行f')
  var a = await p1
  console.log('p1完成了')
  console.log(a)
  return 1
}

console.log('主函数运行...')
const res = f()
console.log('f()完成,返回值是', res)
res.then(res => { console.log(res) })

执行结果是:

主函数运行...
开始执行f
f()完成,返回值是 Promise { <pending> }
----此处等了3s----
p1完成了
100
1

事件循环 eventloop - 面试

js运行在宿主环境中。
宿主环境有: 浏览器, nodejs

浏览器本身是一个复杂的系统,它要做的事情非常多,例如: 执行js代码,请求图片资源,解析css,渲染页面,响应鼠标的点击等等。在实现层面,浏览器内部会用不同的功能模块去完成不同的事情。这些不同的模块就体现为进程

进一步把进程进行划分:

  1. 主进程。用来协调控制其他子进程。
  2. GPU进程。用于3D绘制等。
  3. 渲染进程。就是我们说的浏览器内核,负责具体页面的渲染,脚本执行,事件处理等。浏览器的每个tab页背后就有一个渲染进程。

具体来说,一个渲染进程包括:

  1. 主线程。统一调度其他线程
  2. GUI渲染线程。负责渲染页面,布局和绘制。与JS引擎互斥。
  3. JS引擎线程。负责处理解析和执行javascript脚本程序。与GUI渲染线程互斥的
  4. 事件触发线程。用来控制事件循环(鼠标点击、setTimeout、ajax等)。当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中。
  5. setInterval与setTimeout所在的线程。定时任务并不是由JS引擎计时的,是由定时触发线程来计时的。计时完毕后,通知事件触发线程
  6. 异步http请求线程。浏览器有一个单独的线程用于处理AJAX请求,当请求完成时,若有回调函数,通知事件触发线程。
  7. io线程。用来接收其他进程的消息。

每个渲染进程(一个tab页)都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是消息队列和事件循环。
eventLoop

  1. 主线程上要做很多事情,例如:js代码执行,页面布局计算,渲染等
  2. 主线程同一时刻只能做一件事,事情多了就要排队。所以主线程维护了任务队列。
  3. 某个事件发生时,事件触发线程 就把对应的任务添加到主线程的任务队列中。
  4. 主线程上的任务完成之后,就会从任务队列中取出任务来执行。

任务是以事件及其回调的方式存在的。当事件(用户的点击,图片的成功加载)发生时,将其回调添加到任务队列;主线程上的任务完成之后,就会从任务队列中取出任务来执行,此过程不断重复从而形成一个循环,称为eventLoop

宏任务和微任务

为什么任务要分为同步任务和异步任务?
● 同步任务:同步任务不需要进行等待可立即看到执行结果,比如console
● 异步任务:异步任务需要等待一定的时候才能看到结果,比如setTimeout、网络请求

异步任务,又可以细分为宏任务和微任务。下面列举目前学过的宏任务和微任务。
2022-04-30T11:42:34.png

console.log('1')
new Promise((resolve, reject) => {
  resolve('2')
}).then((res) => {
  console.log(res)
})
setTimeout(() => {
  console.log('3')
})
new Promise((resolve, reject) => {
  resolve('4')
}).then((res) => {
  console.log(res)
})
console.log('5')

● 先执行同步代码
● 遇到宏任务,放入队列
● 遇到微任务,放入微任务队列
● 执行栈为空
○ 将微任务入栈执行
● 所有的微任务完成之后,取出宏任务队列来执行

面试题分析

  • 1
console.log(1)

setTimeout(function() {
  console.log(2)
}, 0)

const p = new Promise((resolve, reject) => {
  resolve(1000)
})
p.then(data => {
  console.log(data)
})

console.log(3)

结果:1,3,1000,2

  • 2
console.log(1)
setTimeout(function() {
  console.log(2)
  new Promise(function(resolve) {
    console.log(3)
    resolve()
  }).then(function() {
    console.log(4)
  })
})

new Promise(function(resolve) {
  console.log(5)
  resolve()
}).then(function() {
  console.log(6)
})
setTimeout(function() {
  console.log(7)
  new Promise(function(resolve) {
    console.log(8)
    resolve()
  }).then(function() {
    console.log(9)
  })
})
console.log(10)

结果:1,5,10,6,2,3,4,7,8,9

  • 3
 console.log(1)

  setTimeout(function() {
    console.log(2)
  }, 0)

  const p = new Promise((resolve, reject) => {
    console.log(3)
    resolve(1000) // 标记为成功
    console.log(4)
  })

  p.then(data => {
    console.log(data)
  })

  console.log(5)

结果:1,3,4,5,1000,2

  • 4
 new Promise((resolve, reject) => {
    resolve(1)

    new Promise((resolve, reject) => {
      resolve(2)
    }).then(data => {
      console.log(data)
    })

  }).then(data => {
    console.log(data)
  })

  console.log(3)

结果:3,2,1

发表评论