当前位置: 首页 > article >正文

【JavaScript】异步编程汇总

异步编程解决方案:

  1. 回调函数
  2. Promise
  3. Generator
  4. await / async

回调函数

回调函数是早期处理异步编程的主要方式,虽然它本身存在很多的缺陷,比如那个时候对于复杂的异步处理常常会出现回调地狱。

但是因为 JavaScript 中当时并没有很好的API来帮助我们以比较优雅的方式编写代码,所以依然应用非常广泛。

function fetchData(url, callback) {
    setTimeout(() => {
        callback(url + "/data")
    }, 2000)
}

fetchData('https://www.example.com', (data) => {
    console.log(data)
})

回调地狱(金字塔式代码):可读性差、维护困难、错误处理复杂。

function fetchData(url, callback) {
    setTimeout(() => {
        callback(url + "/data")
    }, 2000)
}

fetchData('https://www.example.com', (data) => {
    console.log(data)
    fetchData(data + 'data2', (data2) => {
        console.log(data2)
        fetchData(data2 + 'data3', (data3) => {
            console.log(data3)
        })
    })
})

并且从回调函数的设计者和使用者的角度,我们还必须考虑两个问题:

  1. 从方法设计者的角度:我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等;
  2. 从方法使用者的角度:对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以便可以理解它这个函数到底怎么用;

为了解决回调地狱,也为了提供一种统一的异步处理方案,提出了 Promise ,async/ await 等。

Promise

Promise是一个类,可以翻译成承诺、许诺、期约;

当我们需要的时候,给予调用者一个承诺返回(比如我们的fetchData就会返回一个承诺):待会儿我会给你回调数据时,就可以创建一个Promise 的对象;在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor

这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject;
当我们调用resolve 回调函数时,会执行Promise 对象的then方法传入的回调函数;
当我们调用reject 回调函数时,会执行Promise对象的catch方法传入的回调函数;

我们可以将它划分成三个状态:

待定(pending): 初始状态,既没有被兑现,也没有被拒绝;当执行executor中的代码时,处于该状态;
已兑现 fulfilled :意味着操作成功完成;执行了resolve 时,处于该状态,Promise已经被兑现;
已拒绝(rejected):意味着操作失败;执行了reject时,处于该状态,Promise已经被拒绝;

function fetchData(url, callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 成功: resolve
            resolve(url + "/data")
            // 失败
            // reject(new Error("请求失败"))
        }, 2000)
    })
}

// resolve => then  reject => catch
fetchData('https://www.example.com').then(data => {}).catch(err => {})

之前使用回调函数的示例用 Promise 重构

function fetchData(url, callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 成功: resolve
            resolve(url + "/data")
            // 失败
            // reject(new Error("请求失败"))
        }, 2000)
    })
}

// resolve => then  reject => catch
fetchData('https://www.example.com').then(data => {
    console.log(data)
    fetchData(data + '/data2').then(data2 => {
        console.log(data2)
    })
}).catch(err => {})

但是会发现这样仍然存在回调地狱。所以我们使用:

function fetchData(url, callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 成功: resolve
            resolve(url + "/data")
            // 失败
            // reject(new Error("请求失败"))
        }, 2000)
    })
}

// resolve => then  reject => catch
fetchData('https://www.example.com').then(data => {
    console.log(data)
    return  fetchData(data + '/data2')
}).then(data2 => {
    console.log(data2)
})

Generator

生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。

平时我们会编写很多的函数,这些函数终止的条件是什么呢?

函数执行完
函数返回,比如return xxx
函数发生异常,比如throw错误

生成器函数也是一个函数,但是和普通的函数有一些区别:

首先,生成器函数需要在function的后面加一个符号:*
其次,生成器函数可以通过yield关键字来控制函数的执行流程:
最后,生成器函数的返回值是一个Generator(生成器):

function* foo() {
    console.log('111')
    yield
    console.log('222')
    yield
    console.log('333')
}
const gen = foo()
gen.next() // 111
gen.next() // 222
gen.next() // 333
gen.next() // (无输出)

生成器函数本身也是一个迭代器。

function* foo() {
    console.log('111')
    yield '1'
    console.log('222')
    yield '2'
    console.log('333')
    return ""
}
const gen = foo()
const res1 = gen.next() // 111
console.log(res1) // {value: '1', done: false}
const res2 = gen.next() // 222
console.log(res2) // {value: '2', done: false}
const res3 = gen.next() // 333
console.log(res3) // {value: "", done: true}
const res4 = gen.next()
console.log(res4) // {value: undefined, done: true}
function* foo(value1) {
    console.log('first:', value1)
    const value2 = yield '1'
    console.log('second:', value2)
    const value3 = yield '2'
    console.log('third:', value3)
    return ""
}

