侯策《前端开发核心知识进阶》读书笔记——异步

setTimeout

JavaScript 中所有任务分为同步任务和异步任务。

同步任务是指:当前主线程将要消化执行的任务,这些任务一起形成执行栈(execution context stack)
异步任务是指:不进入主线程,而是进入任务队列(task queue),即不会马上进行的任务。
当同步任务全都被消化,主线程空闲时,即上面提到的执行栈 execution context stack 为空时,将会执行任务队列中的任务,即异步任务。

这样的机制保证了:虽然 JavaScript 是单线程的,但是对于一些耗时的任务,我们可以将其丢入任务队列当中,这样一来,也就不会阻碍其他同步代码的执行。等到异步任务完成之后,再去进行相关逻辑的操作。

const t1 = new Date()
setTimeout(() => {
    const t3 = new Date()
    console.log('setTimeout block')
    console.log('t3 - t1 =', t3 - t1)
}, 100)


let t2 = new Date()

while (t2 - t1 < 200) {
    t2 = new Date()
}

console.log('end here')

// end here
// setTimeout block
// t3 - t1 = 200

即便 setTimeout 定时器的定时为 100 毫秒,但是同步任务中 while 循环将执行 200 毫秒,计时到时后仍然会先执行主线程中的同步任务,只有当同步任务全部执行完毕,end here 输出,才会开始执行任务队列当中的任务。此时 t3 和 t1 的时间差为 200 毫秒,而不是定时器设定的 100 毫秒。

最小延迟

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

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

//here 2
//here 100

另一种情况

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

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

在 Chrome 中运行结果相反,事实上针对这两个 setTimeout,谁先进入任务队列,谁先执行并不会严格按照 1 毫秒和 0 毫秒的区分。

表面上看,1 毫秒和 0 毫秒的延迟完全是等价的。这就有点类似“最小延迟时间”这个概念。直观上看,最小延迟时间是 1 毫秒,在 1 毫秒以内的定时,都以最小延迟时间处理。此时,在代码顺序上谁靠前,谁就先会在主线程空闲时优先被执行。

MDN 上给出的最小延时概念是 4 毫秒,可以参考 最小延迟时间,另外,setTimeout 也有“最大延时”的概念。这都依赖于规范的制定和浏览器引擎的实现。

宏任务(macrotask)与微任务(microtask)

宏任务和微任务虽然都是异步任务,都在任务队列中,但是他们也是在两个不同的队列中。

宏任务包括:

  • setTimeout
  • setInterval
  • I/O
  • 事件
  • postMessage
  • setImmediate (Node.js,浏览器端该 API 已经废弃)
  • requestAnimationFrame
  • UI 渲染

微任务包括:

  • Promise.then
  • MutationObserver
  • process.nextTick (Node.js)
    例子:
console.log('start here')

const foo = () => (new Promise((resolve, reject) => {
    console.log('first promise constructor')

    let promise1 = new Promise((resolve, reject) => {
        console.log('second promise constructor')

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

        resolve('promise1')
    })

    resolve('promise0')

    promise1.then(arg => {
        console.log(arg)
    })
}))

foo().then(arg => {
    console.log(arg)
})

console.log('end here')
  • 首先输出同步内容:start here,执行 foo 函数,同步输出 first promise constructor,

  • 继续执行 foo 函数,遇见 promise1,执行 promise1 构造函数,同步输出 second promise constructor,以及 end here。同时按照顺序:setTimeout 回调进入任务队列(宏任务),promise1 的完成处理函数(第 18 行)进入任务队列(微任务),第一个(匿名) promise 的完成处理函数(第 23 行)进入任务队列(微任务)

  • 虽然 setTimeout 回调率先进入任务队列,但是优先执行微任务,按照微任务顺序,先输出 promise1(promise1 结果),再输出 promise0(第一个匿名 promise 结果)

  • 此时所有微任务都处理完毕,执行宏任务,输出 setTimeout 回调内容 setTimeout here

讨论实现功能

移动页面上元素 target(document.querySelectorAll('#man')[0])

先从原点出发,向左移动 20px,之后再向上移动 50px,最后再次向左移动 30px,请把运动动画实现出来。

回调方案导致的回调地狱

const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
    position: absolute;
    left: 0px;
    top: 0px
