# 同步与异步

在学习 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');
})