Promise

103 阅读13分钟

1.前置知识

1.1 函数对象与实例对象

  1. 函数对象:将函数作为对象使用时,简称为函数对象
  2. 实例对象:new构造函数或类产生的对象,我们称之为实例对象
<script>
   // 函数对象
   function Person(name,age) {
        this.name = name
        this.age = age
   }
   Person.a = 1 // 将Person看成一个对象
   console.log(Person.a) // 1
   
   // 实例对象
   const p1 = new Person('张三',18) // p1 是Person的实例对象
   console.log(p1) // {name:'张三',age:18}
</script>

1.2 回调函数的分类

简单来讲,回调函数是一个函数被作为参数传递给另一个函数,回调函数在另一个函数中被调用,可以是匿名函数,也可以是命名函数 它的特点是:

  1. 我们的自己定义的函数
  2. 我们没有手动调用
  3. 最终它执行了

回调函数分为同步回调函数异步回调函数

  • 同步的回调函数: 函数立即在主线程上执行,不会放入回调队列中(比如:数组遍历相关的回调函数,Promise的executor函数)
  • 异步回调函数: 函数不会立即执行,会放入回调队列中以后执行(比如:定时器的回调,ajax的回调,Promise的成功与失败的回调)
//  同步的回调函数
let arr = [1,3,5,7,9]
arr.forEach((item)=>{
    console.log(item)
})
console.log('主线程上的代码')
// 执行结果为    1,3,5,7,9, 主线程上的代码
// 异步的回调函数
setTimeout(()=>{
    console.log('@@')
},0)
console.log('主线程')
// 执行结果为: 主线程   @@

1.3 js中的error

1.3.1js中的错误的类型

Error:所有错误的父类型

  • ReferenceError:引用的变量不存在
  • TypeError:数据类型不正确
  • RangeError:数据值不在其所允许的范围内 ~死循环
  • SyntaxError:语法错误
// ReferenceError:引用的变量不存在
 console.log(a)

// TypeError:数据类型不正确
const demo = ()=>{}
demo()()

// RangeError:数据值不在其所允许的范围内
const demo = ()=>{demo()}
demo()
        
