Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >你真的懂异步编程吗?

你真的懂异步编程吗?

原创
作者头像
西岭老湿
修改于 2021-01-12 09:59:27
修改于 2021-01-12 09:59:27
8700
举报
文章被收录于专栏:西岭老湿西岭老湿

为什么要学习异步编程?

在JS 代码中,异步无处不在,Ajax通信,Node中的文件读写等等等,只有搞清楚异步编程的原理和概念,才能在JS的世界中任意驰骋,随便撒欢;

单线程 JavaScript 异步方案

首先我们需要了解,JavaScript 代码的运行是单线程,采用单线程模式工作的原因也很简单,最早就是在页面中实现 Dom 操作,如果采用多线程,就会造成复杂的线程同步问题,如果一个线程修改了某个元素,另一个线程又删除了这个元素,浏览器渲染就会出现问题;

单线程的含义就是: JS执行环境中负责执行代码的线程只有一个;就类似于只有一个人干活;一次只能做一个任务,有多个任务自然是要排队的;

优点:安全,简单

缺点:遇到任务量大的操作,会阻塞,后面的任务会长时间等待,出现假死的情况;

image-20201224170055928.gif
image-20201224170055928.gif

为了解决阻塞的问题,Javascript 将任务的执行模式分成了两种,同步模式( Synchronous)和 异步模式( Asynchronous)

后面我们将分以下几个内容,来详细讲解 JavaScript 的同步与异步:

1、同步模式与异步模式

2、事件循环与消息队列

3、异步编程的几种方式

4、Promise 异步方案、宏任务/微任务队列

5、Generator 异步方案、 Async / Await语法糖

同步与异步

代码依次执行,后面的任务需要等待前面任务执行结束后,才会执行,同步并不是同时执行,而是排队执行;

先来看一段代码:

代码语言:txt
AI代码解释
复制
console.log('global begin')
function bar () {
  console.log('bar task')
}
function foo () {
  console.log('foo task')
  bar()
}
foo()
console.log('global end')

动画形式展现 同步代码 的执行过程:

image-20201224190320238.gif
image-20201224190320238.gif

代码会按照既定的语法规则,依次执行,如果中间遇到大量复杂任务,后面的代码则会阻塞等待;

再来看一段异步代码:

代码语言:txt
AI代码解释
复制
console.log('global begin')

setTimeout(function timer1 () {
  console.log('timer1 invoke')
}, 1800)

setTimeout(function timer2 () {
  console.log('timer2 invoke')
  setTimeout(function inner () {
    console.log('inner invoke')
  }, 1000)
}, 1000)

console.log('global end')

异步代码的执行,要相对复杂一些:

image-20201224190320240.gif
image-20201224190320240.gif

代码首先按照同步模式执行,当遇到异步代码时,会开启异步执行线程,在上面的代码中,setTimeout 会开启环境运行时的执行线程运行相关代码,代码运行结束后,会将结果放入到消息队列,等待 JS 线程结束后,消息队列的任务再依次执行;

流程图如下:

clipboard.png
clipboard.png

回调函数

通过上图,我们会看到,在整个代码的执行中,JS 本身的执行依然是单线程的,异步执行的最终结果,依然需要回到 JS 线程上进行处理,在JS中,异步的结果 回到 JS 主线程 的方式采用的是 “ 回调函数 ” 的形式 , 所谓的 回调函数 就是在 JS 主线程上声明一个函数,然后将函数作为参数传入异步调用线程,当异步执行结束后,调用这个函数,将结果以实参的形式传入函数的调用(也有可能不传参,但是函数调用一定会有),前面代码中 setTimeout 就是一个异步方法,传入的第一个参数就是 回调函数,这个函数的执行就是消息队列中的 “回调”;

下面我们自己封装一个 ajax 请求,来进一步说明回调函数与异步的关系

Ajax 的异步请求封装

代码语言:txt
AI代码解释
复制
function myAjax(url,callback) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (this.readyState == 4) {
            if (this.status == 200) {
                // 成功的回调
                callback(null,this.responseText)
            } else {
                // 失败的回调
                callback(new Error(),null);
            }
        }
    }

    xhr.open('get', url)
    xhr.send();
}

上面的代码,封装了一个 myAjax 的函数,用于发送异步的 ajax 请求,函数调用时,代码实际是按照同步模式执行的,当执行到 xhr.send() 时,就会开启异步的网络请求,向指定的 url 地址发送网络请求,从建立网络链接到断开网络连接的整个过程是异步线程在执行的;换个说法就是 myAjax 函数执行到 xhr.send() 后,函数的调用执行就已经结束了,如果 myAjax 函数调用的后面有代码,则会继续执行,不会等待 ajax 的请求结果;

