# JS异步进阶

# 前言

本文分为以下几个部分:

  • 为什么需要异步?
  • 异步的实现机制?EventLoop?
  • 宏任务和微任务?
  • 异步的演化

# 一、异步基础

# 为什么需要异步

JS设计为单线程,同步会阻塞代码的执行,异步不会阻塞代码的执行

# 异步的使用场景

  • 网络请求,如Ajax请求
  • 定时任务,如setTimeout

# Promise的基本使用

问题:回调地狱 promise的好处:窜行的方式代替回调嵌套

# 二、异步编程的演化

  • callback
  • promise
  • generator
  • async/await

# callback

高阶函数:接受函数作为参数的函数

function sayHello(callback) {
  console.log('Hello');
  return callback()
}
sayHello(() => {
  console.log('callback')
})
1
2
3
4
5
6
7

比如下面的高阶函数例子:

[1,2,3].map(el => el * el);
[1,2,3].filter(el => el > 2);
1
2

高阶函数中的函数参数被执行,就是回调,异步编程中回调是非常常见的编程范式。

但是会有回调地狱的问题,造成阅读困难,我们可以通过将函数进行提取缓解,但依然很难阅读。

为了解决这个问题,JS引入了 promise

# Promise

promise 有三个状态:pending fulfilled rejected ,默认状态是 pending

promise 内部可以通过 resolve() 修改状态为fulfilled,或reject() 修改状态为 rejected

修改状态为 fulfilled 会触发 promise.then() 修改状态为 rejected 会触发 promise.catch()

因此我们可以把成功的回调处理函数传入 promise.then(),报错的回调处理函数放入 promise.catch()

const p = new Promise((resolved, rejected) => {
  resolved(1);
})
p.then((result) => {
  console.log(result);
})

1
2
3
4
5
6
7

promise 还支持链式调用,可以简化代码

但是毕竟还是异步的思维,于是ES7引入了 Async/Await ,一步到位

async 放在函数前,表示返回的是一个包裹了返回值的 promise,同时告诉JS引擎内部可能有异步函数调用 await 一定要配合 async 才能使用,不然会报错

有了 async/await 组合,我们可以使用同步的写法来处理异步调用了

错误处理可以用 try{} catch(){}

try {

} catch(err) {

}
1
2
3
4
5

# async/await

同步语法,彻底消灭回调函数

# 1、async/await 使用

// async function() {
//   await 
// }
1
2
3

# 2、async/await 和 Promise 的关系

  • 执行async函数,返回的是一个Promise对象
  • await 相当于 promise 的 then
  • try...catch可捕获异常,代替了 promise 的 catch

# 3、async/await是语法糖,异步的本质还是回调函数

  • async 函数会同步执行
  • await 的后面,都可以看成是 callback 里的内容,即异步
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

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

console.log('script start');
async1();
console.log('script end');

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4、for...of 的应用场景

  • forEach for...in 同步
  • for...of 支持异步
async function multiple(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num * num);
        }, 1000)
    })
    
}

// forEach 是同步的,因此1秒后,全部输出
[1,2,3].forEach(async n => {
    let res = await multiple(n)
    console.log(res);
})

