JS 异步编程
js 是一门单线程的语言,这个也就意味着,一次只能有一个人干活,所以为了不造成页面的堵塞,js 是异步加载。
笔记点击这里
回调函数的基本概念
function loadImage(src, reslove, reject) {
const image = new Image()
image.src = src
image.onload = reslove(image) // 这时候就相当于是直接同步代码了 因为直接调用了reslove这个方法,就不异步了
image.onload = () => {
reslove(image)
} // 这样就是正常的异步的将src又给传了出去
/*
两个onload的区别 如果使用第一onload 打印结果是
先打印 http://39.96.42.170 之后才打印 图片加载成功了 Image对象
*/
image.onerror = reject
}
loadImage(
'http://39.96.42.170/bg.jpg',
() => {
console.log('图片加载成功了')
},
() => {
console.log('图片加载失败')
}
)
console.log('http://39.96.42.170')
/*
执行结果: 先打印 http://39.96.42.170 之后才打印 图片加载成功了或是图片加载失败
*/
注意点(重点):
- 我们写的回调函数,如果函数里面直接执行了回调函数,那就不异步了
- 正确的方法应该是在函数中嵌套一个函数,再将值传递出去
通过上面的例子就可以很清晰的知道了,因为加载图片相对来说是一个耗时的,由于单线程的特性,如果等到第加载完成了之后再执行后面的逻辑,页面必定是卡顿的,用户体验自然也就不行了,所以 JS 异步就是为了解决这个。
定时器任务轮询案例
JS 中的定时器有两种,分别是setTimeOut()
和setInterval()
这两种,我们发现,使用这两个方法的时候我们都是传递回调函数,这就原因也是很好理解,因为定时是耗时的,不可能去等这个执行完才去执行别的任务,所以就采用回调函数异步的方式来编写的方法
function interval(callback, delay) {
let timeId = setInterval(() => {
callback(timeId) // 通过异步的形式 将timeId 传递出去
}, delay)
}
interval(timeId => {
const div = document.querySelector('div')
let left = div.offsetLeft
div.style.left = left + 10 + 'px'
console.log('left', left)
if (left + div.offsetWidth >= document.body.clientWidth) {
clearInterval(timeId)
interval(timeId => {
let width = div.offsetWidth
div.style.width = width - 10 + 'px'
if (width <= 20) {
clearInterval(timeId)
}
}, 100)
}
}, 50)
/*
这里写的延迟执行时长是50ms 但是实际并不是一定是50ms 这个也是个不确定的因素,会等主线程执行结束了之后,
并且任务队列循环到这个任务之后,等待50ms才执行
如此例,必须主线程for循环执行结束了之后才会执行异步任务队列的任务,而主线程可能就会执行好久
*/
for (let i = 0; i < 20000000; i++) {
console.log(i)
}
复习 DOM:
-
DOM.offsetLeft
DOM.offsetWidth
都是属性,获取一个 DOM 元素的 left 属性值和宽度 -
document.body.clientWidth
获取浏览器窗口的宽度
注意点(重点):
- 定时器上规定的时间并不代表一定过这么久就会执行,还要看主线程代码执行的时间和轮询队列的排列
回调地狱的产生
在 JS 中,主线程是从上到下执行的,遇到加载的时候会交给异步加载模块去进行加载,而异步加载模块就没有说先进先出,后进后出的原则的,它的原则就是谁快,就先出谁。
function aMode() {
console.log('a模块')
}
function bMode() {
aMode()
console.log('bMode 执行了')
}
function load(src, reslove, reject) {
let script = document.createElement('script')
script.src = src
script.onload = reslove
script.onerror = reject
document.body.appendChild(script)
}
load(
'./module/a.js',
() => {
aMode()
},
() => {
console.log('线程加载失败')
}
)
load('./module/b.js', () => {
bMode()
})
load('./')
console.log('主线程')
很明显,模块 b 是依赖与模块 a 执行的,因为执行模块 b 的时候,会执行模块 a 的aMode()
方法,而再看我们主逻辑代码这,虽然我们代码的顺序是先写了加载 a 模块,再加载 b 模块,但是由于异步加载模块的执行顺序并不是先看到谁执行谁,而是谁先执行完就先出谁,所以以上的代码有一定的几率会报错,如图
- 错误加载时
- 正确加载时
所以,为了解决这个加载快慢的因素,我们就只能等一个模块加载完成之后再去加载第二个模块,就是这样,要实现这个就只能在加载成功的回调函数里面去加载其他的模块,这样就能够保证第一个被依赖的模块一定是已经加载完成的
load('./module/a.js', () => {
// 只要走到这个回调里面,就说明一定时已经拿到了值
load('./module/b.js', () => {})
})
Promise 机制
Promise 会将创建的任务是微任务,微任务的重要性是大于宏任务的。也就是说当又有宏任务又有微任务的情况下,浏览器会优先的执行微任务队列里面的任务
new Promise((resolve, reject) => {
resolve('操作成功')
})
.then(
() => {
console.log('成功的回调1')
},
() => {
console.log('失败的回调')
}
)
.then(() => {
console.log('成功的回调2')
})
知识点:
- 从结构上,解决了回调地狱,代码的结构不再是向右边倾斜的,而是从上到下的,并且执行的逻辑也是从上到下的
- Promise 的
then(callback1,callback2)
里面的两个回调分别是成功的回调和失败的回调,即是resolve()
和reject()
处理的结果的返回,可以接收到对应返回的参数 - 浏览器的执行顺序 主线程>微任务>宏任务
主线程、微任务、宏任务执行顺序
执行顺序前面已经学习过了,就是主线程>微任务>宏任务
这里的例子可以想象成主线程是至尊 VIP,微任务是 VIP,宏任务就只是普通顾客,即使排队排在最前面,也得给 VIP 让路,让 VIP 先体验
<script>
setTimeout(()=>{
console.log('宏任务 setTimeout')
},0)
new Promise((resolve,reject)=>{
resolve('微任务 Promise')
console.log('Promise的结构体中也是主线程')
})
console.log('主线程')
</script>
// 执行输出结果:Promise的结构体中也是主线程、主线程、微任务 Promise、宏任务 setTimeout
知识点(细节)
- 执行顺序:主线程>微任务>宏任务
- Promise 的函数体也是主线程,并不是 Promise 的所有都是微任务,只有他
resolve()
或reject()
的回调部分才是微任务 - 即使
setTimeou()
的延迟时间是 0,并且写在最前面,也是得等主线程和微任务执行完了之后才会去执行 - Promise 的状态是单向的,不可逆的,一旦触发了一个状态之后就不可以进行更改状态了
Promise 的 then()方法细节
Promise()的 then()方法返回的值默认也是一个 Promise
new Promise((resolve, reject) => {
resolve('请求成功1')
})
.then(
res => {
return '请求成功' // 只要then 里面有return 结果就可以继续使用then来接收
},
err => {}
)
.then(res => {
console.log('kkk', res)
})
// 执行结果 kkk 请求成功
- 只要 then 里面有 return 结果就可以继续使用 then 来接收,不论是第一个成功的回调的 return 还是 第二个失败的回调的 return 只要在 then 一下就可以拿到那个 return 的值
new Promise((resolve, reject) => {
resolve('请求成功1')
})
.then(
res => {
return new Promise((resolve, reject) => {
reject('失败了')
})
},
err => {}
)
.then(null, err => {
console.log('失败的结果', err)
})
//6-Promise的then.html:31 失败的结果 失败了
- 正常的使用,then 回调里面 return 一个新的 Promise,这样因为 return 的是一个 Promise,所以我们就可以继续的链式的在后面继续一直的 then 下去了
Promise 多种错误监听方式与 catch 总监听
Promise 的 resolve 和 reject 状态分别可以在 then 方法的两个回调函数中捕获,但是除此之外还可以使用 catch()函数中来进行总获取
let p1 = new Promise((resolve, rejetc) => {
// resolve('处理成功')
rejetc('处理失败')
})
p1.then(res => {
return new Promise((resolve, reject) => {
reject('error')
})
}).catch(err => {
console.log('catch总接收', err)
})
// 打印结果:catch总接收 处理失败
知识点:
- catch 是对整个 promise 流的一个总的错误监听,如果在 then 中没有写第二个错误处理的函数,那么这个错误会走到 catch 这里
- 因为 catch 是可以实现对整个 Promise 流的监听错误,所以 catch 正常情况下都是写在最后面的
- 如果一个 promise 的 then 中有对错误的处理,那么执行的顺序是会选择走自己的错误处理,原因也很好理解,自己有错误处理就用自己的,没有就用公用的
let p2 = new Promise((resolve, reject) => {
jimmy()
// try {
// jimmy()
// } catch (error) {
// reject(error)
// }
})
p2.then(
res => {
console.log(res)
},
err => {
console.log(err.message)
}
)
// 输出结果:jimmy is not defined
的确是存在逻辑错误,调用了一个没有定义的函数,但是我们会发现自动的走到了错误处理函数上,我们可以理解是在 promise 内部封装了我们看不见的 try-catch 模块,进入 catch 的时候会自动的 reject 出来错误信息
小案例 – 使用 Promise 封装一个 ajax
使用 ajax 就避免不了回调地狱,所以使用 Promise 来封装一下 ajax 是一个优解,axios 就是通过 Promise 封装得来的
// 要有面向对象思想,万物皆可封装成为一个我们自己的东西
class HttpError extends Error {
constructor(msg) {
super(msg) // 这里的msg 会被默认放在 message 属性里面
this.name = '接口错误'
}
}
class ParamsError extends Error {
constructor(msg) {
super(msg)
this.name = '参数错误'
}
}
function ajax(url) {
return new Promise((resolve, reject) => {
if (!/^https?/.test(url)) {
reject(new ParamsError('请求地址错误'))
}
let xml = new XMLHttpRequest()
xml.open('GET', url)
xml.send()
xml.onload = () => {
console.log('kkk', xml)
// 箭头函数this指向的是 调用这个函数对象的this
if (xml.status == 200) {
resolve(xml.response)
} else if (xml.status == 404) {
reject(new HttpError('请输入正确的接口'))
} else {
reject('加载失败')
}
}
})
}
ajax('https://jsonplaceholder.typicode.com/posts')
.then(res => {
console.log(res)
})
.catch(err => {
// instanceof 能够判断该对象对象是从哪个类实例出来的
/*
最典型的就是可以用来判断是对象还是数组
*/
if (err instanceof ParamsError) {
console.log(err)
} else if (err instanceof HttpError) {
alert(err)
}
})
总结:
- 这个案例虽小,但是涉及到的东西还是很多的,如创建自己的错误类,箭头函数的指向,创建基础的 ajax,
instanceof
关键字的使用 - Array 是继承 Object 类的 object 对象是通过实例化 Object 实现的,所以我们呢可以通过 对象是否是通过 Array 实例化出来的 来判断是否数组还是普通对象
finally 关键名
finally 表示无论 promise 请求的状态是成功还是拒绝都会执行的的代码块,就十分的类似 try-catch-finally
- 这个关键字可以做很多的操作,一个比较常用的操作就是可以封装一个加载动画,每次执行异步请求的时候显示这个动画,finally 的时候将这个动画给取消掉。
封装定时器
function timeout(delay) {
return new Promise((resolve, reject) => {
setTimeout(resolve, delay)
})
}
// 使用promise 不会造成会回调地狱 是一个竖装从下到下的执行过程 整个过程十分清晰
timeout(2000)
.then(() => {
console.log('使用Promise')
return timeout(2000)
})
.then(() => {
console.log('封装的定时器')
})
// 使用原生setTimeout 容易造成回调地狱 不好处理
setTimeout(() => {
console.log('不封装')
setTimeout(() => {
console.log('回调地狱')
}, 2000)
}, 2000)
封装轮询器
function interval(delay, callback) {
return new Promise((resolve, reject) => {
let id = setInterval(() => {
callback(id, resolve)
}, delay)
})
}
// 一旦理解了callback的使用 就更能理解promise的使用了 这样封装之后将setInter
interval(1000, (id, resolve) => {
console.log('hello wolrd')
clearInterval(id)
resolve('封装成功')
}).then(res => {
console.log('promise', res)
})
Promise.resolve 与 Promise.reject 接口
Promise.resolve()
是 promise 提供的一个直接获取成功状态的接口,使用的点就在于不用写上完整的 promise
Promise.reslove('success').then(res => {
console.log(res) // success
})
Promise.reject('error').then(null, err => {
console.log(err) // error
})
new Promise((reslove, reject) => {
reslove('成功a')
})
.then(res => {
if (res != '成功') {
throw new Error('不为成功!')
}
})
.catch(err => {
console.log('anser1', err)
})
// Promise.reject 的用途在于可以直接返回一个失败的额状态, 我们可以再第二个回调或者错误中捕获到错误信息
new Promise((resolve, reject) => {
resolve('成功b')
})
.then(res => {
if (res != '成功') {
// 当不是我们想要的状态 我们可以直接使用Promise.reject返回一个错误状态
return Promise.reject('不为成功')
}
})
.catch(err => {
console.log('anser2', err)
})
- 这两个接口不是必须的,如果没有这两个 Promise 也是照样使用
- 用途也有,比如缓存数据,当向后端获取一个基本不变的数据的时候,就可以判断是否缓存中有数据,如果有就直接走
Promise.resolve(缓存数据)
这样就可以直接拿到数据。
Promise.all()
Promise.all()
Promise 提供的一个批量处理 Promise 的一个接口,
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败1')
}, 1000)
}).then(null, err => {
console.log('接收到了失败')
// 因为对失败进行了处理 所以默认会返回一个成功状态的promise
})
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功2')
}, 1000)
})
Promise.all([promise1, promise2])
.then(res => {
console.log('success', res)
})
.catch(err => {
console.log('error', err)
})
// success (2) ["成功1", "成功2"]
-
promise.all()
参数是传递一个数组 - 这个数组每个成员都是一个 Promise,只有数组的 Promise 的状态都是成功的状态 才会走到成功的回调 - Promise 无论是成功的处理还是失败的处理 默认返回的都是一个成功的状态
Promise.allSettled()
Promise.allSettled()
也是一个用来批量处理 Promise 的一个接口
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败了')
}, 1000)
})
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功了')
}, 1000)
})
Promise.allSettled([promise1, promise2]).then(res => {
console.log('最终的数据', res)
/*
(2) [{…}, {…}]
0: {status: "rejected", reason: "失败了"}
1: {status: "fulfilled", value: "成功了"}
length: 2
__proto__: Array(0)
*/
})
-
Promise.allSteeled()和Promise.all()
的却别在于,前者返回的状态永远都是成功的状态 -
Promise.allSettled()
参数传递的也是一个 Promise 数组,无论是成功还是失败的状态,都会走到 then 里面 -
Promise.allSettled()
返回的是一个对象数组,其中对象包含了 status:状态,reson:失败原因,value 成功的值
Promise.race()
Promise.race()
传递一个数组 数组里面的 Promise 哪个快就会先执行哪个
// 封装带有请求超时处理的函数
function query(url, delay = 3000) {
let promise1 = axios.get(url)
let promise2 = new Promise((resolve, reject) => {
setTimeout((resolve, reject) => {
reject('请求超时')
}, delay)
})
return Promise.race([promise1, promise2])
}
query('http://127.0.0.1:666')
.then(res => {
console.log('请求结果', res)
})
.catch(err => {
console.log('请求超时了请重试')
})
async await 语法糖
async
`await`是 promise 提供的语法糖,本质上也是一个 promise
注意点:
- 一个 function 如果加上 async 关键字修饰之后,调用这个函数,这个函数会默认返回一个解决状态的 promise,原理是因为 Pormise 的无论是 then 还是 catch 都是会默认返回一个解决状态的 Promise
async 本质
async function promise() {}
console.log(promise()) // Promise {<fulfilled>: undefined}
function normal() {
return Promise.resolve()
}
console.log(normal()) // Promise {<fulfilled>: undefined}
- async 链式操作
async function Promise2() {
let res = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('请求成功')
}, 2000)
})
/*
await 默认会返回一个 promise 就可以继续一直的 await 下去 虽然是本质上是异步 但是代码的执行顺序是同步执行的
*/
let res2 = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('请求成功2')
}, 2000)
})
console.log('结果', res2)
}
Promise2()
// 过4秒会输出 结果 请求成功2
使用async
await
语法糖的好处在于 和传统的 Promise 相比 我们可以不用一直写 .then()这样的回调函数, 要获取值直接一个 await 来接收返回值即可
async、await 监听错误
- 函数只要加上了 async 关键字之后,默认就是会返回一个 promise,因为,我们在调用函数的时候就可以使用 catch 来进行捕获
- 在 async 内部如果想监听 await 部分的错误,就需要使用传统的 try-catch 模块来进行监听错误
async function promise1() {
console.log(aaa)
}
promise1().catch(err => {
console.log('错误原因', err.message) //错误原因 aaa is not defined
})
/*
因为 加上async关键字之后 函数的返回值变成了一个promise, promise就可以使用catch来捕获错误
*/
async function query() {
try {
let lesson = await axios.post('http://127.0.0.1:666')
} catch (error) {
console.log('await 错误原因', error.message)
}
}
query()
/*
await 内部监听错误就可以使用传统的 try-catch模块进行监听模块
*/