深入浅出Promise


同步与异步

在学习Promise之前我们需要先明白一些基础知识,首先我们要知道什么叫实例对象,什么叫函数对象。所谓实例对象就是我们使用 new 关键字创建出来的对象,称为实例对象,一般简称对象。而函数对象是指当我们把一个函数当作对象使用时,此时我们称这个函数为函数对象

//函数的两种身份:函数、函数对象
function foo(){
    console.log('ok');
}

foo() // ok 当我们用()调用函数时,此时函数称为一个函数
foo.a = 20 // 当我们像这样给函数添加属性或者方法时,此时函数称为一个函数对象

按函数的调用者分类我们可以将某些函数称为回调函数,回调函数就是我们定义的,我们没调用,最终执行了。最典型的例子就是定时器里我们传入的函数。根据回调函数执行时机的不同,我们又可以将回调函数分为同步回调函数和异步回调函数

同步回调函数的特点就是立即执行,完全执行完了才结束,不会放入回调队列中。换言之就是它是在主线程上执行的。例如数组遍历相关的回调函数、Promise的excutor函数

let arr = [10,20,30]
arr.forEach(value => console.log(value))  
console.log('主线程')
// 输出顺序为: 10 20 30 主线程

上述例子中可以明显看到,数组forEach方法里的回调函数先于主线程上的 console.log 输出,这表明这个回调函数就是一个同步回调

与同步回调函数相反,异步回调函数的特点就是延迟执行,它会被放入回调队列里,等主线程上的函数都执行完以后将来再根据条件执行。例如定时器上的回调函数、ajax回调、Promise的成功 | 失败回调

setTimeout(()=>(console.log('hello!')),0)
console.log('主线程')
//输出顺序为: 主线程 hello!

即便定时器的延迟设为 0 ,它里面的回调函数依然要等待主线程执行完毕才能执行。这就是一个典型的异步回调

通过理解同步回调和异步回调的例子,我们可以明白。所谓同步就是绝对的串行执行,只有上一步执行完了下一步才能继续执行。想象这样的一个场景,我们去做饭,电饭煲在煮饭的同时我们可以继续处理我们的菜,我们不必等饭完全煮好了才开始做菜,这样太浪费时间了。这种就是异步执行。如果我们必须等到饭煮好了才开始炒菜之类的,那这种就是同步执行。

Promise

有了上面同步异步的概念以后,接下来我们就可以开始学习Promise了。我们先来看看它是什么,官方给的定义是啥。

  • 抽象表达:

    • Promise是JS中进行异步编程新方案(旧的是谁?---纯回调)
  • 具体表达:

  • 从语法上来说: Promise是一个构造函数

  • 从功能上来说: promise对象用来封装一个异步操作并可以获取其结果

