JS常见异步编程与MST
1.并发与并行的区别
并发:宏观概念,是指在多任务下,例如任务A,B,在一段时间内,通过任务的切换完成了两个任务,这种情况称之为并发
并行:微观概念,假设CPU中存在两个核心,那么我们就可以同时完成任务A与任务B,这种同时完成多个任务的情况就称之为并行
2.回调函数是什么?有什么缺点?怎么解决回调地狱的问题?
ajax(url, () => {
// 逻辑
})
这就是一个回调函数,但是他有一个致命缺点就是回调地狱,回调地狱常见如下:
ajax(url, () => {
// 逻辑
ajax(url1, () => {
// 逻辑
ajax(url2, () => {
// 逻辑
})
})
})
回调函数的根本问题就是:
- 嵌套函数存在耦合性,嵌套的越深,耦合越强,一旦有所改动,就会牵一发而动全身
- 嵌套函数一多,就很难处理错误
回调函数还有几个缺点:不能使用try...catch来捕获错误,也不能直接使用return
Generator是什么?
Generator是JS中比较难理解的一个概念,他把函数的执行化为了debug一样的感觉,我们可以通过它来控制函数的执行
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
上面的代码解析一下:
- 首先Generator函数调用(函数名前面带一个*号即为Generator函数调用)与普通的函数调用不同,他会返回一个迭代器
- 当执行第一次next的时候,传参会被忽略(这里指的是it.next()的传参),并且函数暂停在yield(x+1)处,所以返回5 + 1 = 6
- 当执行第二次next时,传入的参数等于上一个yield的返回值,如果不传参,yield永远返回undefined。此时let y = 2 *12,所以第二个yield等于2 * 12 / 3 = 8
- 当第三次执行next时,传入的参数会传递给z,所以z = 13,x = 5, y = 24,相加等于42
Generator函数一般也不会用到,而且它一般会配合co库来使用,当然我们可以通过Generator函数解决回调地狱的问题,可以把之前回调地狱的例子改写成如下代码
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
Promise的特点是什么,分别有什么优缺点?什么是Promise链?Promise构造函数函数执行和then函数执行有什么区别?
- 等待中
- 完成了
- 拒绝了
承诺一旦从等待状态变为其他状态,这个过程就不可逆了,状态将不能再次变更。
当我们在构造Promise时,构造函数内部的代码是立即执行的
new Promise((resolve, reject) => {
console.log('new Promise')
resolve('success')
})
console.log('finish')
Promise实现了链式调用,也就是说每次调用then之后返回的都是一个Promise,并且是一个全新的Promise,原因也是因为状态不可改变。如果在then中使用了return,那么return的值会被Promise.resolve()包装
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成resolve(2)
})
.then(res => {
console.log(res) // => 2
})
Promise很好地解决了回调地狱的问题
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
})
.then(res => {
consoloe.log(res)
return ajax(url2)
}).then(res => {console.log(res})
Promise的缺点
- 无法取消Promise
- 错误需要通过回调函数捕获
async与await的特点,他们的优点和缺点分别是什么?await的原理是什么?
一个函数如果加上一个async,那么该函数就会返回一个Promise
async function test() {
return '1'
}
console.log(test()) // -> Promise {<resolved>: '1'}
async就是讲函数返回值用Promise.resolve()包裹了一下,和then中处理返回值一样,并且await只能配套async使用
async function test() {
let value = await sleep()
}
async和await可以说是异步终极解决方案了,相比直接使用Promise来说,优势在于处理then的调用链,能够更清晰准确的写出代码,毕竟写上一大堆then也非常恶心,并且也能够优雅的解决回调地狱问题。最大的缺点就是await将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await会导致性能上的降低。
async function test() {
// 如果下面的代码互相没有依赖性,完全可以使用Promise.all的方式
// 如果有依赖性,其实就是解决回调地狱的例子
await fetch(url)
await fetch(url1)
await fetch(url2)
}
看一个使用await的例子
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a)
}
b()
a++
console.log('1', a) // -> '1' 1
以上代码的疑点在于
- 首先函数b先执行,在执行到await 10之前变量a还是0,因为await内部实现了generator,generator会保留堆栈中的东西,所以这个时候a=0就被保存下来
- 因为await是异步操作,后来的表达式不返回Promise的话,就会包装成Promise。resolve(返回值),然后回去执行函数外面的同步代码
- 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,,就是a = 0 + 10
上述解释中提到了await内部实现了generator,其实await就是generator加上Promise的语法糖,且内部实现了自动执行generator。
常用定时器函数(setTimeout, setInterval, requestAnimationFrame各有什么特点?)
异步编程当然少不了定时器,常见的定时器函数有setTimeout,setInterval,requestAnimationFrame.我们先来讲讲最常用的setTimeout,很多人认为setTimeout延时多久,就应该是多久之后运行,其实这是不对的,因为js是单线程执行的,如果前面的代码影响了性能,就会导致setTimeout不会按期执行,当然可以通过一些方法来让他相对准确
接下来来看setInterval,其实这个函数作用和setTimeout基本一致,只是该函数是每隔一段时间执行一次回调函数
通常不建议使用setInterval。第一,他和setTimeout一样,不能保证在预期时间执行任务。第二,它存在执行积累的问题
function demo() {
setInterval(function() {
console.log(2)
}, 1000)
sleep(2000)
}
demo()
以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题
如果有循环定时器的需求,完全可以通过requestAnimationFrame来实现
function setInterval(callback, interval) {
let timer
const now = Date.now
let startTime = now()
let endTime = startTime
const loop = () => {
timer = window.requestAnimationFrame(loop)
endTime = now()
if(endTime -startTime >= interval) {
startTime = endTime = now()
}
}
timer = window.requestAnimationFrame(loop)
return timer
}
let a = 0
setInterval(timer => {
console.log(1)
a++
if(a === 3) cancelAnimationFrame(timer)
}, 1000)
首先requestAnimationFrame自带函数节流功能,基本可以保证在16.6ms内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现setTimeout。
手写Promise
简易版Promise
在完成符合Promise/A+规范的代码前,先实现一个简易版Promise
// Promise的三个状态
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
function myPromise(fn) {
const that = this
that.state = PENDING
that.value = null
that.resolvedCallbacks = []
that.rejectedCallbacks = []
// 待完善resolve和reject函数
// 待完善执行fn函数
}
- 首先创建三个常量用于表示状态
- 在函数体内部创建常量that,因为代码可能会异步执行,用于获取正确的this对象
- 一开始Promise的状态应该是pending
- value变量用于保存resolve或者reject中传入的值
- resolvedCallback和rejectedCallback用于保存then中的回调,因为当执行完Promise时,状态可能还是等待中,这时候应该吧then中的回调保存起来用于状态改变时使用
接下来我们完善resolve和reject函数,添加在MyPromise函数体内部
function resolve (value) {
if (that.state === PENDING) {
that.state = RESOLVED
that.value = value
that.resolvedCallbacks.map(cb => cb(value))
}
}
function reject(value) {
if (that.state === PENDING) {
that.state = REJECTED
that.value = value
that.rejectedCallbacks.map(cb => cb(value))
}
}
- 首先两个函数都要判断当前状态是否为等待中,因为规范规定只有等待态才可以改变状态
- 将当前状态更改为对应状态,并且将传入的值赋值给value
- 遍历回调数组并执行
完成以上两个函数以后,我们就该实现如何执行Promise中传入的函数了
try {
fn(resolve, reject)
} catch (e) {
reject(e)
}
- 实现起来很简单,执行myPromise传入的参数fn,并且把刚刚定义的两个函数当做参数传进去
- 要注意的是,在执行函数的过程中可能会出现错误,需要捕获错误并且执行reject函数
最后来看看then函数
MyPromise.prototype.then = function (onFulfilled, onRejected) {
const that = this
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function'
? onRejected
: r => {
throw r
}
if (that.state === PENDING) {
that.resolvedCallbacks.push(onFulfilled)
that.rejectedCallbacks.push(onRejected)
}
if (that.state === REJECTED) {
onFulfilled(that.value)
}
if (that.state === REJECTED) {
onRejected(that.value)
}
}
-
首先判断两个参数是否为函数类型,因为这两个参数是可选参数
-
当参数不是函数类型时,需要创建一个函数赋值给对应的参数,同时也实现了透传,比如如下代码
// 下面代码在简单版本的Promise会报错 // 只是作为一个透传的例子 Promise.resolve(4).then().then((value) => console.log(value))
-
接下来就是一系列判断状态的逻辑,当状态不是等待态时,就去执行相对应的函数。如果状态是等待态的话,就往回调函数中push函数,比如如下代码就会进入等待态的逻辑
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
}).then(value => {
console.log(value);
})