JS 异步编程

1. Iterator|Generator|await

1.1 Iterator 语法

Iterator 是一个接口(规范),为不同的数据结构提供统一的访问机制
ES6 当中并不存在Iterator 类

1.1.1 Iterator 特点

  1. 任何数据结构只要部署了 Iterator 接口,就可以实现遍历操作,依次处理每个成员
  2. 拥有 next 方法,用于依次遍历数据结构成员
  3. 每一次遍历都会返回一个对象 {done: false, value: xxx}
    1. done 键会记录着当前数据结构是否遍历完成
    2. value 记录着本次遍历拿到的值

1.1.2 Iterator 模拟

class Iterator {
  constructor(arr) {
    this.arr = arr
    this.index = 0
  }
  // next方法用于遍历结构,返回 {}
  next = () => {
    if (this.index > this.arr.length - 1) {
      // 如果说当前数据结构中内容都已遍历完成则不会再遍历
      return {
        done: true,
        value: undefined
      }
    }
    return {
      done: false,
      value: this.arr[this.index++]
    }
  }
}

const arr = [1, 2, 3, 4]
const it = new Iterator(arr)
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())

1.1.3 可遍历结构

虽然 ES6当中并没有内置 Iterator 类,但是有些数据默认拥有 Symbol.iterator 属性,此时数据就被称之为遍历结构,采用 for of 循环处理
Array String Set Map NodeList Arguments 或者是基于 generator 创建的对象...
对象本身并不满足 Iterator 接口规范

const arr = [1, 2, 3, 4, 5, 6, 7, 8]

arr[Symbol.iterator] = function () {
  // 此接口符合 Iterator 规范,因此要返回一个对象,且具备 next 方法 
  let index = 0
  let self = this
  return {
    next() {
      if (index > self.length - 1) {
        return {
          done: true,
          value: undefined
        }
      }
      let ret = {
        done: false,
        value: self[index]
      }
      index += 2
      return ret
    }
  }
}
for (let item of arr) {
  console.log(item)
}

1.1.4 遍历类数组对象

对象默认没有实现 Iterator 接口

const obj = {
  0: '100',
  1: 200,
  2: 300,
  length: 3,
  //[Symbol.iterator]: Array.prototype[Symbol.iterator]
  [Symbol.iterator]: function () {
    let index = 0
    let self = this
    return {
      next() {
        return index > self.length - 1 ? {
          done: true,
          value: undefined
        } : {
          done: false,
          value: self[index++]
        }
      }
    }
  }
}

for (let item of obj) {
  console.log(item)
}

1.1.5 遍历键值对对象

const obj = {
  name: 'zce',
  age: 40,
  gender: '男',
  length: 3,
  [Symbol.iterator]: function () {
    let self = this
    let keys = [
      ...Object.getOwnPropertyNames(self),
      ...Object.getOwnPropertySymbols(self)
    ]
    let index = 0
    return {
      next() {
        return index > self.length - 1 ? {
          done: true,
          value: undefined
        } : {
          done: false,
          value: self[keys[index++]]
        }
      }
    }
  }
}

for (let item of obj) {
  console.log(item)
}

1.2 Generator 语法

生成器对象是由一个 generator function 返回,并且符合可迭代协议和迭代器协议

1.2.1 生成器对象特点

  • 生成器对象,由生成器函数默认返回,且生成器函数不能执行 new 的实例化操作
  • 生成器实例对象的原型身上会存在
    • next 方法,用于遍历对应的值
    • return 结束遍历并且返回 return 指定的值
  • 沿着原型链向上查找可以在原型对象上找到 Symbol.iterator 属性
function* foo() {
  return 100
}

const iterator = foo()
console.log(iterator) // 生成器对象, 由生成器函数默认返回
console.log(iterator instanceof foo)  // 检测生成器对象是否为生成器函数实例对象
console.log(iterator.__proto__ == foo.prototype)  // 生成器对象的显式和隐式原型 
// new foo()  // 生成器函数不能执行 new 操作

1.2.2 yield 基础语法

function* foo() {
  console.log('aa')
  const m = yield 1
  console.log(m, '<----')
  console.log('bb')
  yield 2
  console.log('cc')
  yield 3
  return 100
}

const iterator = foo()
console.log(iterator.next())
console.log(iterator.next(1000))
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