但是,myAjax 函数调用结束后,ajax 的网络请求却依然在进行着,如果想要获取到 ajax 网络请求的结果,我们就需要在结果返回后,调用一个 JS 线程的函数,将结果以实参的形式传入:

代码语言:txt
AI代码解释
复制
myAjax('./d1.json',function(err,data){
    console.log(data);
})

回调函数让我们轻松处理异步的结果,但是,如果代码是异步执行的,而逻辑是同步的; 就会出现 “回调地狱”,举个栗子:

代码B需要等待代码A执行结束才能执行,而代码C又需要等待代码B,代码D又需要等待代码C,而代码 A、B、C都是异步执行的;

代码语言:txt
AI代码解释
复制
// 回调函数 回调地狱 
myAjax('./d1.json',function(err,data){
    console.log(data);
    if(!err){
        myAjax('./d2.json',function(err,data){
            console.log(data);
            if(!err){
                myAjax('./d3.json',function(){
                    console.log(data);
                })
            }
        })
    }
})

没错,代码执行是异步的,但是异步的结果,是需要有强前后顺序的,著名的"回调地狱"就是这么诞生的;

相对来说,代码逻辑是固定的,但是,这个编码体验,要差很多,尤其在后期维护的时候,层级嵌套太深,让人头皮发麻;

如何让我们的代码不在地狱中受苦呢?

有请 Promise 出山,拯救程序员的头发;

Promise

Snipaste_2020-11-20_14-00-99.gif
Snipaste_2020-11-20_14-00-99.gif

Promise 译为 承诺、许诺、希望,意思就是异步任务交给我来做,一定(承诺、许诺)给你个结果;在执行的过程中,Promise 的状态会修改为 pending ,一旦有了结果,就会再次更改状态,异步执行成功的状态是 Fulfilled , 这就是承诺给你的结果,状态修改后,会调用成功的回调函数 onFulfilled 来将异步结果返回;异步执行成功的状态是 Rejected, 这就是承诺给你的结果,然后调用 onRejected 说明失败的原因(异常接管);

将前面对 ajax 函数的封装,改为 Promise 的方式;

Promise 重构 Ajax 的异步请求封装

代码语言:txt
AI代码解释
复制
function myAjax(url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function () {
            if (this.readyState == 4) {
                if (this.status == 200) {
                    // 成功的回调
                    resolve(this.responseText)
                } else {
                    // 失败的回调
                    reject(new Error());
                }
            }
        }

        xhr.open('get', url)
        xhr.send();
    })
}

还是前面提到的逻辑,如果返回的结果中,又有 ajax 请求需要发送,可一定记得使用链式调用,不要在then中直接发起下一次请求,否则,又是地狱见了:

代码语言:txt
AI代码解释
复制
//  ==== Promise 误区====
myAjax('./d1.json').then(data=>{
    console.log(data);
    myAjax('./d2.json').then(data=>{
        console.log(data)
        // ……回调地狱……
    })
})

链式的意思就是在上一次 then 中,返回下一次调用的 Promise 对象,我们的代码,就不会进地狱了;

代码语言:txt
AI代码解释
复制
myAjax('./d1.json')
    .then(data=>{
    console.log(data);
    return myAjax('./d2.json')
})
    .then(data=>{
    console.log(data)
    return myAjax('./d3.json')
})
    .then(data=>{
    console.log(data);
})
    .catch(err=>{
    console.log(err);
})

虽然我们脱离了回调地狱,但是 .then 的链式调用依然不太友好,频繁的 .then 并不符合自然的运行逻辑,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。于是,在 Promise 的基础上,Async 函数来了;

终极异步解决方案,千呼万唤的在 ES2017中发布了;

Async/Await 语法糖

Async 函数使用起来,也是很简单,将调用异步的逻辑全部写进一个函数中,函数前面使用 async 关键字,在函数中异步调用逻辑的前面使用 await ,异步调用会在 await 的地方等待结果,然后进入下一行代码的执行,这就保证了,代码的后续逻辑,可以等待异步的 ajax 调用结果了,而代码看起来的执行逻辑,和同步代码几乎一样;

代码语言:txt
AI代码解释
复制
 async function callAjax(){
     var a = await myAjax('./d1.json')
     console.log(a);
     var b = await myAjax('./d2.json');
     console.log(b)
     var c = await myAjax('./d3.json');
     console.log(c)
 }