其实看了上面的表达我们还是不懂什么是Promise。从异步这个词入手,我们都知道ajax就是一个典型的异步操作。那么我们就可以用Promise来封装ajax请求。那我们为什么非要用Promise来封装异步操作,我们用普通的回调函数形式不一样可以吗?这就涉及到回调地狱这个问题,关于[Promise的优越性](# 回调地狱 )我们后面再谈。首先来明确一些基础的Promise知识

  • Promise不是回调,是一个内置的构造函数,是程序员自己new调用的。
  • new Promise的时候,要传入一个回调函数,它是同步的回调,会立即在主线程上执行,它被称为executor函数
  • 每一个Promise实例都有3种状态:初始化(pending)、成功(fulfilled)、失败(rejected)
  • 每一个Promise实例在刚被new出来的那一刻,状态都是初始化(pending)
  • executor函数会接收到2个参数,它们都是函数,分别用形参:resolve、reject接收
    • 调用resolve函数会:
      • 让Promise实例状态变为成功(fulfilled)
      • 可以指定成功的value。
    • 调用reject函数会:
      • 让Promise实例状态变为失败(rejected)
      • 可以指定失败的reason。
//new 一个实例
const p  = new Promise((resolve,reject)=>{})

//根据上面描述这个Promise必须传入一个excutor函数,而这个excutor函数又两个形参也是函数,我们不必定义该两个形参函数,可以直接调用
(resolve,reject)=>{}  //excutor

当我们使用一个Promise管理异步操作的时候,我们要在excutor函数内启动异步任务然后再用它的 then 方法来指定异步任务结束后根据Promise实例的状态来调用相应的回调函数。

//用Promise封装一个自己的get请求ajax
function myAjax(url,datas){
    return new Promise(
        (resolve,reject)=>{
            const xhr = new XHLHttpRequest();
            xhr.onreadystatuschange = ()=>{
                if(xhr.readyState === 4){
                    if(xhr.status === 200) resolve('ok') //请求成功将Promise状态置为fulfilled
                    else reject('falure')  //请求失败将Promise状态置为rejected
                }
            }
            xhr.opne('GET',url);
            xhr.send();
        }
    );
}

const p = myAjax('http://127.0.0.1/get',{test:'test'});

p.then( //为Promise实例指定成功与失败的回调函数
    (value)=>{ console.log('请求成功1',value); }, //fulfilled状态调用
    (reason)=>{ console.log('请求失败1',reason); } //rejected状态调用
)

//一个promise指定多个成功/失败回调函数, 则会依次调用并不会覆盖
p.then( //为Promise实例指定成功与失败的回调函数
    (value)=>{ console.log('请求成功2',value); }, //fulfilled状态调用
    (reason)=>{ console.log('请求失败2',reason); } //rejected状态调用
)

//若该次请求失败则依次输出:请求失败1 请求失败2
  • 关于状态的注意点:
    • 三个状态:
      • pending: 未确定的------初始状态
      • fulfilled: 成功的------调用resolve()后的状态
      • rejected: 失败的-------调用reject()后的状态
    • 两种状态改变
      • pending ==> fulfilled
      • pending ==> rejected
    • 状态只能改变一次!

Promise基本方法

Promise构造函数: new Promise (executor) {}

  • executor函数: 是同步执行的,(resolve, reject) => {}
  • resolve函数: 调用resolve将Promise实例内部状态改为成功(fulfilled)。
  • reject函数: 调用reject将Promise实例内部状态改为失败(rejected)。
  • 说明: excutor函数会在Promise内部立即同步调用,异步代码放在excutor函数中。

Promise.prototype.then方法: Promise实例.then(onFulfilled,onRejected)

  • onFulfilled: 成功的回调函数 (value) => {}
  • onRejected: 失败的回调函数 (reason) => {}
  • 特别注意(难点):then方法会返回一个新的Promise实例对象
  • 如果上一个回调返回的是一个非promise对象,则这个新的Promise实例状态为fulfilled
  • 当上一个回调返回一个Promise对象则该新返回的Promise对象的状态与回调返回的Promise对象一致
const p = new Promise((resolve,reject) =>{ resolve('ok'); }) 
const x = p.then(
	value => {return 1}, //返回非Promise值
    reason => {return new Promise.reject(996)}
)
x.then(
	value => console.log('成功了'), //x状态为fulfilled,输出成功了
    reason => console.log('失败了')
)

const z = p.then(
	value => {return new Promise((resolve,reject)=>{})}, //返回Promise值
    reason => {return new Promise.reject(996)}
)
z.then( //z状态为 pending ,不调用回调函数
	value => console.log('成功了'), 
    reason => console.log('失败了') 
)

Promise.prototype.catch方法: Promise实例.catch(onRejected)

  • onRejected: 失败的回调函数 (reason) => {}
  • 说明: catch方法是then方法的语法糖, 相当于: then(undefined, onRejected)

Promise.resolve方法: Promise.resolve(value)

  • 说明: 用于快速返回一个状态为fulfilled或rejected的Promise实例对象
  • 备注:value的值可能是:(1)非Promise值 (2)Promise值
  • 当传入的值为非Promise值时或空值时,直接返回一个 fulfilled 状态的 Promise实例
  • 当传入的值为 Promise 时,返回的Promise状态跟随传入的Promise

Promise.reject方法: Promise.reject方法(reason)

  • 说明: 用于快速返回一个状态必为rejected的Promise实例对象
const x = Promise.reject(996);
x.then(
	value => console.log('成功了',value),
    reason => console.log('失败了',reason) //调用该函数 输出:失败了 996
)

Promise.all方法: Promise.all(promiseArr)

  • promiseArr: 包含n个Promise实例的数组
  • 说明: 返回一个新的Promise实例, 只有所有的promise都成功才成功, 只要有一个失败了就直接失败。
  • 若没有失败的值,且没有pending的值即全都成功返回的新Promise状态为fulfilled
  • 若没有失败的值,但存在pending的值即数组内仅有pending和fulfilled两种值,则返回的新Promise状态为pending

Promise.race方法: Promise.race(promiseArr)

  • promiseArr: 包含n个Promise实例的数组
  • 说明: 返回一个新的Promise实例, 成功还是很失败?以最先出结果的promise为准。
  • 若最先出结果的promise为pending则跳过该promise
  • 这也就意味着race返回的Promise实例仅有fulfilled和rejected两种状态,不存在pending状态的值

回调地狱

上面我们学习了基本Promise使用,但是我们依然没有看出来Promise的优越性在哪里。现在我们有这么一个需求,连发三次Ajax请求,仅当上次请求成功时才发送下一次请求,若请求失败则中断以后的所有请求。

//用纯回调的方式封装ajax
function sendAjax(url,data,success,error){
    const xhr = new XMLHttpRequest()
    xhr.onreadystatechange = ()=>{
        if(xhr.readyState === 4){
            if(xhr.status >= 200 && xhr.status < 300) success(xhr.response);
            else error('请求出了点问题');
        }
    }
    //整理参数
    let str = ''
    for (let key in data){
        str += `${key}=${data[key]}&`
    }
    str = str.slice(0,-1)
    xhr.open('GET',url+'?'+str)
    xhr.responseType = 'json'
    xhr.send()
}
//传统解决方案,链式连发三个请求
sendAjax(
    'https://api.apiopen.top/getJoke',
    {page:1,count:2,type:'video'},
    response =>{
        console.log('第1次成功了',response);
        sendAjax(
            'https://api.apiopen.top/getJoke',
            {page:1,count:2,type:'video'},
            response =>{
                console.log('第2次成功了',response);
                sendAjax(
                    'https://api.apiopen.top/getJoke',
                    {page:1,count:2,type:'video'},
                    response =>{
                        console.log('第3次成功了',response);   //回调地狱
                    }, 
                    err =>{console.log('第3次失败了',err);}
                )
            }, 
            err =>{console.log('第2次失败了',err);}
        )
    }, 
    err =>{console.log('第1次失败了',err);}
)

可以看到当我们要进行三次链式的异步请求时,采用纯回调的方式来处理就导致了回调地狱的问题。要对代码进行维护十分困难。而如果我们采用Promise去封装异步请求,则可以解决回调地狱的问题

//Promsie封装ajax
function sendAjax(url,data){
    return new Promise((resolve,reject)=>{
        const xhr = new XMLHttpRequest()
        xhr.onreadystatechange = ()=>{
            if(xhr.readyState === 4){
                if(xhr.status >= 200 && xhr.status < 300) resolve(xhr.response);
                else reject('请求出了点问题');
            }
        }
        //整理参数
        let str = ''
        for (let key in data){
            str += `${key}=${data[key]}&`
        }
        str = str.slice(0,-1)
        xhr.open('GET',url+'?'+str)
        xhr.responseType = 'json'
        xhr.send()
    })
}

//利用Promise进行三次链式ajax请求
sendAjax('https://api.apiopen.top/getJoke',{page:1})
    .then(
        value => {
            console.log('第1次请求成功了',value);
            //发送第2次请求
            return sendAjax('https://api.apiopen.top/getJoke',{page:1})
    	}
)
    .then(
        value => {
            console.log('第2次请求成功了',value);
            //发送第3次请求
            return sendAjax('https://api.apiopen.top/getJoke',{page:1})
        }
)
    .then(
        value => {console.log('第3次请求成功了',value);}
)
	.cathe(
		err => console.log(err);  //利用错误穿透进行兜底
)

可以明显地对比出来,利用Promise进行链式异步操作能清晰地看到调用结构,维护起来相比纯回调方便了很多。这就解决了回调地狱的问题。

Promise的优势

  • 指定回调函数的方式更加灵活:

    • 旧的: 必须在启动异步任务前指定
    • Promise: 启动异步任务 => 返回promie对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定)
  • 支持链式调用, 可以解决回调地狱问题

    • 什么是回调地狱:回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调函数执行的条件
    • 回调地狱的弊病:代码不便于阅读、不便于异常的处理
    • 一个不是很优秀的解决方案:then的链式调用
    • 终极解决方案:async/await(底层实际上依然使用then的链式调用)