for (let item of iterator) {
  console.log(item)
}

1.2.3 yield 嵌套和this

function* fn1() {
  console.log(this, '<-----')
  yield 1
  yield 2
}

function* fn2() {
  yield 3
  yield* fn1()
  yield 4
}

const iterator = fn2()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

1.3 await 语法

2. 异步编程

2.1 名词说明

JS 当中是否存在异步编程
JS 是否能同时处理多件事情(JS是多线程的?)
事件循环是为了解决什么问题或者达到了什么效果
事件循环中的循环是如何体现的

2.1.1 进程与线程

  1. 进程可以看做是一个应用程序(例如打开浏览器或者浏览器打开一个页面)
  2. 线程是程序当中具体做事情的人,每个线程同一时刻只能做一件事
  3. 一个进程当中可以包含多个线程

2.1.2 同步异步

  1. 同步编程:一件事一件事的去做,上一件没有完成,下一件不会被处理(单线程)
  2. 异步编程:上一件事没有处理完,下一件事可以可以继续去处理(多线程)
  3. JS是单线程,基于 EventLoop 机制来实现异步编程的

2.1.3 JS 中的异步操作

  1. promise(then)
  2. async/await(generator)
  3. requestAnimationFrame
  4. 定时器操作
  5. ajax(网络请求)
  6. 事件线绑定

2.1.4 JS 单线程

  1. 浏览器是多线程的,GUI 渲染线程、JS引擎线程、事件触发线程、异步HTTP请求线程
  2. 浏览器平台下的 JS 代码是由 JS引擎执行的,所以它是单线程
  3. JS 中大部分代码都是同步编程,可以基于单线程的 EventLoop 实现异步效果

2.2 EventLoop与EventQueue

模型图
测试工具

2.2.1 代码执行顺序

  1. 浏览器加载界面之后会分配一个线程来执行 JS代码,称之叫 JS引擎(主线程)
  2. JS引擎会自下而下执行 JS 代码,此过程会遇到(定时器、网络请求、事件绑定、promise)
  3. 遇到上述的代码之后,浏览器会开启一个 Event Queue(任务|事件)队列 优先级队列结构
  4. 在队列中存在二个任务队列:微任务micro task | 宏任务 macro task
  5. 最终会将遇到的异步任务放到 Event Queue队列当中(未执行)
  6. 主线程会继续向下执行同步代码,直到所有同步代码执行完就会处理异步任务
  7. 进入 Event Queue 当中查找异步任务,找到之后放入主线程里执行(此时主线程又被步用)
  8. 执行完一个异步任务之后,主线程再次空闲,此时再进入 Event Queue 查找余下的异步任务

2.2.2 异步任务执行顺序

  1. 先执行微任务(只要有微任务就不会处理宏任务)
  2. 微任务(一般是谁先放置谁先执行)
  3. 宏任务(一般是谁先到达的谁先执行)

2.2.3 整体顺序

  1. 同步任务
  2. 异步微任务
  3. 异步宏任务

- 如果同步任务执行过程中遇到可执行的异步任务,此时依然需要等到同步任务执行完
- 如果同步任务执行完,还没有可执行的异步任务,此时也只能等待
- 不论何时放入的微任务,只要异步微任务存在,就永远不会执行异步宏任务(已经放入到微任务队列)

2.2.4 setTimeout 补充

  • 设置定义时器这个操作是同步的
  • 放置在 Event Queue 中的任务是异步宏任务
  • 函数调用返回数字,表示当前是第几个定时器
  • 等待时间设置为0时,也不是立即执行,因为浏览器存在最快的反应时间
  • 定时器的等待时间到了之后,它的代码可能还会执行(处于异步队列中,同步任务还未完成执行)

2.3 流程练习

2.3.1 执行流程

setTimeout(() => {
  console.log('1')
}, 30)

console.log(2)

setTimeout(() => {
  console.log(3)
}, 20)

console.log(4)


console.time('AA')
// 消耗95ms
for (let i = 0; i < 88888888; i++) { }
console.timeEnd('AA')

console.log(5)

setTimeout(() => {
  console.log(6)
}, 18)

console.log(7)

setTimeout(() => {
  console.log(8)
}, 25)

console.log(9)

2.3.2 主线程占用

