异步篇

72 阅读6分钟

js为什么需要异步,因为异步的本质是单线程,异步又是基于回调实现的

setTimeout

传参

  • 如果传入立即执行函数,会被立即执行。
  • 给执行函数传参可以用字符串形式,或者传参
function fnHello(name) {
    console.log('hello, ' + name)
}
setTimeout(fnHello('tony'), 2000)
setTimeout("fnHello('jack')", 2000)
setTimeout(fnHello, 2000, 'li')

面试题

var b = '2'
function abc() {
    let b = 1
    ++b
    setTimeout(() => {
        test('fun test')
    }, 0)
    setTimeout(test('test fun'), 1000)
    console.log(b)
    function test(str) {
        this.b++
        console.log(str)
        console.log(this.b++)
    }
}
abc()

解析执行结果

  1. test fun:setTimeout第一个参数,是个立即执行的函数,打印传入的字符串
  2. 3:test执行函数没有任何调用者,this指向window,全局作用域的b++,字符串被隐式转换成Number。函数执行完b为4
  3. 2:函数作用域++b的结果
  4. fun test:同步代码执行完,执行栈清空,从宏任务队列中取出回调压栈执行
  5. 5:全局作用域的b++,4 => 5

Promise

把嵌套的回调变成链式调用,类似一节一节的管道。resolve会进到then回调,reject进到catch的回调

三种状态

const p1 = Promise.resolve(1).then(() => {
    return 'resolve.suc'
})
console.log('p1', p1)

const p2 = Promise.reject(2).then(() => {
    return 'reject1.err'
})
console.log('p2', p2)

const p3 = Promise.reject(3).then(() => {
    return 'reject2.err'
}).catch(err => {
    console.log('p3', err)
})
console.log('p3', p3)

// p1和p3的最终状态都是fulfilled,p2是rejected

异常捕获

  • then和catch内部没有报错,返回resolve的promise,进到then
  • 如果有报错,都会返回reject的promise,进到catch
const p4 = Promise.resolve(4)
    .then(() => {
        throw new Error('p4.then.throw err ----- ')
    })
    .catch((err) => {
        console.log('p4.catch', err)
    })
// 输出:p4.catch Error: p4.then.throw err-----
const p5 = Promise.resolve(5)
    .then(() => {
        throw new Error('p5.then.throw err ----- ')
    })
    .catch((err1) => {
        console.log('p5.catch1 ---', err1)
        throw new Error('p5.catch1.throw err ----- ')
    })
    .then((r) => {
        console.log('p5.then2', r)
    })
    .catch((err2) => {
        console.log('p5.catch2 ---', err2)
    })
// p5.catch1 --- Error: p5.then.throw err -----
// p5.catch2 --- Error: p5.catch1.throw err -----  

面试题

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

// 第二题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})

// 第三题
Promise.resolve().then(() => {
    console.log(1)
    throw new Error('erro1')
}).catch(() => {
    console.log(2)
}).catch(() => {
    console.log(3)
})

解析执行结果

第一题

  • 1:第1个then,内部没有报错进到下一个then
  • 3:第2个then

第二题

  • 1:第1个then,内部报错进到catch
  • 2:第1个catch,内部没有报错进到下一个then
  • 3:第2个then

第三题

  • 1:第1个then,内部报错进到catch
  • 2:第1个catch,内部没有报错进到下一个then。后边是catch不是then,所以不执行

async/await

Generator 函数的语法糖。可以用同步语法来写异步了。

  • 执行async函数,返回的都是promise对象
  • await相当于promise.then
  • try/catch可以捕获异常,代替了catch
async function fn1() {
    return 100 // 相当于 Promise.resolve(100)
}
const r1 = fn1();

// await 相当于then
!(async function () {
    const r2 = await Promise.resolve(200)
    console.log("r2", r2)
})()

// 直接返回值,相当于Promise.resolve(300),同上
!(async function () {
    const r3 = await 300
    console.log("r3", r3)
})()

// await可以接收promise对象
!(async function () {
    const r4 = await fn1()
    console.log("r4", r4)
})()

// try...catch相当于catch
// await后的都会被放到then里,当接收到reject的promise,会进到catch,所以await后的代码不会执行
!(async function () {
    const p5 = Promise.reject("r5.err")
    try {
        const r5 = await p5
        console.log("r5", r5)
    } catch (err) {
        console.log("r5.catch", err) // 相当于catch
    }
})()

