详解异步编程

我们知道JavaScript语言的执行环境是单线程,也就是一次只能完成一个任务。如果有多个任务就必须排队,前面一个任务完成,再执行后面的一个任务

这种模式虽然实现起来简单,执行环境相对单纯,但是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的有的浏览器无响应(假死,往往就是因为某一段JS代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,JavaScript语言将任务的执行模式分成两种:同步和异步。下面主要介绍异步编程的几种方法:

回调函数(Callback)

回调函数是异步操作的最基本的方法
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。

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

但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设有多个请求存在依赖

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

回调地狱的根本问题就是:

嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
嵌套函数一多,就很难处理错误
当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。在接下来的几小节中,我们将来学习通过别的技术解决这些问题。

定时器

setTimeout比较常用,很多人认为setTimeout是延时多久,那就应该是多久后就执行
其实这个观点是错误的,因为JS是单线程执行的,如果前面的代码影响了性能,就会导致setTimeout不会按时执行

Promise/A+

Promist本意是承诺的意思,在程序中的意思就是承诺我过一段时间后会给你一个结果,什么时候会用到异步事件?答案是异步操作,异步操作指可能比较长时间才有结果的才做,比如网络请求,读取本地文件.

Pormise的三种状态

  • 等待中(pending)
  • 完成了(resolved)
  • 拒绝了(rejected)

这个承诺一旦从等待状态变成了其他状态就永远不能更改状态,也就是说一旦状态变成了resolved后,就不能再次改变

new Promise((resolve, reject) => {
  resolve('success')
  // 无效
  reject('reject')
})

当我们在构造Promist的时候,构造函数内部的代码是立即执行的

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('finifsh')
// new Promise -> finifsh
Promise的链式调用
  • 每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
  • 如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
  • 如果then中出现异常,会走下一个then的失败回调
  • 如果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
  })
  • then中可以不传递参数,如果不传递参数会透到下一个then中
  • catch会捕获到没有捕获的异常

Promise也很好的解决了回调地狱的问题

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

其实它也是存在一些缺点的,无法取消Promise,错误需要通过回调函数捕获

生成器Generators/yield

Generator函数是ES6中提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator最大的特点就是可以控制函数的执行.

  • 语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
  • Generator函数除了状态机,还是一个遍历器对象生成函数
  • 可暂停函数,yield可暂停,next方法可启动,每次返回的是yield后的表达式结果
  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当做是上一个yield表达式的返回值.

我们先看一个列子

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函数调用和普通函数不同,它会返回一个迭代器
  • 当执行第一次next时,传参会被忽略,并且函数暂停在yield(x+1)处,所以返回5+1=6
  • 当执行第二次next时,传入的参数等于上一个yield的返回值,传入的参数12就会被当做上一个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()

async/await

特点:

  • async/await是基于Promise实现的,它不能用于普通的回调函数
  • async/await与Promise是一样的,是非阻塞的
  • async/await是的异步代码看起来像同步代码

如果一个函数加上async,那么这个函数就会返回一个Promise

async function test() {
  return "1"
}
console.log(test()) // -> Promise {: "1"}

async就是将函数返回值使用Promise.resolve()包裹了下,和then中处理返回值一样,并且await只能配合async使用.

async function test() {
  let value = await sleep()
}

async/await可以说是异步终极解决方案,相比直接使用Promise来说,优势在于处理then的调用链,并且也能够优雅地解决回调地狱问题。

当然也有一些缺点,因为await将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await会导致性能上的降低

async function test() {
  // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
  // 如果有依赖性的话,其实就是解决回调地狱的例子了
  await fetch(url)
  await fetch(url1)
  await fetch(url2)
}

async/await函数对Generator函数的改进体现:

  • 内置执行器,Generator函数的执行必须靠执行器,所以才有了co函数库,而async函数自带执行器。也就是说async函数的执行与普通函数一模一样,只要一行
  • 更广的适用性,co函数库约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命名后面,可以跟Promise对象和原始类型的值(数值,字符串和布尔值,但这时等同于同步操作)
  • 更好的语义,async和await,比起星号*和yield,语义更清楚了,async表示函数里面有异步操作,await表示紧跟后面的表达式需要等待结果.

你可能感兴趣的:(JavaScript基础)