学习笔记(一)

234 阅读8分钟

JS常见异步编程与MST

1.并发与并行的区别

并发:宏观概念,是指在多任务下,例如任务A,B,在一段时间内,通过任务的切换完成了两个任务,这种情况称之为并发

并行:微观概念,假设CPU中存在两个核心,那么我们就可以同时完成任务A与任务B,这种同时完成多个任务的情况就称之为并行

2.回调函数是什么?有什么缺点?怎么解决回调地狱的问题?

ajax(url, () => {
  // 逻辑  
})

这就是一个回调函数,但是他有一个致命缺点就是回调地狱,回调地狱常见如下:

ajax(url, () => {
  // 逻辑
    ajax(url1, () => {
        // 逻辑
        ajax(url2, () => {
          // 逻辑  
        })
    })
})

回调函数的根本问题就是:

  1. 嵌套函数存在耦合性,嵌套的越深,耦合越强,一旦有所改动,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

回调函数还有几个缺点:不能使用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);
    })