// SyntaxError:语法错误
console.log(1

1.3.2 错误处理

try{}catch(){}捕获错误

  1. try中放可能出现错误的代码,一旦出现错误立即停止try中代码的执行,调用catch,并携带出错误信息
  2. 没有错误,catch不会被调用
try{
    console.log(1)
    console.log(a) // a没有定义
    console.log(2)  // 不会执行
}catch(err){
    console.log(err) //  a is not defined
}

throw new Error 抛出错误,一般是new Error(错误对象)抛出一个错误对象,抛出的错误对象中: message属性是错误相关信息,stack属性是记录信息

// 如何抛出一个错误
function demo() {
     const date = Date.now()
     if (date % 2 === 0) {
        console.log('偶数,可以正常工作')
     }else{
        // 抛出一个错误对象
        throw new Error('奇数不可以工作!') 
     }
}

2.Promise的理解和使用

2.1 Promise是什么?

Promise 是JS中进行异步编程的新方案

  1. 从语法上来说:Promise是一个构造函数,
  2. 从功能上来说:Promise的实例对象可以用来封装一个异步操作,并可以获取其结果(成功/失败)的值
  1. Promise不是回调函数,是一个内置的构造函数,是程序员需要自己new调用的
  2. new Promise的时候,必须要传递一个回调函数(且是同步的回调),会立即在主线程上执行, 官方称之为promise的executor函数
  3. 每个Promise实例对象都有三种状态,分别为:初始状态(pending),成功(fufilled),失败(rejected)
  4. 每一个Promise实例在刚被new出来的那一刻,状态都是初始化(pending)
  5. executor函数会接收到两个参数,它们都是函数,分别用形参:resolve,reject接收
    1. 调用resolve,会让Promise实例的状态变为成功(fufilled),同时可以指定成功的value
    2. 调用reject,会让Promise实例的状态变为失败(rejected),同时可以指定失败的reason
// 创建一个Promise实例对象
 const p = new Promise((resolve,reject)=>{
      resolve('ok')
      // reject('reason')
 })
 console.log('p',p) // 一般不把Promise实例做打印

2.2 Promise的基本使用

2.2.1编码步骤:

  1. 创建Promise的实例对象(pending状态),传入executor函数
  2. executor中启动异步任务(定时器,ajax请求等)
  3. 根据异步任务的结果,做不同的处理:
    • 如果异步任务成功了: 我们调用resolve(value),让Promise实例对象状态变为成功(fulfilled),同时指定成功的value
    • 如果异步任务失败了: 我们调用reject(reason),让Promise实例对象状态变为失败(rejected),同时指定失败的reason
  4. 通过then方法为Promise的实例指定成功,失败的回调函数,来获取成功的value,失败的reason 注意:then方法所指定的:成功的回调,失败的回调都是异步的回调
// 创建一个Promise实例对象
const p = new Promise((resolve, reject) => {
        reject('我是一些错误信息')
        // resolve('我是服务器返回的数据')
})
p.then(
    // 成功的回调 --- 异步
    (value) => { console.log('成功1',value) }, 
    // 失败的回调 --- 异步
    (reason) => { console.log('失败1',reason) }
)
p.then(
    // 成功的回调 --- 异步
    (value) => { console.log('成功2',value) }, 
    // 失败的回调 --- 异步
    (reason) => { console.log('失败2',reason) }
)
console.log('@@@')

// 打印顺序:@@@   失败1...     失败2...     (由此判断为异步的回调)

注意:

  1. Promise只有三个状态pending,fufilled,rejected
  2. 状态两种改变 pending-->fufilled(初始化到成功), pending--->rejected(初始化到失败)
  3. 状态只能改变一次 !!!!
  4. 一个promise指定多个成功/失败回调函数,都会调用吗? 如上面代码所示:失败1,失败2都打印了,所以是

2.2.2与ajax配合使用

const p = new Promise((resolve, reject) => {
      // 发送ajax请求
      // https://api.apiopen.top/api/getHaoKanVideo?page=1&count&2type=video
      // 上方接口为一个开放的获取搞笑视频的api接口
      const xhr = new XMLHttpRequest()
      xhr.onreadystatechange= ()=>{
          if(xhr.readyState === 4){
             // readyState为4代表接收完毕,接收的可能是:服务器返回的成功数据,服务器返回的错误数据
             if (xhr.status === 200) {
                 resolve(xhr.response)
             }else{
                 reject('请求出错,请稍后再试!!')
             }
          }
      }
      xhr.open('GET','https://api.apiopen.top/api/getHaoKanVideo')
      xhr.responseType = 'json'
      xhr.send()
      })
      p.then(
          // 成功的回调 --- 异步
          (value) => { console.log('成功value',value) },  // value就是接口获取成功的数据
          // 失败的回调 --- 异步
         (reason) => { console.log('失败reason',reason) } // resaon就是接口获取失败的提示
      )

封装ajax请求: 定义一个sendAjax请求,对xhr的get请求进行封装,该函数接收两个参数,url(请求地址),data(参数对象)

function sendAjax(url, data) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(xhr.response)
                } else {
                    reject('请求出错,请稍后再试!!')
                }
            }
        }
        let str = ''
        for (const key in data) {
            str += `${key}=${data[key]}&`
        }
        str = str.slice(0, -1)
        xhr.open('GET', url + '?' + str)
        xhr.responseType = 'json'
        xhr.send()
    })
}
const x = sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 })
x.then(
    // 成功的回调 --- 异步
    (value) => { console.log('成功了', value) },
    // 失败的回调 --- 异步
    (reason) => { console.log('失败了', reason) }
)    

3.Promise相关的api

  1. Promise构造函数: new Promise (executor){},用于创建一个Promise实例对象
  • executor函数:同步执行,(resolve,reject)=>{}
  • resolve函数:调用resolve函数,将Promise实例内部状态改为成功(fufilled)
  • reject函数:调用reject函数,将Promise实例内部状态改为失败(rejected)
  • 说明:executor函数会在Promise内部立即同步调用,异步代码放在executor函数中
  1. Promise.prototype.then方法:供Promise实例调用,用于指定成功或者失败的回调,Promise实例.then(onFufilled,onRejected)
  • onFufilled:成功的回调函数(value)=>{}
  • onRejected:失败的回调函数(reason)=>{}
  • 特别注意(难点):then方法会返回一个新的Promise实例对象
  1. Promise.prototype.catch方法:供Promise实例调用,可以直接传入失败的回调函数,Promise实例.catch(onRejected)
  • onRejected:失败的回调函数(reason)=>{}
  • catch方法是then方法的语法糖,相当于:then(undefined,onRejected)
  1. Promise.resolve方法:用于快速返回一个状态为fufilled或rejected的Promise实例对象Promise.resolve(value)
  • Promise.resolve()可以传Promise的值,它的状态由传入的promise值的状态保持一致
  • 可以传非Promise的值(比如:字符串,{},[]等)
  1. Promise.reject方法:用于快速返回一个状态必须为rejected的Promise实例对象,Promise.reject(reason)
  1. Promise.all方法:Promise.all(promiseArr)
  • promiseArr:包含n个Promise实例的数组
  • 说明:返回一个新的Promise实例,只有所有的promise都成功才成功,只要有一个失败了就失败
  1. Promise.race方法:Promise.race(promiseArr)
  • 说明:返回一个新的Promise实例,成功还是失败? 以最先出结果的promise为准
