zoukankan      html  css  js  c++  java
  • 侯策《前端开发核心知识进阶》读书笔记——异步

    知识储备:

    阮一峰《ECMAScript 6 入门》读书笔记——Promise 

    阮一峰《ECMAScript 6 入门》读书笔记——async 函数 

    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 的语法糖,它能够自动执行生成器函数,更加方便地实现异步流程。

  • 相关阅读:
    Sqlite框架Delphi10.3(07)
    FastReport 6.8.11在Delphi10.3上的安装
    Delphi 中,InputQuery 函数的妙用
    Javaday25(sax解析xml,json,设计模式)
    并发学习第五篇——volatile关键字
    并发学习第四篇——synchronized关键字
    并发学习第二篇——Thread
    并发学习第一篇——Runnable
    git-仓库迁移并保留commit log
    并发编程理论基础二——画图理解
  • 原文地址:https://www.cnblogs.com/fmyao/p/12802571.html
Copyright © 2011-2022 走看看