# 同步与异步
在学习 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。
- 调用 resolve 函数会:
//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 为抛出的那个异常
- 如果 then 所指定的回调返回的是非 Promise 值 a:
# 如何中断 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'); | |
}) |