// Promise.all和Promise.race
const p1 = Promise.reject('a')
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('b')
    }, 500)
})
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('c')
    }, 2000)
})
// Promise.all
const x1 = Promise.all([p1,p2,p3])
x1.then(
    (value)=>{console.log('成功',value)},
    (reason)=>{console.log('失败',reason}
)
// 执行结果 失败a(只有所有的成功才成功,p1,p3都失败)
// Promise.race   
const x2 = Promise.race([p3,p2,p1])
x2.then(
    (value)=>{console.log('成功',value)},
    (reason)=>{console.log('失败',reason)}
)
// 执行结果  失败a (p1为失败且最先出结果)

4.promise的几个关键问题

4.1 如何改变一个Promise实例对象的状态

  1. 执行resolve(value)方法,如果当前状态是pending,就会变为fulfilled
  2. 执行reject(reason)方法,如果当前状态是pending,就会变为rejected
  3. 在执行器函数(excutor)中抛出异常,如果当前状态是pending,就会变为rejected
const p = new Promise((resolve, reject) => {
      throw 900 // 手动抛异常
      // resolve(1)
      // reject('-1')
})
console.log('p',p) // rejected

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

  1. 都有可能,正常情况下是先指定回调再改变状态,但也可以先改状态再执行回调
  2. 如何先改状态再指定回调? 延迟一会再调用then()
  3. Promise实例什么时候才能得到数据? 两个条件(1.指定回调了,2.状态改变了)缺一不可
  • 如果先指定的回调,那当状态发生改变时,回调函就会调用,得到数据
  • 如果先改变状态,那当指定回调时,回调函数就会调用,得到数据
// 1.先指定回调,后改变状态
const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('a')
    }, 1000)
})
p1.then(
    (value) => { console.log('成功了', value) },
    (reason) => { console.log('失败了', reason) }
)

// 2.先改状态,后指定回调( 延迟一会调用then)
const p2 = new Promise((resolve, reject) => {
    resolve('a')
})
setTimeout(() => {
    p2.then(
        (value) => { console.log('成功了', value) },
        (reason) => { console.log('失败了', reason) }
    )
},2000)

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

简单来说:由then()所指定的回调函数执行的结果决定

详细来说:

  1. 如果then所指定的回调返回的是非Promise值(undefined,{},[],字符串等):那么【新Promise实例】状态为:成功(fufilled),成功的value为非Promise值a,
  2. 如果then所指定的回调返回的是一个Promise实例p:那么【新Promise实例】的状态,值,都与p一致
  3. 如果then所指定的回调抛出异常:那么【新Promise实例】的状态为rejected,reason为抛出的那个异常
const p = new Promise((resolve, reject) => {
        resolve('a')
        // reject('a')
})
p.then(
    value => { console.log('成功了1', value); return Promise.reject('a') },
    reason => { console.log('失败了1', reason) }
).then(
    value => { console.log('成功了2', value) },
    reason => { console.log('失败了2', reason) }
).then(
    value => { console.log('成功了3', value) },
    reason => { console.log('失败了3', reason) }
).then(
    value => { console.log('成功了4', value) },
    reason => { console.log('失败了4', reason) }
)

代码执行结果:

image.png

4.4Promise如何串联多个异步任务 ---- 通过then的链式调用