const gen = foo('arg1')
const res1 = gen.next()
console.log(res1.value)
const res2 = gen.next('arg2')
console.log(res2.value)
const res3 = gen.next('arg3')
console.log(res3.value)
// first: arg1
// 1
// second: arg2
// 2
// third: arg3

之前使用回调函数的示例用 Generator 重构

function fetchData(url, callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(url + "/data")
        }, 2000)
    })
}

function* getData() {
    const res1 = yield fetchData("https://example.com")
    console.log(res1)
    const res2 = yield fetchData(res1 + "/data1")
    console.log(res2)
}
const gen = getData()
gen.next().value.then(res => {
    gen.next(res).value.then(res => {
        gen.next(res)
    })
})
// https://example.com/data
// https://example.com/data/data1/data

目前我们的写法有两个问题:

第一,我们不能确定到底需要调用几层的Promise关系;
第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?

那么我们能不能使用一种方法让getData自动来执行呢?其实是可以的,这里有两种方法:

方法一:使用co库(https://github.com/tj/co)
方法二:自己编写一个自动执行的函数

这里我们自己编写一个自动执行的函数:

function fetchData(url, callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(url + "/data")
        }, 2000)
    })
}

function* getData() {
    const res1 = yield fetchData("https://example.com")
    console.log(res1)
    const res2 = yield fetchData(res1 + "/data1")
    console.log(res2)
}

/**
 * 自动执行生成器函数
 * @param genFn 生成器函数
 */
function execGenFn(genFn) {
    const gen = genFn()
    // 如果使用 while 循环 由于while是同步任务 而yield是异步任务 所以代码很不好写,很容易造成死循环
    // 所以使用递归的方式
    function execFn(res) {
        const { value, done } = gen.next(res)
        if (done) {
            return
        }
        value.then(res => {
            execFn(res)
        })
    }
    execFn()
}
execGenFn(getData)
// https://example.com/data
// https://example.com/data/data1/data

async/await

以上代码还是有些复杂,因此 ES2017 出现了 async/await ,而 ES2015出现的 Promise,因此 Generator 相当于一个过渡的异步编程方案。async/await 就是最新的改进。

之前使用回调函数的示例用 async/await 重构

function fetchData(url, callback) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(url + "/data")
        }, 2000)
    })
}

async function getData() {
    const res1 = await fetchData("https://example.com")
    console.log(res1)
    const res2 = await fetchData(res1 + "/data1")
    console.log(res2)
}

getData()

async 函数的若干种写法

async: 关键字用于声明一个异步函数:

  • async是asynchronous.单词的缩写,异步、非同步;
  • sync是synchronous.单词的缩写,同步、同时;

异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行。

异步函数有返回值时(new Promise 传入函数参数中函数的返回值同理),和普通函数会有区别:

  • 情况一:异步函数也可以有返回值,但是异步函数的返回值会被包裹到 Promise.resolve中;
  • 情况二:如果我们的异步函数的返回值是Promise, Promise.resolve 的状态会由 Promise决定;
  • 情况三:如果我们的异步函数的返回值是一个对象并且实现了 thenable,那么会由对象的then方法来决定;

如果 async 中出现异常,不会像普通函数那样报错,而是作为 Promise 的 reject 来传递。

// 返回普通值,会被包裹在 Promise.resolve() 中
async function foo() {
    return "aaa"
}
// 返回一个 Promise 会等待到它的 resolve 或 reject
async function foo2() {
    return Promise.resolve("bbb")
}

// 返回一个 thenable 对象,会被转为 Promise
async function foo3() {
    return {
        then(resolve, reject) {
            resolve("ccc")
        }
    }
}

foo().then(res => {
    console.log(res)
})
foo2().then(res => {
    console.log(res)
})
foo3().then(res => {
    console.log(res)
})
// 执行顺序: aaa ccc bbb

输出结果都正常,但是顺序可能很多人不理解,这也是一个难点,说明如下。

先来说说 thenable 对象,延迟执行微任务一次。

async function foo() {
    return "aaa"
}
async function foo2() {
    // thenable 会延迟一次微任务
    return {
        then(resolve, reject) {
            resolve("ccc")
        }
    }
}
foo().then(res => {
    console.log(res)
}).then(() => {
    console.log("aaa2")
}).then(() => {
    console.log("aaa3")
})
foo2().then(res => {
    console.log(res)
})
// 如果没有 thenable ,顺序为 aaa ccc aaa2 aaa3
// 但真正的输出:
// aaa
// aaa2
// ccc
// aaa3

然后是 Promise.resolve(“bbb”) ,它返回的 then 的微任务延迟执行两次。