循环中使用异步

根据以下代码,实现依次延时打印结果

const arr = [1, 2, 3]
// 延时计算值的平方
function squ(n) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(n * n);
        }, 1000)
    })
}

使用forEach

arr.forEach(async n => {
    const r1 = await squ(n)
    console.log("r1", r1)
})
// 1秒后,三个结果一次输出,这不是期望的结果

// forEach实现原理
Array.prototype.customForEach = function (fn) {
    for (var i = 0; i < this.length; i++) {
        fn(this[i], i)
    }
}
// 尽管定义的时候是加了async/await,但是内部还是一个没有添加await的执行函数,所以会同步执行,同步输出结果

使用for循环

// 异步执行
!(async function () {
    for (let i = 0; i < arr.length; i++) {
        const r2 = await squ(arr[i]);
        console.log("for", r2)
    }
})()

// 异步执行
!(async function () {
    for (let i in arr) {
        const r2 = await squ(arr[i]);
        console.log("for in", r2)
    }
})()

// 异步执行
!(async function () {
    for (let i of arr) {
        const r2 = await squ(i);
        console.log("for of", r2)
    }
})()

// 这三种for循环都是每间隔1s输出一次结果

面试题

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

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

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

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

解析执行结果

  1. script start:同步代码直接打印
  2. async1 start:async1() 会立即执行函数(函数前面加上await后,执行可以拆解为两步:1.执行函数,2.await)
  3. async2:await async2() 立即执行,并将后边所有代码块封装起来(类似全放到Promise.then里),等待同步任务执行完。到此async1函数暂时执行完了
  4. script end:同步代码,清空调用栈
  5. async1 end:调用栈空了,从微任务队列中,取出回调压栈执行。
  6. async3:同第3步
  7. async1 end 2:同第5步

宏任务和微任务

  • 宏任务:setTimeout setInterval DOM 事件 ajax
  • 微任务:Promise
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="container"></div>
        <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
        <script>
            const $p1 = $('<p>一段文字</p>')
            const $p2 = $('<p>一段文字</p>')
            const $p3 = $('<p>一段文字</p>')
            $('#container').append($p1).append($p2).append($p3)
            alert('p.length:' + $('#container').children().length)
            Promise.resolve().then(() => {
                alert('Promise.resolve')
            })
            setTimeout(function () {
                alert('setTimeout')
            })
        </script>
    </body>
</html>

js执行和dom渲染是用的同一线程,使用alert来阻断js执行,也会阻断dom渲染。可以判断各任务执行的优先级。 解析执行结果:

  • 第1个alert弹出p的长度是3,但页面上没有看到三个p元素,说明dom的渲染被阻断了
  • 第2个alert弹出Promise,还是没看到三个p元素,dom的渲染还在处于被阻断的状态
  • 第3个alert弹出setTimeout,页面出现了三个p元素,说明dom渲染结束

由此可以得到执行顺序:Promise > dom 渲染 > 定时器

测试题

async function fn() {
    return 100
}
!(async function () {
    const a = fn()
    const b = await fn()
    console.log("a", a)
    console.log("b", b)
})()

// 输出:promise 100

// async函数返回的是promise对象,加上await就是promise.then的返回结果
!(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')
})()

// 输出:start 100 200 err(300)

// reject的promise会走到catch,所以async接收reject会进到try的catch中
// 由于后边两行代码都被then的回调中,所以不输出
console.log(100)
setTimeout(() => {
    console.log(200)
})
Promise.resolve().then(() => {
    console.log(300)
})
console.log(400)

// 输出:100 400 300 200

// promise和定时器的执行顺序

面试题

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

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

console.log('script start')

setTimeout(function () {
    console.log('setTimeout')
}, 0)

async1()

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

console.log('script end')

解析执行结果

  1. script start:同步代码
  2. async1 start:async1()立即执行函数
  3. async2:await async2()立即执行函数,后边代码快都封装起来,放到微任务队列末尾
  4. promise1:new Promise()传入的函数也会立即执行,resolve()后该实例状态变为fulfilled,将then回调放到微任务队列末尾
  5. script end:同步代码
  6. async1 end:执行栈空了,从微任务队列取出第3步的回调
  7. promise2:执行栈又空了,从微任务队列取出第4步的回调
  8. setTimeout:宏任务总是在同步任务、微任务、dom渲染之后执行

