大前端进阶-js异步编程

AlgoPulseMaster
• 阅读 1254
学不动了怎么办?换个姿势继续学。

JS引擎

js引擎执行过程分为三个阶段:

  • 解释阶段

js代码加载完毕后进入此阶段,此阶段完成由字符串到机器码的转变过程。

  • 预处理(编译)阶段与执行阶段

由于js是解释性语言,所以会一边编译一边执行。创建变量对象发生在预编译阶段(编译阶段生成执行上下文,执行上下文包含变量,作用域链和this),变量赋值发生在执行阶段。

同步VS异步

浏览器内核是多线程的,和js执行相关的线程包含:

  1. js引擎主线程,负责解释和执行js代码。
  2. ajax请求线程。
  3. DOM处理线程。
  4. 定时任务线程。

所有同步任务都是在js引擎主线程上按照顺序一次执行,形成一个执行栈,任务的执行也是任务在执行栈上的不断入栈和出栈。除了执行栈,事件触发线程管理一个任务队列,异步任务有了结果,会向任务队列放入回调。一旦所有同步任务执行完毕之后(js引擎空闲),会读取任务队列,将任务放入执行栈并执行。

**事件循环**(Event Loop)
指的是脚本运行时,先依次运行执行栈,然后从任务队列中取出事件执行来运行任务队列中的任务,这个过程是不断重复的。

异步编程

回调函数

回调函数是js异步编程的常用方式,但是回调函数容易层层嵌套形成回调地狱。

const getData = (data, callback) => {
    setTimeout(() => {
        callback(data + 1)
    }, 1000)
}

getData(1, function (v1) {
    getData(v1, function (v2) {
        getData(v2)
    })
})

层层嵌套的代码,不仅影响代码逻辑,而且可读性非常差,可以用Promise 对象、Generator 函数、async 函数替代,使得回调扁平化。

Promise

Promise对象提供then方法,then方法可以链式调用,链式调用时then方法依次执行。

const getData = function (data) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(data + 1)
        }, 1000)
    })
}

getData(1)
    .then(v1 => {
        return getData(v1)
    })
    .then(v2 => {
        return getData(v2)
    })

Promise对象不仅能解决回调地狱问题,同时还提供了静态的all和race方法。all方法接收一个数组,当数组中存在异步调用的时候,只有异步方法全部执行完成之后,then方法才执行。race方法同样接受一个数组,与all不同的是,参数数组中的多个异步调用,如果其中一个完成或者失败,那么then方法就会执行,可以用race方法实现ajax超时判断。

const getData = function (data) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(data + 1)
        }, 1000)
    })
}

function timeout(duration) {
    return Promise.race([
        getData(1),
        new Promise((resolve, reject) => {
            setTimeout(() => {
                reject(new Error('调用超时'))
            }, duration)
        })
    ])
}

timeout(500).then(value => {
    console.log(value)
}).catch(err => {
    console.log(err.message)
})

由于设置的超时时间是500毫秒,此时getData异步函数尚未执行完成,因此race方法会捕获到超时错误。

Generator

Generator生成器,可以利用*和yield关键字解决异步嵌套的回调地狱问题。

const getData = function (data) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(data + 1)
        }, 1000)
    })
}

function* gen() {
    let v1 = yield getData(1)
    console.log(v1)
    // yield会使函数暂停执行,当调用next方法时会继续执行,同时next方法传入的参数会作为yield的返回值
    let v2 = yield getData(v1)
    console.log(v2)
}

function co(generator) {
    let g = generator()

    function handleResult(result) {
        if (result.done) return
        else {
            result.value.then((value) => {
                handleResult(g.next(value))
            }, err => {
                console.log(err)
            })
        }
    }

    handleResult(g.next())
}

co(gen)

async

通常async和await一起使用,是一种语法糖,可以理解为通过async和await关键字可以更方便的实现上节中的generator。

const getData = function (data) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(data + 1)
        }, 1000)
    })
}

async function main() {
    let v1 = await getData(1)
    console.log(v1)
    // yield会使函数暂停执行,当调用next方法时会继续执行,同时next方法传入的参数会作为yield的返回值
    let v2 = await getData(v1)
    console.log(v2)
}

main()

微任务vs宏任务

如果执行下面的代码:

setTimeout(() => {
    console.log('timeout')
}, 0)
new Promise(resolve => {
    resolve('promise')
}).then(v => {
    console.log(v)
})

会发现,同样是异步操作,Promise的回调函数会早于setTimeout的回调函数,这就涉及到微任务(Promise等)和宏任务(setTimeout等)。
宏任务和微任务均为异步任务,只是执行顺序不同,通常情况下,会优先执行微任务,当微任务执行完毕之后,再执行宏任务。
盗用网上的执行逻辑图:
大前端进阶-js异步编程
常见的宏任务有:setTimeout(同等延迟时间下,setTimeOut的优先级高于setImmediate),setInterval, setImmediate, I/O, UI rendering。
常见的微任务有:Promise,process.nextTick(在node环境中nextTick优先级高于promise)。

参考:
js运行机制及异步编程(一)
js运行机制及异步编程(二)

点赞
收藏
评论区
推荐文章
小万哥 小万哥
1年前
掌握 C++ 编译过程:面试中常见问题解析
C编译过程C是一种高级编程语言,但是计算机并不能直接理解它。因此,需要将C代码翻译成计算机可以理解的机器语言。这个过程就是编译过程,是C程序从源代码到可执行文件的转换过程,包括预处理、编译、汇编和链接四个阶段。1.预处理在编译器开始编译之前
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Stella981 Stella981
3年前
Maven 构建生命周期
Maven构建生命周期定义了一个项目构建跟发布的过程。一个典型的Maven构建(build)生命周期是由以下几个阶段的序列组成的:阶段处理描述验证validate验证项目验证项目是否正确且所有必须信息是可用的编译compile执行编译源代码编译在此阶段完成测试Test测试使用适当的单元测试框架(例如JUn
Stella981 Stella981
3年前
Javascript解析机制 执行机制
HTML5学堂:在学习JavaScript过程中,我们需要了解事件的机制是怎么执行的?本文将会提到JavaScript事件机制的解析,希望对大家有帮助!javascript解析的过程主要分为两个阶段,分别是编译与执行阶段。在编译期,javascript解释器将完成对javascript代码的预处理,即将javascript代码转换为字节码。在执行
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这