callAjax();

注意:await 关键词 只能在 async 函数内部使用

因为使用简单,很多人也不会探究其使用的原理,无非就是两个 单词,加到前面,用就好了,虽然会用,日常开发看起来也没什么问题,但是一遇到 Bug 调试,就凉凉,面试的时候也总是知其然不知其所以然,咱们先来一个面试题试试,你看你能运行出正确的结果吗?

async 面试题

请写出以下代码的运行结果:

代码语言:txt
AI代码解释
复制
setTimeout(function () {
    console.log('setTimeout')
}, 0)

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

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

console.log('script start')

async1();

console.log('script end')

答案我放在最后面,你也可以自己写出来运行一下;

想要把结果搞清楚,我们需要引入另一个内容:Generator 生成器函数;

Generator 生成器函数,返回 遍历器对象,先看一段代码:

Generator 基础用法

代码语言:txt
AI代码解释
复制
function * foo(){
    console.log('test');
    // 暂停执行并向外返回值 
    yield 'yyy'; // 调用 next 后,返回对象值
    console.log(33);
}

// 调用函数 不会立即执行,返回 生成器对象
const generator =  foo();

// 调用 next 方法,才会 *开始* 执行 
// 返回 包含 yield 内容的对象 
const yieldData = generator.next();

console.log(yieldData) //=> {value: "yyy", done: false}
// 对象中 done ,表示生成器是否已经执行完毕
// 函数中的代码并没有执行结束

// 下一次的 next 方法调用,会从前面函数的 yeild 后的代码开始执行
console.log(generator.next()); //=> {value: undefined, done: true}

你会发现,在函数声明的地方,函数名前面多了 * 星号,函数体中的代码有个 yield ,用于函数执行的暂停;简单点说就是,这个函数不是个普通函数,调用后不会立即执行全部代码,而是在执行到 yield 的地方暂停函数的执行,并给调用者返回一个遍历器对象,yield 后面的数据,就是遍历器对象的 value 属性值,如果要继续执行后面的代码,需要使用 遍历器对象中的 next() 方法,代码会从上一次暂停的地方继续往下执行;

是不是so easy 啊;

同时,在调用next 的时候,还可以传递参数,函数中上一次停止的 yeild 就会接受到当前传入的参数;

代码语言:txt
AI代码解释
复制
function * foo(){
    console.log('test');
    // 下次 next 调用传参接受
    const res = yield 'yyy'; 
    console.log(res);
}

const generator =  foo();

// next 传值 
const yieldData = generator.next();
console.log(yieldData) 

// 下次 next 调用传参,可以在 yield 接受返回值
generator.next('test123');

Generator 的最大特点就是让函数的运行,可以暂停,不要小看他,有了这个暂停,我们能做的事情就太多,在调用异步代码时,就可以先 yield 停一下,停下来我们就可以等待异步的结果了;那么如何把 Generator 写到异步中呢?

Generator 异步方案

将调用ajax的代码写到 生成器函数的 yield 后面,每次的异步执行,都要在 yield 中暂停,调用的返回结果是一个 Promise 对象,我们可以从 迭代器对象的 value 属性获取到Promise 对象,然后使用 .then 进行链式调用处理异步结果,结果处理的代码叫做 执行器,就是具体负责运行逻辑的代码;

代码语言:txt
AI代码解释
复制
function ajax(url) {
    ……
}

// 声明一个生成器函数
function * fun(){
    yield myAjax('./d1.json')
    yield myAjax('./d2.json')
    yield myAjax('./d3.json')
}

// 返回 遍历器对象 
var f = fun();
// 生成器函数的执行器 
// 调用 next 方法,执行异步代码
var g = f.next();
g.value.then(data=>{
    console.log(data);
    // console.log(f.next());
    g = f.next();
    g.value.then(data=>{
        console.log(data)
        // g.......
    })
})

而执行器的逻辑中,是相同嵌套的,因此可以写成递归的方式对执行器进行改造:

代码语言:txt
AI代码解释
复制
// 声明一个生成器函数
function * fun(){
    yield myAjax('./d1.json')
    yield myAjax('./d2.json')
    yield myAjax('./d3.json')
}

// 返回 遍历器对象 
var f = fun();
// 递归方式 封装
// 生成器函数的执行器
function handle(res){
    if(res.done) return;
    res.value.then(data=>{
        console.log(data)
        handle(f.next())
    })
}
handle(f.next());

然后,再将执行的逻辑,进行封装复用,形成独立的函数模块;