async function foo() {
    return "aaa"
}
async function foo2() {
    return Promise.resolve("bbb")
}
foo().then(res => {
    console.log(res)
}).then(() => {
    console.log("aaa2")
}).then(() => {
    console.log("aaa3")
}).then(() => {
    console.log("aaa4")
})
foo2().then(res => {
    console.log(res)
})
// 如果没有 thenable ,顺序为 aaa ccc aaa2 aaa3
// 但真正的输出:
// aaa
// aaa2
// aaa3
// bbb
// aaa4

await 的使用

async 函数另外一个特殊之处就是可以在它内部使用await:关键字,而普通函数中是不可以的。

await关键字有什么特点呢?

通常使用await 是后面会跟上一个表达式,这个表达式通常会返回一个Promise。那么awaits会等到Promise的状态变成fulfilled状态,之后继续执行异步函数。

await不同值的处理:

  • 如果await后面是一个普通的值,那么会直接返回这个值;
  • 如果await后面是一个thenable的对象,那么会根据对象的then方法调用来决定后续的值;
  • 如果await 后面的表达式,返回的Promise是reject的状态,那么会将这个reject结果直接作为函数的Promise的reject值;
async function foo() {
    const res = await new Promise(resolve =>{
        setTimeout(() => {
            resolve('111')
        }, 2000)
    })
    // 以下会被放在微任务中执行
    console.log(res)
}
foo()

async function foo() {
    const res = await new Promise((resolve, reject) => {
        setTimeout(() => {
            reject(new Error('foo error'))
        }, 2000)
    })
    console.log(res)
}
foo().catch(err => {
    console.log(err)
})

异步编程面试题

回调函数在异步编程中的作用和缺点

  1. 回调函数是前端开发中非常重要的一种编程方式

因为 JavaScript 是支持函数式编程的,所以函数可以作为第一公民传递给另外一个函数。
那么另外一个函数在合适的时机可以反过来调用这个函数,被调用的这个函数我们就称之为是回调函数。

  1. 在早期的 JavaScript 异步编程中,回调函数的应用是非常广泛的:

比如说网络请求、用户交互的事件回调、Timer定时器等;
我们都不确定事件在什么时候完成,所以我们需要通过回调函数来监听事件的完成,并且执行对应的操作。
这样做一方面不会引起我们主线程的阻塞,另一方面可以在合适的时机去执行某些特定函数的代码。

  1. 但是回调函数也是有缺点的,因为我们经常需要在一个异步回调中,去执行其他的异步操作,而其他的异步操作往往又会有对应的回调函数,这样就会引起回掉地狱(Callback Hell)。

比如我们在早期的开发中,一个网络请求获取到数据后,我们会根据这个请求立马就发送另外一个请求,并且获取到数据后可能再次回发送另外一个请求。
也就是多个异步操作需要按照某种特定的顺序执行时,常常会产生回调地址。
这种依赖关系可能会引起回调函数的嵌套,造成我们的代码可读性、可维护性变差。
另外回调函数的错误处理机制相对复杂,处理起来也非常麻烦。

解决回调地狱

在早期没有 Promise 的情况下,解决回调地狱确实是比较棘手的一个问题。但是如果项目不引入解决方案,往往会让代码后期非常复杂,难以维护,所以我在架构项目时,对多异步编程的代码就会制定统一的规范:

  1. 方案一:函数单独封装

当你遇到复杂的嵌套回调时,可以将每个异步的步骤单独抽取成函数,来避免回调地狱。
这种方式是将回调函数抽取到外部,单独去调用,这样代码结构会更加扁平化,便于理解和维护。

  1. 方案二:使用Async.js库

Async.js是一个非常流行的控制流库,这个库提供了许多的高阶函数来简化异步的操作。
比如waterfall,可以按照顺序执行一系列的异步任务,并且可以将结果传递给下一个任务。
当然,如果不想引入这种第三方库,我们也可以自己来封装实现。

  1. 也有其他的解决方案,比如nodejs中基于事件驱动,当有异步的结果时会发出一个事件,在其他地方来监听事件进行后续的操作,避免回调的嵌套。

当然,在Promise、Generator、await、async出现之后,对于异步的处理,变得非常的简单和优雅了。

Promise 是什么

Promise是一种用于处理异步操作的 JavaScript 类,可以通过这个类创建出Promise 的对象。

当我们创建一个Promise对象返回给其他人时,相当于给到其他人一个"承诺”,这个承诺会在之后的某个时间点“兑现”或者“拒绝”。
因为对于一个Promise来说有三种状态:Pending(等待)、Fulfilled(兑现或者完成)、Rejected(拒绝或者失败)。
Promise允许我们通过.then() 方法处理成功的结果,通过.catch() 方法处理失败的结果。
这种链式调用极大地提高了异步代码的可读性和维护性。

  1. 所以Promisei引入的核心就是提供一种更加优雅的方式来处理异步操作,避免传统的回调函数复杂性。
  2. 在Promisei引入之前,JavaScript 主要依赖回调函数来处理异步操作。