Promise关键问题

如何改变一个Promise实例的状态?

  • 执行resolve(value): 如果当前是pending就会变为fulfilled
  • 执行reject(reason): 如果当前是pending就会变为rejected
  • 执行器函数(executor)抛出异常: 如果当前是pending就会变为rejected

改变Promise实例的状态和指定回调函数谁先谁后?

  • 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调
  • 如何先改状态再指定回调?
    • 延迟一会再调用then()
  • Promise实例什么时候才能得到数据?
    • 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
    • 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据

Promise实例.then()返回的是一个【新的Promise实例】,它的值和状态由什么决定?

  • 简单表达: 由then()所指定的回调函数执行的结果决定
  • 详细表达:
    • 如果then所指定的回调返回的是非Promise值a:
      • 那么【新Promise实例】状态为:成功(fulfilled), 成功的value为a
    • 如果then所指定的回调返回的是一个Promise实例p:
      • 那么【新Promise实例】的状态、值,都与p一致
    • 如果then所指定的回调抛出异常:
      • 那么【新Promise实例】状态为rejected, reason为抛出的那个异常

如何中断promise链:

  • 当使用promise的then链式调用时, 在中间中断, 不再调用后面的回调函数。
  • 办法: 在失败的回调函数中返回一个pendding状态的Promise实例。