// for...of 支持异步,1秒输出一个数
for (let i of [1,2,3]) {
    let res = await multiple(i);
    console.log(res)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 三、宏任务和微任务

# 什么是宏任务,什么是微任务?

  • 宏任务:SetTimeout、Ajax、I/O、DOM事件
  • 微任务:Promise、Async/Await

# 为什么要加入微任务?

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如监听 DOM 的变化。

监听 DOM 变化技术方案的演化史

  1. 轮询
  2. Mutation Event
  3. Mutation Observer(采用微任务机制,有效地权衡了实时性和执行效率的问题)

# Eventloop 和 DOM渲染

eventloop过程:

  1. 执行当前同步代码,
  2. callstack空闲,检查当前宏任务的微任务队列,并依次执行
  3. 尝试DOM渲染
  4. 触发EventLoop,从消息队列中获取宏任务并执行

# 宏任务和微任务的区别

  • 本质:宏任务是由浏览器规定的、微任务是ES6语法规定的
  • 在EventLoop中的位置:微任务在DOM渲染前触发;宏任务在DOM渲染后触发
  • 实现原理:微任务会被放入当前执行栈的微任务队列中,执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数;宏任务是把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数

# setTimeout的实现原理

渲染进程内部的延迟执行队列

# ajax的实现原理

执行流程:

  1. 利用IPC和网络进程通讯
  2. 网络进程发送网络请求到服务器
  3. 网络进程收到服务器返回的消息,将消息体封装起来加入到消息队列中
  4. 等当前执行栈空时,从消息队列中取出并执行

# 问答题:

# EventLoop的机制

EventLoop 是 JS异步回调的实现机制

eventloop 执行过程:

  1. 首先执行同步代码
  2. 遇到异步调用,如setTimeout或ajax请求等web api时,则交由浏览器的其他进程或线程处理,继续执行同步代码
  3. 异步调用处理完毕后,会将回调加入到回调队列中,等到同步代码执行完毕,会依次从回调队列取出并加入到执行栈中执行

# Promise 有哪三种状态?如何变化?

# 三种状态:

  • pending resolved rejected
  • pending -> resolved 或 pending -> rejected
  • 变化不可逆

# 状态的表现

  • resolved 会触发 then 回调函数
  • rejected 会触发 catch 回调函数

# then和catch状态改变规则(重要)

  • Promise.then 正常返回 resolved ,里面有报错则返回 rejected
  • Promise.catch 正常返回 resolved ,里面有报错则返回 rejected

# 场景题

# 场景题1:promise then和catch的连接

第一题:

Promise.resolve().then(() => {
    console.log(1)
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})

1
2
3
4
5
6
7
8

第二题:

Promise.resolve().then(() => {
    console.log(1)
    throw new Error('error1')
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})
1
2
3
4
5
6
7
8

第三题:

Promise.resolve().then(() => {
    console.log(1);
    throw new Error('err')
}).catch(() => {
    console.log(2)
}).catch(() => {
    console.log(3)
})
1
2
3
4
5
6
7
8

# 场景题2:async/await语法

async function fn() {
  return 100
}
(async function() {
  const a = fn()
  const b = await fn()
})
1
2
3
4
5
6
7
(async function() {
  console.log('start')
  const a = await 100
  console.log('a', a)
  const b = await Promise.resolve(200)
  console.log('b', b)
  const c = await Promise.reject(300)
  console.log('c', c)
  console.log('end')
})()
1
2
3
4
5
6
7
8
9
10

# 场景题3:promise 和 setTimeout 的顺序

console.log(100)
setTimeout(() => {
  console.log(200)
})
Promise.resolve().then(() => {
  console.log(300)
})
console.log(400)
1
2
3
4
5
6
7
8

# 场景题4:外加async/await的顺序问题

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

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

console.log('script start');
async1();
console.log('script end');
1
2
3
4
5
6
7
8
9
10
11
12
13

# 图片请求

codepen (opens new window)

# Ajax请求封装

TODO...

# 模拟红绿灯

codepen (opens new window)

# 手写Promise

  • 初始化 & 异步调用
  • then catch 链式调用
  • API .resolve .reject .all .race
/**
 * @description MyPromise
 * @author Aaron
 */
class MyPromise {
  state = 'pending' // 状态 pending fulfilled rejected
  value = undefined // 成功后的值
  reason = undefined // 失败后的原因

  resolveCallbacks = [];
  rejectCallbacks = [];

  constructor(fn) { // 传入有异步操作的函数
    const resolveHandler = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.resolveCallbacks.forEach(fn => fn(value));
      }
    }

    const rejectHandler = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.rejectCallbacks.forEach(fn => fn(reason));
      }
    }
    try {
      fn(resolveHandler, rejectHandler); // 跑异步代码
    } catch(err) {
      rejectHandler(err);
    }
  }

  then(fn1, fn2) {
    // 当pending状态时,fn1和fn2会保存在callbacks中
    fn1 = typeof fn1 === 'function' ? fn1 : (v) => v;
    fn2 = typeof fn2 === 'function' ? fn2 : (v) => v;

    if (this.state === 'pending') {
      return new MyPromise((resolve, reject) => {
        this.resolveCallbacks.push(() => {
          try {
            const newValue = fn1(this.value);
            resolve(newValue);
          } catch (err) {
            reject(err)
          }
        });
        this.rejectCallbacks.push(() => {
          try {
            const newReason = fn2(this.reason);
            resolve(newReason);
          } catch (err) {
            reject(err)
          }
        });
      })
    } else if (this.state === 'fulfilled') {
      return new MyPromise((resolve, reject) => {
        try {
          const newValue = fn1(this.value);
          resolve(newValue)
        } catch(err) {
          reject(err)
        }
      })
    } else if (this.state === 'rejected') {
      const p1 = new MyPromise((resolve, reject) => {
        try {
          const newReason = fn2(this.reason);
          reject(newReason);
        } catch (err) {
          reject(err)
        }
      })
      return p1;
    }
  } 

  // 就是 then 的一个语法糖,简单模式
  catch(fn) {
    return this.then(null, fn)
  }

}


MyPromise.resolve = (value) => {
  return new MyPromise((resolve, reject) => resolve(value));
}

MyPromise.reject = (reason) => {
  return new MyPromise((resolve, reject) => reject(reason));
}

MyPromise.all = (promiseList = []) => {
  return new MyPromise((resolve, reject) => {
    const result = []; // 存储所有的返回结果
    const length = promiseList.length;
    let resolveCount = 0;

    promiseList.forEach((p,index) => {
      p.then(data => {
        result[index] = data; // 注意:这里promise resolve的结果需对应到result索引中
        resolveCount ++;
        
        if (resolveCount == length) {
          resolve(result);
        }
      }).catch (err => {
        reject(err)
      })
    })
  })
}

MyPromise.race = (promiseList = []) => {
  let resolved = false; // 标记
  return new MyPromise((resolve, reject) => {
    promiseList.forEach(p => {
      p.then(data => {
        if (!resolved) {
          resolve(data);
          resolve = true;
        }
      }).catch (err => {
        reject(err);
      });
    })
  })
}


const p1 = new MyPromise((resolve, reject) => {
  // resolve(100)
  // reject('错误信息...');
  setTimeout(() => {
    resolve(100)
  }, 500)
})

const p11 = p1.then(data1 => {
  console.log('data1 ', data1);
  return data1 + 1;
})

const p12 = p11.then(data2 => {
  console.log('data2 ', data2);
  return data2 + 2;
})

const p13 = p12.catch(err => console.error(err));

const p2 = MyPromise.resolve(200)
const p3 = MyPromise.reject('错误信息...')
const p4 = MyPromise.all([p1,p2]) // 传入 promise 数组,等待所有都 fulfilled 之后,返回新 Promise,包含前面所有的结果
const p5 = MyPromise.race([p1,p2]) // 传入 promise 数组,只要有一个 fulfilled ,即可返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

codepen (opens new window)

上次更新: 2/24/2022, 2:41:04 PM