7.JS- 异步编程

279 阅读10分钟

《深入理解 JavaScript 异步》

《深入掌握 ECMAScript 6 异步编程》

★ 回调函数(Callback)

涉及面试题:什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?

什么是回调函数

就是一个函数以传参的方式传给另一个函数调用 那么这个函数就叫做是回调函数

以下代码就是一个回调函数的例子:

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

回调函数有什么缺点

  • 1.容易写出回调地狱
  • 2.不能使用 try catch 捕获错误,
  • 3.不能直接 return

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

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

假设多个请求存在依赖性,你可能就会写出如下代码:

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

如何解决回调地狱问题?

  • Generator
  • Promise
  • async await
  • 常用定时器函数

接下来学习解决回调地狱的办法


★ Generator

涉及面试题:你理解的 Generator 是什么?

概念

Generator 函数是 ES6 提供的一种异步编程解决方案

特点

Generator 最大的特点就是可以控制函数的执行

形式上理解

形式上,Generator 函数是一个普通函数,但是有两个特征。

  • 1、function关键字与函数名之间有一个星号;
  • 2、函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

执行上理解

执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

如何调用

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的不是函数运行结果,而是一个指向内部状态的指针对象,就是遍历器对象(Iterator Object)。

调用:执行带*号的函数,得到的是生成器对象,只有手动调用next()方法,这个函数的函数体才会开始执行

执行:调用next传入的参数,会作为yeild执行的返回值

异常:generator.throw() 带*的函数里面执行异常使用try catch捕获

如何执行 (next)

必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

【简单理解一下,就是: next 继续;yield暂停】

第一次执行 暂停到第一个yield里面,返回第一个yield里面的结果

第二次执行 暂停到第二个yield里面,返回第二个yield里面的结果

第三次执行 没有yield就执行完了

所以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}


// 第一次next
//console.log(it.next())  => {value: 6, done: false} 
//console的value是6,因为返回的是第一个yield 里面的值 x+1 5+1=6,暂停到yield里面


// 第二次next
//console.log(it.next(12))  => {value: 8, done: false}
//因为这个时候返回的是第二个yield,就是yield (y / 3);
//但是因为传的参数是12,12等于第一个yield的值,12=(yield (x + 1)  
//所以 y=2*12 =24 所以 yield (y / 3) = 24/3=8

//第三次next
//console.log(it.next(13))  => {value: 42, done: true}
//13 = yield (y / 3);
//z=13


第一个参数x=5;
y=24;上面算出来的
x+y+z = 5+24+13=42



/////   继续    ////////
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}
console.log(it.next()) // => {value: undefined, done: true}


let ik = foo(5)
console.log(ik.next()) // => {value: 6, done: false}
console.log(ik.next())  // => {value: NaN, done: false}


let ia = foo(5)
let ib = ia.next()
console.log('ib',ib) // {value: 6, done: false}

let ic = ia.next(ib.value)
console.log('ic',ic) //{value: 4, done: false}

let id = ia.next(ic.value)
console.log('id',id) //{value: 21, done: true}  x5+y12+z4 = 21

你也许会疑惑为什么会产生与你预想不同的值,接下来就让我为你逐行代码分析原因

  • 首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,就是yield (x + 1)=12,如果你不传参,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 链?Promise 构造函数执行和 then 函数执行有什么区别?

三种状态

Promise有如下三种状态,状态变成为其他状态就永远不能更改状态

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)
new Promise((resolve, reject) => {
  resolve('success')
  // 无效
  reject('reject')
})

立即执行

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

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})

image.png

链式调用

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))

前面都是在讲述 Promise 的一些优点和特点,其实它也是存在一些缺点的,比如无法取消Promise,错误需要通过回调函数捕获。


★ async await

涉及面试题:async 及 await 的特点,它们的优点和缺点分别是什么?await 原理是什么?

一个函数如果加上 async ,那么该函数就会返回一个 Promise,可以使用then啦

async function test() {
  return "1"
}
let ret = test()
console.log(ret) // Promise {<fulfilled>: '1'}
ret.then(res => console.log(res))  //1

await 只能配套 async 使用

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)
}

举例

let a = 0
let b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10 后输出这里
}
b()
a++
console.log('1', a) // -> '1' 1  先执行同步的 a++ 

image.png

对于以上代码你可能会有疑惑,让我来解释下原因

  • 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  • 因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
  • 同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10

上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。

原理

zhuanlan.zhihu.com/p/115112361


★ Promise、Generator、Async三者的区别

【最开始promise解决了回调地狱,但是还是一堆then,代码不好看,又用async await解决这个问题,原理主要是Generator,也是es6提出来的新写法,async 对应的是 * ,await 对应的是 yield。】

一个函数加上 async ,那么该函数就会返回一个 Promise,可以使用then了

Async 是 Generator 的一个语法糖。

async 对应的是 * 。

await 对应的是 yield 。

async/await 自动进行了 Generator 的流程控制。

自己思考总结:首先先说Generator,他就是es6提出了一个针对异步编程的解决方案,使用的时候Function跟函数名字中间有个星号*,函数内部使用yield关键字,执行的时候调用next方法,而,我们的async就是相当于generator里面的星号* await相当于yield;那么async await当初其实是为了解决promise点.then,觉得.then太多了代码也不好看,所以是解决这个问题嘛,然后async函数会返回Promise对象,而await呐就相当于 点.then,所以说如果async函数报错的话,await是拿不到的,就要再外面包裹一层try catch,因为promise里面的reject跑错,只有.catch可以捕获到,然后再promise的链式调用里面,.catch里面return的再.then里面也可以拿到,因为.then和.catch返回的都是promise对象

then/catch方法可以返回一个新的promise实例对象。then/catch方法指定的回调函数(执行)return的返回值可以决定这个新的promise实例对象的状态,这正是实现then方法链式调用的基础。


let a = new Promise((resolve, reject) => {
  resolve('success')  
  reject('error') 
}).then((ret)=>{
    console.log(ret) //2
    return ret
}).then((ret)=>{
    console.log(ret) //3
}).catch((ret)=>{
    console.log(ret) 
    return 'kkkk'
}).then((ret)=>{
    console.log(ret) //4
})

console.log(a) //1


//Promise {<pending>}  1
//success              2
//success              3 
//undefined            4



let a = new Promise((resolve, reject) => {
//   resolve('success')
  reject('error')
}).then((ret)=>{
    console.log(ret)
    return ret
}).then((ret)=>{
    console.log(ret)
}).catch((ret)=>{
    console.log(ret)    //2
    return 'kkkk'
}).then((ret)=>{
    console.log(ret)   //3
})

console.log(a)  //1




Promise {<pending>}  //1
error                //2
kkkk                 //3



★ 常用定时器函数


★ 并发(concurrency)和并行(parallelism)区别

并发

并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。

并行

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