代码语言:txt
AI代码解释
复制
function co(fun) {
    // 返回 遍历器对象 
    var f = fun();
    // 递归方式 封装
    // 生成器函数的执行器
    function handle(res) {
        if (res.done) return;
        res.value.then(data => {
            console.log(data)
            handle(f.next())
        })
    }
    handle(f.next());
}

co(fun);

封装完成后,我们再使用时,只需要关注 Generator 中的 yield 部分就行了

代码语言:txt
AI代码解释
复制
function co(fun) {
    ……
}

function * fun(){
    yield myAjax('./d1.json')
    yield myAjax('./d2.json')
    yield myAjax('./d3.json')
}

此时你会发现,使用 Generator 封装后,异步的调用就变的非常简单了,但是,这个封装还是有点麻烦,有大神帮我们做了这个封装,相当强大:https://github.com/tj/co ,感兴趣看一研究一下,而随着 JS 语言的发展,更多的人希望类似 co 模块的封装,能够写进语言标准中,我们直接使用这个语法规则就行了;

其实你也可以对比一下,使用 co 模块后的 Generator 和 async 这两段代码:

代码语言:txt
AI代码解释
复制
//  async / await 
async function callAjax(){
     var a = await myAjax('./d1.json')
     console.log(a);
     var b = await myAjax('./d2.json');
     console.log(b)
     var c = await myAjax('./d3.json');
     console.log(c)
 }
 
 // 使用 co 模块后的 Generator
 function * fun(){
    yield myAjax('./d1.json')
    yield myAjax('./d2.json')
    yield myAjax('./d3.json')
}

你应该也发现了,async 函数就是 Generator 语法糖,不需要自己再去实现 co 执行器函数或者安装 co 模块,写法上将 * 星号 去掉换成放在函数前面的 async ,把函数体的 yield 去掉,换成 await; 完美……

代码语言:txt
AI代码解释
复制
async function callAjax(){
     var a = await myAjax('./d1.json')
     console.log(a);
     var b = await myAjax('./d2.json');
     console.log(b)
     var c = await myAjax('./d3.json');
     console.log(c)
 }
callAjax();

我们再来看一下 Generator ,相信下面的代码,你能很轻松的阅读;

代码语言:txt
AI代码解释
复制
function * f1(){
    console.log(11)
    yield 2;
    console.log('333')
    yield 4;
    console.log('555')
}

var g = f1();
g.next();
console.log(666);
g.next();
console.log(777);

代码运行结果:

image-20201230193712942.png
image-20201230193712942.png

带着 Generator 的思路,我们再回头看看那个 async 的面试题;

请写出以下代码的运行结果:

代码语言:txt
AI代码解释
复制
setTimeout(function () {
    console.log('setTimeout')
}, 0)

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

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

console.log('script start')

async1();

console.log('script end')

运行结果:

image-20201230193446596.png
image-20201230193446596.png

