JS异步编程
并发和并行区别
并发是宏观概念,现在分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
回调函数(Callback)
什么是回调函数(Callback)
其实回调函数并不复杂,明白两个重点即可:
- 函数可以作为一个参数传递到另一个函数中。
- JS是异步编程语言。
为什么需要回调函数?
JavaScript 按从上到下的顺序运行代码。但是,在有些情况下,必须在某些情况发生之后,代码才能运行(或者说必须运行),这就不是按顺序运行了。这是异步编程。
回调函数确保:函数在某个任务完成之前不运行,在任务完成之后立即运行。它帮助我们编写异步 JavaScript 代码,避免问题和错误。
在 JavaScript 里创建回调函数的方法是将它作为参数传递给另一个函数,然后当某个任务完成之后,立即调用它。
回调函数有个致命的弱点,就是容易写出回调地狱。
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
回调地狱的根本问题就是:
- 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
- 嵌套函数一多,就很难处理错误
- 不能使用 try catch 捕获错误
- 不能直接 return
Generator
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。
function
关键字与函数名之间有一个*
;- 函数体内部使用
yield
表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
yield
由于 Generator 函数返回的遍历器对象,只有调用 next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的 next
方法的运行逻辑如下:
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。 - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。
next
next
方法可以带一个参数,该参数就会被当作上一个 yield
表达式的返回值。
function* foo(x) {
const y = 2 * (yield (x + 1));
const z = yield (y / 3);
return (x + y + z);
}
const a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
代码详解:
- 第二次运行
next
方法的时候不带参数,导致y
的值等于2 * undefined
(即NaN
),除以 3 以后还是NaN
,因此返回对象的value
值也等于NaN
。 - 第三次运行
next
方法的时候不带参数,所以z
等于undefined
,返回对象的value
值等于5 + NaN + undefined
,即NaN
。
function* foo(x) {
const y = 2 * (yield (x + 1));
const z = yield (y / 3);
return (x + y + z);
}
const b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
代码详解:
- 当执行第一次
next
时,函数暂停在yield (x + 1)
处,所以返回5 + 1 = 6
; - 当执行第二次
next
时,将上一次yield
表达式的值设为12
,此时const y = 2 * 12
,所以第二个yield
等于2 * 12 / 3 = 8
; - 当执行第三次 next 时,将上一次
yield
表达式的值设为13
,所以z = 13
,x = 5
,y = 24
,相加等于42
。
TIP
由于 next
方法的参数表示上一个 yield
表达式的返回值,所以在第一次使用 next
方法时,传递参数是无效的。V8 引擎直接忽略第一次使用 next
方法时的参数,只有从第二次使用 next
方法开始,参数才是有效的。从语义上讲,第一个 next
方法用来启动遍历器对象,所以不用带有参数。
用 Generator
解决回调地狱如下:
function *fetch() {
yield ajax(url, () => {console.log('这里是首次回调函数');});
yield ajax(url, () => {console.log('这里是第二次回调函数');});
yield ajax(url, () => {console.log('这里是第三次回调函数');});
}
const it = fetch();
const result1 = it.next();
const result2 = it.next();
const result3 = it.next();
Promise
Promise 是异步编程的一种解决方案。
new Promise((resolve, reject) => {})
Promise对象代表一个异步操作,有三种状态:
- pending(进行中)
- fulfilled(已成功)
- rejected(已失败)
特点:
- 对象的状态不受外界影响
- 一旦状态改变就不会再变,任何时候都可得到这个结果
方法:
then():分别指定
resolved
状态和rejected
状态的回调函数- 第一参数:状态变为
resolved
时调用 - 第二参数:状态变为
rejected
时调用(可选)
- 第一参数:状态变为
catch():指定发生错误时的回调函数
finally():用于指定不管
Promise
对象最后状态如何,都会执行的操作。Promise.all():将多个实例包装成一个新实例,返回全部实例状态变更后的结果数组(齐变更再返回)
入参:具有
Iterator
接口的数据结构成功:只有全部实例状态变成
fulfilled
,最终状态才会变成fulfilled
失败:其中一个实例状态变成
rejected
,最终状态就会变成rejected
var p1 = Promise.resolve(1); var p2 = new Promise((resolve) => { setTimeout(() => { resolve(2); }, 100); }) var p3 = 3; Promise.all([p1,p2,p3]).then((res) => { console.log(res); // 输出[1,2,3] })
Promise.race():将多个实例包装成一个新实例,返回全部实例状态优先变更后的结果(先变更先返回)
- 入参:具有
Iterator
接口的数据结构 - 成功失败:哪个实例率先改变状态就返回哪个实例的状态
- 入参:具有
Promise.resolve():将对象转为Promise对象(等价于
new Promise(resolve => resolve()))
Promise.reject():将对象转为状态为
rejected
的Promise对象(等价于new Promise((resolve, reject) => reject()))
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
当我们在构造 Promise
的时候,构造函数内部的代码是立即执行的
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
console.log('end')
})
console.log('finish')
// 打印顺序
// new Promise => end => finish
Promise
实现了链式调用,也就是说每次调用 then
之后返回的都是一个 Promise
,并且是一个全新的 Promise
,原因也是因为状态不可变。如果你在 then
中 使用了 return
,那么 return
的值会被 Promise.resolve()
包装
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
Promise
解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))
总结
- 只有异步操作的结果可决定当前状态是哪一种,其他操作都无法改变这个状态???
- 状态改变只有两种可能:从
pending
变为resolved
、从pending
变为rejected
- 一旦新建
Promise
对象就会立即执行,无法中途取消 - 建议使用
catch()
捕获错误,不要使用then()
第二个参数捕获 then()
返回新实例,其后可再调用另一个then()
then()
运行中抛出错误会被catch()
捕获resolve()
和reject()
的执行总是晚于本轮循环的同步任务- 实例状态的错误具有
冒泡
性质,会一直向后传递直到被捕获为止,错误总是会被下一个catch()
捕获 Promise
中的错误是不会影响外层的运行,window.onerror
也是无法检测到的
async及await
一个函数如果加上 async
,那么该函数就会返回一个 Promise
async function test() {
return '1'
}
console.log(test())
打印结果如下:
async
就是将函数的返回值使用 Promise.resolve()
包裹了一下,和 then
中处理返回值一样,并且 await
只能配套 async
使用
async function test() {
const value = await 3 // await 后面跟的不是 Promise 的话,就会包装成 Promise.resolve(返回值)
}
async
和 await
可以说是异步终极解决方案了,相比直接使用 Promise
来说,优势在于处理 then
的调用链,能够更清晰准确的写出代码,毕竟写一大堆 then
也很恶心,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await
将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await
会导致性能上的降低。
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch(url)
await fetch(url1)
await fetch(url2)
}
常用定时器函数
常见的定时器函数有 setTimeout、setInterval、requestAnimationFrame。
setTimeout
setTimeout
延时执行某一段代码,但setTimeout
由于EventLoop的存在,并不百分百是准时的,一个setTimeout
可能会表示如下的形式:
// 延时1s之后,打印hello,world
setTimeout(() => {
console.log('hello,world');
}, 1000)
setInterval
setInterval
在指定的时间内,重复执行一段代码,与setTimeout
类似,它也不是准时的,并且有时候及其不推荐使用setInterval
定时器,因为它与某些耗时的代码配合使用的话,会存在执行积累的问题,它会等耗时操作结束后,一起一个或者多个执行定时器,存在性能问题。一个setInterval
可能会表示如下的形式:
setInterval(() => {
console.log('hello,world');
}, 1000)
requestAnimationFrame
翻译过来就是请求动画帧,它是html5专门用来设计请求动画的API,它与setTimeout
相比有如下优势:
- 根据不同屏幕的刷新频率,自动调整执行回调函数的时机。
- 当窗口处于未激活状态时,
requestAnimationFrame
会停止执行,而setTimeout
不会 - 自带函数节流功能
let progress = 0;
let timer = null;
function render() {
progress += 1;
if (progress <= 100) {
console.log(progress);
timer = window.requestAnimationFrame(render);
} else {
cancelAnimationFrame(timer);
}
}
//第一帧渲染
window.requestAnimationFrame(render);