# JS异步解决方案的发展历程

JS异步解决方案的发展历程

  • 回调函数

    • 缺点:回调地狱、不能捕获错误
  • 事件监听

    • 缺点:整个流程变成事件驱动,思路不太清晰
  • 发布订阅

    • 优点:多了一个“消息中心”
  • Promise

    • 优点:解决了回调地狱
    • 缺点:1、无法取消Promise;2、错误需要通过回调函数来捕获
  • Generator

    • 优点:控制函数的执行
    • 缺点:要编写自动执行器
  • Async/Await

    • 优点:1、Generator+自动执行器;2、更像同步写法
  • WebWorker

    • 优点:开启了一个“新线程”

# Callback(回调函数)

如果是以前,可以用回调函数实现:

function runAsync(callback){
    if(/* 异步操作成功 */) {
        callback(value)
    }
}

// 传入一个匿名函数作为回调函数
runAsync(funtion(data) {
    console.log(data)
})
1
2
3
4
5
6
7
8
9
10

缺点:

  • 容易造成 回调地狱
  • 影响阅读体验

# Promise

Promise是一个容器,而且代表的是一个异步操作,有3种状态:

  • Pending(进行中)
  • Fulfilled(已成功)
  • Rejected(已失败)

只有 异步操作的结果 可以决定当前是哪一种状态,任何其他操作都无法改变这个状态(也就是“承诺”的意思)。

并且,它的状态可以影响后续的then行为

// Promise的构造函数接收一个函数作为参数,这个函数又可以传入两个参数:resolve、reject;
// 它们分别表示:异步操作执行后,Promise的状态变为Fulfilled/Rejected的回调函数。
var promise = new Promise(function (resolve, reject) {
    // ...
    if(/* 异步操作成功 */) {
        resolve(value) // 这个value表示的是异步操作后获得的数据
    } else {
        reject(error) // 这个error表示的是异步操作后报出的错误
    }
})
1
2
3
4
5
6
7
8
9
10

优点:

  • 解决了 回调地狱
  • 方便阅读

缺点:

  • 返回值传递
    • 仍然需要创建then调用链,需要创建匿名函数,把返回值一层层传递给下一个then
  • 异常不会向上抛出
    • then里函数有异常,then调用链外面写try-catch没有效果
  • 不方便调试
    • 在某个.then设置断点,不能直接进到下一个.then方法

对于Promise的异常捕获:

Promise.prototype.catch().then(null, function(err) { ... })的别名

p.then(data => console.log(data))
.catch(err => console.log(err))

// 等价于
p.then(data => console.log()})
.then(null, err => console.log(err))
1
2
3
4
5
6

可知,catch()then()第二个参数的区别

  • catch()可以捕获前面所有的异常(包括Promise里的reject、then里的
  • 第二个参数只能捕获Promise里的reject、前一个then的错误

# Promise.all()的用法、异常处理

# Promise.all([])接收一个数组,数组中每个元素都是Promise的实例

例如:

var p1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 3000, 'first')
})
var p2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 0, 'second')
})
var p3 = new Promise((resolve, reject) => {
    setTimeout(resolve, 1000, 'third')
})

var p = Promise.all([p1, p2, p3])
p.then(data => console.log(data)) // ['first', 'second', 'third']
1
2
3
4
5
6
7
8
9
10
11
12
  • p1、p2、p3都为fulFilled参数的顺序传给p的回调函数then
  • p1、p2、p3其中一个为rejected,会把第一个变rejected的值传给p的回调函数catch

# 异常处理

因为Promise.all()方法是一旦抛出其中一个异常,那其他正常返回的数据也无法使用了

alt

解决办法:

  • 方法一:改为串行调用(失去了并发优势)
  • 方法二:将p1、p2、p3这些promise自身定义一个catch方法
    • 那它被rejected时,也不会触发Promise.all()的catch。而是会触发自身定义的catch。因为他们自身定义的catch方法返回的是一个新Promise实例作为参数的这个promise实际上指的会变成这个新实例,这个新实例会变成resolved
  • 方法三:在Promise内,先用try-catch吃掉这个异常。在其catch内再调用resolve(err),让外面的Promise“感觉”像是调用成功(和方法二的区别是,方法二是个新实例)
// 方法二:
var p2 = new Promise((resolve, reject) => {
    setTimeout(resolve, 0, xxx)
})
.then(result => result)
.catch(err => err)
1
2
3
4
5
6
// 方法三:
var p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        try {
            console.log(xxx) // xxx未声明,会抛出异常给下面的catch块
        } catch(err) {
            resolve(err) // 在内部的catch里调用resolve(err)
        }
    })
})
1
2
3
4
5
6
7
8
9
10

alt

# Promise源码思路

let p = new Promise((resolve, reject) => {
    resolve('heshiyu')
})
p.then(res => console.log('heshiyu'))
1
2
3
4
  • 改变new Promise里的callback的this指向(指向实例本身以及实例本身定义的resolve、reject)
  • Promise的状态管理、改变
  • 保存子promise
  • then
    • 回调接收两个参数
    • 回调return value传给下一个promise
    • 自身返回promise
  • done:循环children
  • handle:处理children,处理完改变children实现递归

es6 promise源码实现

# 实现Promise.all