`

const walk = (direction, distance, callback) => {
    setTimeout(() => {
        let currentLeft = parseInt(target.style.left, 10)
        let currentTop = parseInt(target.style.top, 10)

        const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)

        if (shouldFinish) {
            // 任务执行结束,执行下一个回调
            callback && callback()
        }
        else {
            if (direction === 'left') {
                currentLeft--
                target.style.left = `${currentLeft}px`
            }
            else if (direction === 'top') {
                currentTop--
                target.style.top = `${currentTop}px`
            }

            walk(direction, distance, callback)
        }
    }, 20)
}

walk('left', 20, () => {
    walk('top', 50, () => {
        walk('left', 30, Function.prototype)
    })
})

其中walk的第三个参数为回调函数,可以看到这样的回调嵌套很不优雅,有几次位移任务,就会嵌套几层,是名副其实的回调地狱。

Promise 方案

const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
    position: absolute;
    left: 0px;
    top: 0px
`

const walk = (direction, distance) => 
    new Promise((resolve, reject) => {
        const innerWalk = () => {
            setTimeout(() => {
                let currentLeft = parseInt(target.style.left, 10)
                let currentTop = parseInt(target.style.top, 10)

                const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)

                if (shouldFinish) {
                    // 任务执行结束
                    resolve()
                }
                else {
                    if (direction === 'left') {
                        currentLeft--
                        target.style.left = `${currentLeft}px`
                    }
                    else if (direction === 'top') {
                        currentTop--
                        target.style.top = `${currentTop}px`
                    }

                    innerWalk()
                }
            }, 20)
        }
        innerWalk()
    })

walk('left', 20)
    .then(() => walk('top', 50))
    .then(() => walk('left', 30))
  • walk 函数不再嵌套调用,不再执行 callback,而是函数整体返回一个 promise,以利于后续任务的控制和执行
  • 设置 innerWalk 进行每一像素的递归调用
  • 在当前任务结束时(shouldFinish 为 true),resolve 当前 promise

generator 方案

const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
    position: absolute;
    left: 0px;
    top: 0px
`

const walk = (direction, distance) => 
    new Promise((resolve, reject) => {
        const innerWalk = () => {
            setTimeout(() => {
                let currentLeft = parseInt(target.style.left, 10)
                let currentTop = parseInt(target.style.top, 10)

                const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)

                if (shouldFinish) {
                    // 任务执行结束
                    resolve()
                }
                else {
                    if (direction === 'left') {
                        currentLeft--
                        target.style.left = `${currentLeft}px`
                    }
                    else if (direction === 'top') {
                        currentTop--
                        target.style.top = `${currentTop}px`
                    }

                    innerWalk()
                }
            }, 20)
        }
        innerWalk()
    })

function *taskGenerator() {
    yield walk('left', 20)
    yield walk('top', 50)
    yield walk('left', 30)
}
const gen = taskGenerator()

gen.next()
//向左偏移 20 像素
gen.next()
//向上偏移 50 像素
gen.next()
//向左偏移 30 像素

async/await 方案

  • async 声明的函数,其返回值必定是 promise 对象,如果没有显式返回 promise 对象,也会用 Promise.resolve() 对结果进行包装,保证返回值为 promise 类型
  • await 会先执行其右侧表达逻辑(从右向左执行),并让出主线程,跳出 async 函数,而去继续执行 async 函数外的同步代码
    如果 await 右侧表达逻辑是个 promise,让出主线程,继续执行 async 函数外的同步代码,等待同步任务结束后,且该 promise 被 resolve 时,继续执行 await 后面的逻辑
  • 如果 await 右侧表达逻辑不是 promise 类型,那么仍然异步处理,将其理解包装为 promise, async 函数之外的同步代码执行完毕之后,会回到 async 函数内部,继续执行 await 之后的逻辑
const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
    position: absolute;
    left: 0px;
    top: 0px
`

const walk = (direction, distance) => 
    new Promise((resolve, reject) => {
        const innerWalk = () => {
            setTimeout(() => {
                let currentLeft = parseInt(target.style.left, 10)
                let currentTop = parseInt(target.style.top, 10)

                const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)

                if (shouldFinish) {
                    // 任务执行结束
                    resolve()
                }
                else {
                    if (direction === 'left') {
                        currentLeft--
                        target.style.left = `${currentLeft}px`
                    }
                    else if (direction === 'top') {
                        currentTop--
                        target.style.top = `${currentTop}px`
                    }

                    innerWalk()
                }
            }, 20)
        }
        innerWalk()
    })

const task = async function () {
    await walk('left', 20)
        await walk('top', 50)
        await walk('left', 30)
}

 task()

通过对比 generator 和 async/await 这两种方式,读者应该准确认识到,async/await 就是 generator 的语法糖,它能够自动执行生成器函数,更加方便地实现异步流程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值