是不是恍然大明白呢……

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
JavaScript 异步编程
JavaScrip 采用单线程模式工作的原因,需要进行DOM操作,如果多个线程同时修改DOM浏览器无法知道以哪个线程为主。
用户3045442
2020/07/31
1.2K0
JavaScript 异步编程
async/await 原理及执行顺序分析
之前写了篇文章《这一次,彻底理解Promise原理》,剖析了Promise的相关原理。我们都知道,Promise解决了回调地狱的问题,但是如果遇到复杂的业务,代码里面会包含大量的 then 函数,使得代码依然不是太容易阅读。
桃翁
2019/11/08
1.9K0
【JS】236-JS 异步编程六种方案(原创)
我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。
pingan8787
2019/07/25
9850
【JS】236-JS 异步编程六种方案(原创)
javascript异步编程之generator(生成器函数)与asnyc/await语法糖
相比于传统回调函数的方式处理异步调用,Promise最大的优势就是可以链式调用解决回调嵌套的问题。但是这样写依然会有大量的回调函数,虽然他们之间没有嵌套,但是还是没有达到传统同步代码的可读性。如果以下面的方式写异步代码,它是很简洁,也更容易阅读的。
开水泡饭
2022/12/26
3670
JavaScript异步编程
#前言 从我们一开始学习JavaScript的时候就听到过一段话:JS是单线程的,天生异步,适合IO密集型,不适合CPU密集型。但是,多数JavaScript开发者从来没有认真思考过自己程序中的异步到底是怎么出现的,以及为什么会出现,也没有探索过处理异步的其他方法。到目前为止,还有很多人坚持认为回调函数就完全够用了。
leocoder
2018/10/31
1.1K0
JavaScript 异步编程
完整高频题库仓库地址:https://github.com/hzfe/awesome-interview
HZFEStudio
2021/10/30
1K0
Promise、Generator、Async 合集
我们知道Promise与Async/await函数都是用来解决JavaScript中的异步问题的,从最开始的回调函数处理异步,到Promise处理异步,到Generator处理异步,再到Async/await处理异步,每一次的技术更新都使得JavaScript处理异步的方式更加优雅,从目前来看,Async/await被认为是异步处理的终极解决方案,让JS的异步处理越来越像同步任务。异步编程的最高境界,就是根本不用关心它是不是异步。
泯泷、
2024/03/09
1580
JS异步编程
同步(sync)是一件事一件事的执行,只有前一个任务执行完毕才能执行后一个任务。异步(async)相对于同步,程序无须按照代码顺序自上而下的执行。
Cloud-Cloudys
2020/07/06
3.1K0
js异步编程面试题你能答上来几道
在上一节中我们了解了常见的es6语法的一些知识点。这一章节我们将会学习异步编程这一块内容,鉴于异步编程是js中至关重要的内容,所以我们将会用三个章节来学习异步编程涉及到的重点和难点,同时这一块内容也是面试常考范围。
loveX001
2022/10/10
5150
javascript异步编程
简单来说,异步编程就是在执行一个指令之后不是马上得到结果,而是继续执行后面的指令,等到特定的事件触发后,才得到结果。
OECOM
2020/07/01
5840
ES6②
Generator 函数是Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
ymktchic
2022/01/18
4620
ES6②
大厂高频面试精选
key 的作用是为了在 diff 算法执行时更快的找到对应的节点,提高 diff 速度。
grain先森
2019/03/28
8410
大厂高频面试精选
《深入浅出Node.js》:Node异步编程解决方案 之 async函数
关于async函数,需要明确它是generator函数的语法糖,即将生成器函数的*换成async关键字,将yield关键字换成await关键字。使用async函数相比于生成器函数的改进主要在于前者具备内置执行器,即直接调用async函数就能执行完整个函数,就像普通函数调用那样,而无需像生成器函数通过调用返回的迭代器的next()方法来手动执行后续代码,非常方便。此外语义化更友好,并且async函数返回的还是一个Promise对象,可以使用then()方法来指定下一步操作。
前端_AWhile
2019/08/29
1K0
JavaScript 中如何进行异步编程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
江米小枣
2020/06/16
8230
JavaScript 中如何进行异步编程
JavaScript异步编程
平时开发经常会用到js异步编程,由于前端展示页面都是基于网络机顶盒(IPTV的一般性能不太好,OTT较好),目前公司主要采取的异步编程的方式有setTimeout、setInterval、requestAnimationFrame、ajax,为什么会用到异步呢,就拿业务来说,若前端全部采取同步的方式,那加载图片、生成dom、网络数据请求都会大大增加页面渲染时长。
Jack Chen
2018/09/14
9290
JavaScript异步编程
JavaScript异步编程:Generator与Async
JavaScript异步编程:Generator与Async 从Promise开始,JavaScript就在引入新功能,来帮助更简单的方法来处理异步编程,帮助我们远离回调地狱。 Promise是下边要讲的Generator/yield与async/await的基础,希望你已经提前了解了它。 在大概ES6的时代,推出了Generator/yield两个关键字,使用Generator可以很方便的帮助我们建立一个处理Promise的解释器。 然后,在
贾顺名
2018/06/14
1.1K0
JavaScript 异步编程
众所周知,JavaScript 是单线程的,但异步在 js 中很常见,那么简单来介绍一下异步编程
Krry
2020/09/15
6240
Node.js异步编程进化论
我们知道,Node.js中有两种事件处理方式,分别是callback(回调)和EventEmitter(事件发射器)。本文首先介绍的是callback。
童欧巴
2020/03/28
8980
【深扒】深入理解 JavaScript 中的异步编程
虽然整个思路看起来没什么毛病,对吧。但是它就是不行的,获取数据是异步的,也就是说请求数据的时候,输出已经执行了,这时候必然是 undefined
小丞同学
2021/08/18
7580
两个try catch引起的对JS事件循环的思考
最近在跟朋友闲聊时讨论到一个问题,同样都是异步处理,为什么setTimeout回调抛出的异常不能被try catch,
写代码的阿宗
2022/11/07
1.2K0
相关推荐
JavaScript 异步编程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档