// 死循环是将主线程彻底占用,其它所有事情不在处理
// 抛出异常只会影响下面的同步任务,已经放置在队列当中的任务会继续执行
setTimeout(() => {
  console.log(1)
}, 0)

console.log(2)
// while (true) { }
throw new Error('手动抛出异常')
// console.log(a)

console.log(3)

setTimeout(() => {
  console.log(4)
}, 10)

console.log(5)

2.3 Promise

Promise 是ES6 当中新增的一个类,Promise 是一种承诺,约定的模式,基于这种模式可以有效的处理异步编程
可以有效的管理异步编程当中的代码,让代码开发起来更便捷,维护更方便,可读性更强

2.3.1 语法

  1. executor函数
    1. 执行 new 操作时必须传入参数
    2. executor 函数接收两个函数作为参数,且会立即执行
    3. executor 函数一般用于管理异步操作, 也可写同步
  2. Promise 实例
    1. new 操作执行之后会返回一个 Promise 实例对象
    2. [[PromiseState]] promise状态:pending(准备态), fulfilled(成功态),rejected(失败态)
    3. [[PromiseResult]] promise值:默认是undefined, 一般用于存储成功的结果或者失败的原因
    4. proto:查找 Promise.prototype 原型,存在 then catch finally 三个常见的方法
  3. 状态切换
    1. 执行 resolve 将实例状态改变成功态,传递的值是成功结果
    2. 执行 reject 将实例状态改为 rejected,传递的值是失败结果
    3. 如果 executor 函数中的代码执行报错,则状态也会切换至失败态,报错原因是 value 值
    4. promise 状态一旦从pending态完成了切换,都无法再次改变状态
  4. 异步处理
    1. new Promise 之后会立即执行 executor 函数
    2. 在 executor 函数中管理了一个异步编程代码,此时状态是 pending(如果是异步操作)
    3. 当异步操作达到执行时机时,开始执行(看做是异步操作成功),调用 reject或者resolve修改状态
    4. 状态明确之后就可以执行后续的代码, 成功态调用第一个方法,失败态调用第二个方法
const p1 = new Promise((resolve, reject) => {
  console.log('executor函数必须传且立即执行')
  // resolve('1000')
  // reject('10')
  // console.log(a)
  // 管理异步操作
  setTimeout(() => {
    resolve('1s之后')
  }, 1000)
})
console.log(p1)

p1.then((result) => {
  console.log('成功态-->', result)
}, (reason) => {
  console.log('失败态-->', reason)
})
  1. 执行顺序总结
let p1 = new Promise((resolve, reject) => {
  console.log(1)
  // resolve('ok') 
  // console.log('2')
  setTimeout(() => {
    resolve('ok') 
    console.log('2')
  }, 1000)
})
p1.then((ret) => {
  console.log('success--->', ret)
}, (reason) => {
  console.log('failed--->', reason)
})
console.log(3)
  1. 执行 new Promise 操作
  2. 执行 executor 函数: 执行同步代码 + 执行异步操作(如果有)
  3. 执行 p1.then 注入两个方法(是否立即添加至微任务队列取决于当前 p1 状态是否明确,不考虑 pending)
  4. 同步代码执行完成,等到异步任务也达到可执行状态
  5. 先从队列中查找微任务,如果有则拉入主进程执行,如果没有则查找宏任务,如果有则执行

2.3.2 状态和值整理

  1. new Promise 获取实例
    1. 通过调用 resolve 或 reject 控制 [[PromiseState]] 和 [[PromiseResult]]
    2. executor 函数执行失败, 状态为 rejected 值为报错信息
  2. 执行 then 返回实例
    1. then 注入的两个方法不论执行哪个,只要执行不报错,新实例状态就是 fulfilled,反之就是 rejected,新实例的值就是方法的返回值
    2. 如果方法执行返回新promise实例,则此实例最后的成功或者失败就决定了then拿到的实例是成功还是失败,值都是一样的
let p1 = new Promise((resolve, reject) => {
  resolve('ok')
})

let p2 = p1.then((result) => {
  console.log('success-->', result)
  // return 10
  // console.log(a)
  setTimeout(() => {
    console.log('okk')
  }, 1000)
  // return Promise.resolve('000000')
}, (reason) => {
  console.log('failed-->', reason)
  return 20
})

p2.then((result) => {
  console.log('success-->', result)
}, (reason) => {
  console.log('failed-->', reason)
})