// 会按顺序输出,不过以下是串行的:
var mockAll = function (args) {
   let values = [];
   return new Promise((resolve, reject) => {
           let i = 0;
           next();
           function next() {
               args[i].then(res => {
                   values.push(res);
                   i++;
                   if (i === args.length) {
                       resolve(values)
                   } else {
                       next()
                   }
               }).catch(err => reject(err))
           }
   })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Generator函数

看一个Generator函数

function* asyncJob(x) {
    console.log('aaa')
    var y = yield x + 2
    console.log('bbb')
    console.log('y', y)
    var y2 = yield y + 6
    return y2
}

var g = asyncJob(1) // Generator函数会返回一个遍历器g
console.log(g.next())
// ① 先输出'aaa'
// ② 再输出{ value: 3, done: false }
// value表示:yield语句后面跟的表达式的值,此时y的赋值还未完成!
// done表示:Generator函数是否执行完毕
// -----此时Generator函数执行到此处,执行权交给外面(第一个yield前的、且其后的表达式并返回)-----
console.log('ccc')
// ③ 输出'ccc'
console.log(g.next(660))
// next函数把执行权交回里面,继续执行
// 因为往next()传参(只能带一个),上一次“yield后跟的表达式返回值 = 参数(即660)”,给了y
// 完成y的赋值,因为next传参660,所以y = 660
// ④ 先输出'bbb'
// ⑤ 再输出'y, 660'
// 遇到第二个yield,执行yield后跟表达式
// ⑥ 返回对象{ value: 666, done: false },此时y2的赋值还未完成
// -----此时Generator函数执行到此处,执行权交给外面(第二个yield前的、且其后的表达式并返回)-----
console.log(g.next(3))
// next函数把执行权交回里面,继续执行
// 因为往next()传参(只能带一个),上一次“yield后跟的表达式返回值 = 参数(即3)”,给了y
// 完成y的赋值,因为next传参3,所以y2 = 3
// ⑦ 最后输出{ value: 3, done: true }
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
aaa
{ value: 3, done: false }
ccc
bbb
y, 660
{ value: 666, done: false }
{ value: 3, done: true }
1
2
3
4
5
6
7

# Generator函数能封装异步的原因?

根本原因:Generator函数可以暂停执行恢复执行

两个特性:

  • 函数内外的数据交换
    • 内对外:在外面调用next后,得到返回里的value
    • 外对内:在外面调用next后,传入的参数(只能传一个)
  • 错误处理

# yield的特点

  • 用来说明next函数返回的value值
  • 每个yield调用后,后面的代码都会停止执行
  • yield不能穿透函数(即不能使用forEach来遍历声明yield,必须用for!!)

迭代器对象可以任意具有.next方法的对象

# Generator函数的自动执行

Generator是一个异步操作的容器,它的自动执行需要一种机制(当异步操作有了结果,这种机制就要自动交回执行权),有两种方法:

  • 回调函数。
    • 将异步操作包装成Thunk函数,在回调函数里交回执行权
  • Promise对象。
    • 将异步操作包装成Promise对象,在then方法里交回执行权

# Async、Await

async是一个函数修饰符,表示函数里有异步操作

async函数会返回一个Promise对象,可以使用then添加回调函数;

await表示紧跟在后面的表达式需要等待结果;

后面跟也是Promise

好处:

  • 简洁。易于阅读和理解
  • 错误处理。
    • 可以被try-catch捕捉到
  • 方便调试。

# async

async是Generator函数的语法糖。它会返回一个promise对象(并且会等到内部所有await后面的Promise对象执行完才会发生状态改变)

async function f() {
    return 'Hello world'
}
f().then(v => console.log(v)) // 'Hello world'
1
2
3
4

可见,函数f内部return返回的值,会被then方法回调函数接收到。

# async的使用形式

// 函数声明
async function func1 () { ... }

// 函数表达式
var func1 = async function () { ... }

// 对象的方法
var obj = {
    async func1() { ... }
}

// 类的方法
class Storage {
    async func1() { ... }
}

// 箭头函数
var func1 = async () => { ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# async函数的实现原理

async函数实际上是Generator函数自动执行器的一个包装

async function func1(args) {
    // ...
}

// 等价于
function func1(args) {
    return spawn(function* () {
        // ...
    })
}
1
2
3
4
5
6
7
8
9
10

其中spawn函数

function spawn(genF) {
    return new Promise((resolve, reject) => {
        var gen = genF()
        function step(nextF) {
            try {
                var next = nextF()
            } catch(e) {
                return reject(e)
            }
            if (next.done) {
                return resolve(next.value)
            }

            Promise.resolve(next.value).then(v => {
                step(function() { return gen.next(v) })
            }, e => {
                step(function() { return gen.throw(e) })
            })
        }

        step(function() { return gen.next(undefined) })
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

因为立即resolved的Promise是在本轮事件循环的末尾执行,所以最好前面加个return

# async的错误处理机制

async函数的实现原理可知,函数内部await 后面跟的Promise只要有一个reject了,那就会使得 async函数所返回的Promise对象 也被reject。

如果不想整个async函数中断,有两个方法

async function f () {
    // 方法一:用try-catch捕获:“可能会抛出异常的await”
    try {
        await Promise.reject('error')
    } catch(e) {
        console.log(e)
    }

    // 方法二:对“可能会抛出异常的await”声明catch
    await Promise.reject('error').catch(e => console.log(e))

    return await Promise.resolve('Hello world')
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# await

await后面跟的是一个Promise对象(如果不是,他会被转成一个立即resolve的Promise对象)

# 多个await并发、继发执行

并发执行:

常见场景:一组异步操作的按顺序输出

原理:每次迭代会生成新的async。(原理上只能保证同一个async内部的await是继发)

// ① forEach、map
arr.forEach(async (doc) => {
    await fetchUrl(doc)
})
// ② Promise.all
let [foo, bar] = await Promise.all([getFoo(), getBar()])

1
2
3
4
5
6
7

继发执行:

// for ... of
for (let doc of arr) {
    await fetchUrl(doc)
}
1
2
3
4
更新时间: 5/13/2020, 1:46:53 PM