Promise错误穿透的原理:

  • 当使用promise的then链式调用时, 可以在最后用catch指定一个失败的回调,
  • 前面任何操作出了错误, 都会传到最后失败的回调中处理了
  • 备注:如果不存在then的链式调用,就不需要考虑then的错误穿透。
const p = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        reject(-100)
    },1000)
})
p.then(
    value => {console.log('成功了1',value);return 'b'},
    reason => {throw reason}//底层帮我们补上的这个失败的回调
)
    .then(
    value => {console.log('成功了2',value);return Promise.reject(-108)},
    reason => {throw reason}//底层帮我们补上的这个失败的回调
)
    .catch(
    reason => {throw reason}
)

async & await

  • async修饰的函数

    函数的返回值为promise对象

    Promise实例的结果由async函数执行的返回值决定,返回非Promise值则返回的Promise对象状态为fulfilled,返回Promise则状态跟随返回的Promise,但是不能返回一个 rejected的Promise,否则报错

const p = (async () =>{return 0;})();
console.log(p) //输出fulfilled
p.then(
	value => console.log('成功了',value), //执行成功回调
    reason => console.log('失败了',reason)
)
----------
const p = (async () =>{return Promise.reject(996);})();
console.log(p) //输出pending ,同时也说明了Promise.reject()是个异步函数
p.then(
	value => console.log('成功了',value), 
    reason => console.log('失败了',reason)//执行失败回调
)
----------
const p = (async () =>{return new Promise(()=>{});})();
console.log(p) 
p.then(
	value => console.log('成功了',value), 
    reason => console.log('失败了',reason)
)//不调用任何一个,说明最后状态为pending
----------
const p = (async () =>{return Promise.resolve(200);})();
console.log(p) //输出pending ,同时也说明了Promise.resolve()是个异步函数
p.then(
	value => console.log('成功了',value), //执行成功回调
    reason => console.log('失败了',reason)
)
  • await表达式

    await右侧的表达式一般为Promise实例对象, 但也可以是其它的值

    (1).如果表达式是Promise实例对象, await后的返回值是promise成功的值

    (2).如果表达式是其它值, 直接将此值作为await的返回值

  • 注意:

    await必须写在async函数中, 但async函数中可以没有await

    如果await的Promise实例对象失败了, 就会抛出异常, 需要通过try…catch来捕获处理

const p1 = new Promise((resolve,reason)=>{
    setTimeout(function(){
        resolve('ok了')
    },2000)
})
const p2 = Promise.reject('中断')

const p3 = new Promise((resolve,reason)=>{
    setTimeout(function(){
        resolve('Ok啊')
    },4000)
})

;(async()=>{
    try{
        const x1 = await p1;
        console.log('1',x1); //输出:1 ok了
        const x2 = await p2; //该点直接失败转入失败回调 throw error
        console.log('2',x2);
        const x3 = await p3;
        console.log('3',x3);
    }catch(err){
        console.log(err); //catch接收上面throw 的 error 输出:中断
    }
})()
------
//上面try里面的代码被浏览器翻译为
p1.then(
	value => {console.log('1',value) return p2;} //输出:1 ok了
    // 不写失败回调底层补上了 reason => throw reason
).then(
    value => {console.log('2',value); return p3;}
).then(
	value => {console.log('3',value);}
)

宏任务与微任务

目前为止,除了Promise里的成功和失败回调是微任务,其它异步回调都是宏任务

宏队列:[宏任务1,宏任务2…]

微队列:[微任务1,微任务2…]

规则:每次要执行宏队列里的一个任务之前,先看微队列里是否有待执行的微任务

​ 1.如果有,先执行微任务

​ 2.如果没有,按照宏队列里任务的顺序,依次执行

自我检测

初级版

// //代码一
setTimeout(()=>{
    console.log('timeout')
},0)

Promise.resolve(1).then(
    value => console.log('成功1',value)
)
Promise.resolve(2).then(
    value => console.log('成功2',value)
)
console.log('主线程')


//代码二
setTimeout(()=>{
    console.log('timeout1')
})
setTimeout(()=>{
    console.log('timeout2')
})

Promise.resolve(1).then(
    value => console.log('成功1',value)
)
Promise.resolve(2).then(
    value => console.log('失败2',value)
) 

//代码三
setTimeout(()=>{
    console.log('timeout1')
    Promise.resolve(5).then(
        value => console.log('成功了5')
    )
})
setTimeout(()=>{
    console.log('timeout2')
})

Promise.resolve(3).then(
    value => console.log('成功了3')
)
Promise.resolve(4).then(
    value => console.log('失败了4')
)

高级版

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

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