2.3.3 失败Promise处理

  1. 对于失败的 promise 实例如果没有编写方法处理其结果,则会在控制台中抛出异常信息(不阻碍其它代码执行)
  2. then链中的方法处理存在顺延机制,如果某个方法没有传递,则会顺延至下一个then中具备相同状态需要执行的函数
// 此时浏览器会抛出语法异常
Promise.reject('NO').then((result) => {
  console.log('只添加了成功态')
})

// 顺延机制
Promise.reject('No').then((result) => {
  console.log('成功的方法')
}).then(null, (reason) => {
  console.log('第二个then当中的reject 处理方法', reason)
})

// catch 使用
Promise.resolve('拉勾教育').then((result) => {
  console.log('成功1-->', result)
  return 10
}).then((result) => {
  console.log('成功2-->', result)
  return Promise.reject('用 catch 最后来处理失败')
}).catch(reason => {
  console.log('失败原因是:', reason)
})

2.3.4 Promise与微任务

  1. then 操作
    1. 当前实例的状态如果是明确的,则会创建一个异步微任务
    2. 当前实例的状态如果是pending,此时不会立即创建微任务(可以理解为保存任务)
  2. reject|resolve执行
    1. 调用上述方法时会创建一个异步微任务,同步结束后基于状态执行then的相关操作

2.4 async 与 await

2.4.1 async 特点

  1. 用于修饰函数,默认让函数返回一个 promise 实例
  2. 如果函数执行报错,则 promise状态为 rejected, 值为报错原因
  3. 如果函数执行正常则实例状态为 fulfilled,值为函数返回值,如果函数没有返回值则值就是 undefined
async function foo() {
  return 10
}

console.log(foo())

foo().then((result) => {
  console.log('成功态-->', result)
})

2.4.2 await

  1. await 要基于 async 配合使用,一般不会单独使用 async
  2. await 后面一般放置的是 promise 实例,如果不是,则会将它转为 new Promise.resolve() 处理
  3. await foo() 语法执行规则就是立即执行 foo 函数,接收 foo 的返回值然后处理为 promise 实例
  4. await 本身是一个异步微任务,把当前上下文中 await 下面要执行的代码整体存储到异步微任务当中,当await 后面的promise 实例状态切换之后再去执行之前的异步微任务
function foo() {
  console.log(1)
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(2)
    }, 1000)
  })
}

console.log(3)

async function fn() {
  console.log(4)
  let result = await foo()
  console.log(result)
  console.log(5)
}

fn()

console.log(6)

2.4.3 面试题

async function async1() {
  console.log('async1 执行了')
  await async2()
  console.log('async1 结束了')
}

async function async2() {
  console.log('async2')
}

console.log('同步代码执行了')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

async1()

new Promise((resolve) => {
  console.log('promise1')
  resolve()
}).then(() => {
  console.log('promise2')
})

console.log('同步代码结束了')
console.log('start')
let timer
Promise.resolve()
  .then(() => {
    console.log('a')
  })
  .then(() => {
    console.log('b')
  })

setTimeout(() => {
  Promise.resolve()
    .then(() => {
      console.log('c')
    })
    .then(() => {
      console.log('d')
    })
  timer = setInterval(() => {
    console.log('loop')
  }, 4000)
  console.log('setTimeout')
}, 0)
setTimeout(function foo1() {
  console.log('1')
})

Promise.resolve().then(function foo2() {
  console.log('2')
}).then(function foo3() {
  return Promise.resolve('3').then(function foo4(data) {
    setTimeout(function foo5() {
      console.log('4')
    })
    console.log('5')
    return data
  })
}).then(function foo6(ret) {
  console.log(ret)
})
function fn1() {
  console.log('fn1 start')
  return new Promise((resolve) => {
    resolve('zcegg1')
  })
}
function fn2() {
  console.log('fn2 start')
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('zcegg2')
    }, 10)
  })
}
console.log('a')
setTimeout(async () => {
  console.log('b')
  await fn1()
  console.log('c')
}, 20)
for (let i = 0; i < 88888888; i++) { }  // 90ms  
console.log('d')
fn1().then((result) => {
  console.log('f')
})
fn2().then((result) => {
  console.log('g')
})
setTimeout(() => {
  console.log('h')
}, 0)
console.log('end')
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容