// 利用之前封装的sendAjax方法连发三次请求
function sendAjax(url, data) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    resolve(xhr.response)
                } else {
                    reject('请求出错,请稍后再试!!')
                }
            }
        }
        let str = ''
        for (const 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/api/getHaoKanVideo', { page: 1, size: 2 })
.then(
    (value) => {
        console.log('第1次请求成功', value)
        // 发送第2次请求
        return sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 })
    },
    (reason) => { console.log('第1次请求失败', reason) }
)
.then(
    (value) => {
        console.log('第2次请求成功', value)
        // 发送第3次请求
        return sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 })
    },
    (reason) => { console.log('第2次请求失败', reason) }
)
.then(
    (value) => {
        console.log('第3次请求成功', value)
    },
    (reason) => { console.log('第3次请求失败', reason) }
)

4.5如何中断promise链

中断promise链:是指当使用promise的then链式调用时,在中间中断,不再调用后面的回调函数

具体办法:在失败的回调函数中返回一个pending状态的Promise实例

// 比如中断三次连发sendAjax的请求
// 1.假如我们所传递的url出错,第一次.then回调就会调用onReject失败的回调
// 2.在失败的回调中 return new Promise(()=>{}) 返回一个pending状态的Promise实例
// 3.请求将会中断直接跳出.then的调用链,不会执行第2,3次请求
sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 })
.then(
    (value) => {
        console.log('第1次请求成功', value)
        // 发送第2次请求
        return sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 })
    },
    (reason) => { console.log('第1次请求失败', reason); return new Promise(()=>{})}
)
.then(
    (value) => {
        console.log('第2次请求成功', value)
        // 发送第三次请求
        return sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 })
    },
    (reason) => { console.log('第2次请求失败', reason) }
)
.then(
    (value) => {
        console.log('第3次请求成功', value)
        // return sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 })
    },
    (reason) => { console.log('第3次请求失败', reason) }
)

4.6promise错误穿透

(如果不使用.then的链式调用,不用考虑错误穿透的问题)

当使用promise的then链式调用时,可以在最后用catch指定一个失败的回调,前面的任何操作出了错误,都会传到最后失败的回调中处理,解决每个.then调用中书写失败的回调函数造成代码冗余的问题

// 如下代码,注释掉.then中失败的回调,执行完第一次.then的成功回调返回一个失败的promise值,影响第二次.then的状态为失败,但并没有为它指定失败的回调,最后结果会执行到catch中,打印 "失败了 -111"
const p = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('a')
        // reject(a)
    })
})
p.then(
    (value)=>{console.log('成功1',value); return Promise.reject(-111)},
    // (reason)=>{console.log('失败1',reason);}
)
.then(
    (value)=>{console.log('成功2',value);},
    // (reason)=>{console.log('失败2',reason);}
).catch(
    (reason)=>{console.log('失败了',reason)}
)

5.Promise的优势

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

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

    什么是回调地狱? 回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调函数执行的条件回调地狱的弊病:代码不便于阅读,不便于异常处理

6.async和await的使用

await异步等待

  1. await语句等待一个Promise成功的结果,必须包裹在一个async函数里
  2. 处理Promise失败的结果可以使用 try{ } catch (){ }
// 将sendAjax请求的promise调用链用async await的形式写出来
(async () => {
  try {
      const res1 = await sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 }, 1)
      console.log('res1', res1);
      const res2 = await sendAjax('https://api.apiopen.top/api/getHaoKanVideo', { page: 1, size: 2 }, 2)
      console.log('res2', res2);
      const res3 = await sendAjax('https://api.apiopen.top/api2/getHaoKanVideo', { page: 1, size: 2 }, 3)
      console.log('res3', res3);
  } catch (reason){
      console.log('失败',reason)
  }
})()

6.1async与await的规则

async修饰的函数:

  • 函数的返回值为promise对象
  • Promise的实例结果由async函数执行的返回值决定

await表达式

  • await右侧的表达式一般为Promsie实例对象,但也可以是其他的值
  • 如果表达式是Promise对象,await后的返回值是promise成功的值
  • 如果表达式是其他值,直接将此值作为await的返回值

注意:await必须写在async函数中,但async函数中可以没有await,如果await的promise失败了,就会抛出异常,需要通过try..catch来捕获处理

6.2宏队列与微队列

宏队列:'宏任务1','宏任务2'.....

微队列:'微任务1','微任务2'.....

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

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

定时器所指定的回调为宏任务,Promise所指定的回调为微任务

// 经典面试题一道
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')
})

// 运行结果: 1 7 2 3 8 4 6 5 0