这种方式在处理简单异步任务时还算有效,但随着异步操作的复杂性增加,特别是在多个异步任务需要依次或并行执行时,回调函数会导致所谓的“回调地狱”问题。
回调地狱使得代码变得难以阅读、调试和维护。

  1. 虽然之前也有解决方案,但是存在两个问题:

解决方案是基于现有的回调函数缺点提出的,其实并不优雅。
不同的项目、企业可能采用不同的解决方案,没有统一的标准。

生成器

  1. 生成器(Generator)是 JavaScript 中一种特殊的函数类型,和普通的函数相比,它在定义时是通过function*语法定义。

普通的函数只有等到执行完毕或者return 或者抛出异常时才会终止。
而生成器函数可以控制它的“暂停”和“恢复”执行。
生成器函数每次调用这个迭代器的next() 方法时,生成器函数会从上次暂停的地方继续执行,直到遇到yield表达式或者函数结束。

  1. 生成器函数的核心特点是它可以暂停执行,并通过 yield 关键字将控制权交还给调用者,同时还能保留当前的执行上下文。
  2. 在异步编程中,生成器可以被用来简化复杂的异步逻辑,在没有引入await、async之前,我们可以借助于Promise+Generator 实现await、async的功能(因为await、async是到ES8才成为标准的)

可以通过生成器函数来代替async的功能
可以通过yield来代替await的功能

  1. 只是代码的执行需要借助于co库或者我们自己编写代码来让生成器函数可以自动执行。

async/await 是什么

  1. async和await是ES2017(ES8) 引入的两个关键字,它们目的是让我们的异步代码处理起来更加的优雅,可以把异步代码像同步代码那样去编写,这样可以提供我们代码的可读性、可维护性。
  2. async 用于声明一个异步函数,它本质上是一个返回Promise的函数。

无论函数内部是否手动的返回一个Promise对象,都会将返回值包装到一个Promise来处理;
如果我们的异步函数的返回值是Promise, Promise.resolve的状态会由Promise决定;
如果我们的异步函数的返回值是一个对象并且实现了thenable, 那么会由对象的then方法来决定;
如果函数内部发生了异常,那么函数就会执行Promise的reject操作。

  1. await只能在async 函数内部使用,它用于等待一个Promise 的结果。

通常使用await: 是后面会跟上一个表达式,这个表达式会返回一个Promise;
那么await 会等到Promise的状态变成fulfilled 状态,之后继续执行异步函数;

  1. async/await. 与Promisel的优势和不同:
  1. 编写方便:在处理多个异步操作时,我们可以通过await关键字轻松实现同步代码的编写方式,编写起来更加方便。
  2. 代码可读性和可维护性:使用async/awaiti语法,我们可以像编写同步代码那样来编写异步代码,避免了Promise链式调用的嵌套。
  3. 错误处理:在Promiser中,错误处理通常通过.catch()方法进行,而在async/await中,我们可以直接使用try.catch 来捕获和处理异常。

http://www.kler.cn/a/543320.html

相关文章:

  • pdf.js默认显示侧边栏和默认手形工具
  • Mac上搭建宝塔环境并部署PHP项目
  • mysql读写分离与proxysql的结合
  • 玩转观察者模式
  • 探秘Hugging Face与DeepSeek:AI开源世界的闪耀双子星
  • 26~31.ppt
  • Lua语言的安全开发
  • 一文讲清springboot所有注解
  • 小蓝相机启动阶段trace学习笔记
  • 每日一题——括号生成
  • Selenium:网页frame与多窗口处理
  • 【大模型】阿里云百炼平台对接DeepSeek-R1大模型使用详解
  • Linux命名管道与共享内存
  • Linux之kernel(1)系统基础理论(2)
  • 51单片机俄罗斯方块整行消除函数
  • 数字人技术之LatentSync Win11本地部署
  • FPGA视频缩放转GTY光口传输,基于Aurora 8b/10b编解码架构,提供3套工程源码和技术支持
  • 数据结构 day02
  • 基于python sanic框架,使用Nacos进行微服务管理
  • elment-plus的表单的其中一项通过了验证再去走别的函数怎么写,不是全部内容通过验证
  • 银河麒麟kylin V10安装docker时出现的问题
  • 大数据学习之SparkStreaming、PB级百战出行网约车项目一
  • 数据可视化:让数据讲故事的力量
  • AI前端开发社区与资源:效率提升的秘密武器
  • 『哈哥赠书 - 55期』-『码农职场:IT人求职就业手册』
  • 使用 EMQX 接入 LwM2M 协议设备