实现Promise

class MyPromise {
    // 三种状态:pending/fulfilled/rejected
    status = "pending";

    // 记录当前Promise的值,给新的Promise
    value = undefined; // 执行结果
    reason = undefined; // 错误信息

    // 存贮在pending状态下,then注册的回调
    resolveCallbacks = [];
    rejectCallbacks = [];

    /**
     * 执行函数,捕获异常。修改实例状态
     * @param {自定义函数} fn 
     */
    constructor(fn) {
        const resolveHandler = value => {
            this.value = value;
            this.status = "fulfilled";
            this.resolveCallbacks.forEach(callback => callback(this.value))
        }
        const rejectHandler = reason => {
            this.reason = reason;
            this.status = "rejected";
            this.rejectCallbacks.forEach(callback => callback(this.reason))
        }
        try {
            fn(resolveHandler, rejectHandler);
        } catch (err) {
            rejectHandler(err)
        }
    }
    /**
     * then始终返回新的Promise实例
     * @param {成功回调} fn1 
     * @param {失败回调} fn2 
     * @returns 
     */
    then(fn1, fn2) {
        // 处理兼容,保证都是可执行的函数
        fn1 = typeof fn1 === "function" ? fn1 : fn1 => fn1;
        fn2 = typeof fn2 === "function" ? fn2 : fn2 => fn2;

        return new MyPromise((resolve, reject) => {
            if (this.status === "fulfilled") {
                try {
                    // 执行成功回调的结果,传给当前Promise,状态变为resolve
                    resolve(fn1(this.value))
                } catch (err) {
                    // 捕获错误,当前Promise状态变为rejected
                    reject(err)
                }
            } else if (this.status === "rejected") {
                try {
                    // 执行失败回调的结果,传给当前Promise,状态变为reject
                    reject(fn2(this.reason))
                } catch (err) {
                    // 捕获错误,当前Promise状态变为rejected
                    reject(err)
                }
            } else if (this.status === "pending") {
                // 当实例还在定时器,或者ajax时,拿不到最新值。先存起来,等状态变了后执行
                this.resolveCallbacks.push(() => {
                    try {
                        resolve(fn1(this.value))
                    } catch (err) {
                        reject(err)
                    }
                })
                this.rejectCallbacks.push(() => {
                    try {
                        resolve(fn2(this.reason))
                    } catch (err) {
                        reject(err)
                    }
                })
            }
        })
    }

    catch(fn1) {
        return this.then(null, fn1)
    }
    static resolve(param) {
        return new MyPromise((resolve, reject) => resolve(param))
    }
    static reject(param) {
        return new MyPromise((resolve, reject) => reject(param))
    }
    /**
     * 全部执行完或者有一个失败就返回
     * @param {实例数组} promiseList 
     * @returns 
     */
    static all(promiseList = []) {
        return new MyPromise((resolve, reject) => {
            let cur = 0
            const res = [];
            promiseList.forEach(p => {
                p.then(r => {
                    res.push(r)
                    cur++
                    if (cur === promiseList.length) {
                        resolve(res)
                    }
                }).catch(err => {
                    reject(err)
                })
            })
        })
    }
    /**
     * 赛跑,有一个结束就返回
     * @param {*} promiseList 
     * @returns 
     */
    static race(promiseList = []) {
        let isOver = false
        return new MyPromise((resolve, reject) => {
            promiseList.forEach(p => {
                p.then(r => {
                    if (!isOver) {
                        resolve(r)
                    }
                    isOver = true
                }).catch(err => {
                    reject(err)
                })
            })
        })
    }
}

测试MyPromise

const p1 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('2s.end ---')
    }, 2000)
})
const p2 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('5s.end ---')
    }, 5000)
})
const res = MyPromise.all([
    p1,
    MyPromise.resolve(100),
    p2
    // MyPromise.resolve(200)
    // MyPromise.reject('200 --- ')
])
res.then((r) => {
    // console.log('all.r', r)
}).catch((err) => {
    console.log('all.err', err)
})

const res2 = MyPromise.race([
    p1,
    // MyPromise.resolve(100),
    p2
    // MyPromise.reject('200 --- ')
])
res2.then((r) => {
    // console.log('race.r', r)
}).catch((err) => {
    console.log('race.err', err)
})

const p0 = function () {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Promise.3s')
        }, 3000)
    })
}