웹 프론트엔드 JS 튜토리얼 JavaScript 비동기 프로그래밍에 대한 자세한 소개(예제 포함)

JavaScript 비동기 프로그래밍에 대한 자세한 소개(예제 포함)

Oct 20, 2018 pm 04:39 PM
javascript node.js 프런트 엔드

이 기사는 PHP 시너지 구현에 대한 자세한 설명을 제공합니다(코드 포함). 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

서문

이 글을 쓴 원래 의도는 좀 더 깊은 이해를 원하면 JS와 비동기 프로그래밍은 넘어야 할 장애물입니다. 여기에는 매우 많고 광범위한 내용이 포함되어 있으므로 JS를 처음 접하는 경우 그 당시에는 이 개념을 완전히 이해하지 못했을 수도 있지만, 아직 노출되지 않고 이해되지 않은 지식 포인트가 많이 있습니다. 내가 이미 습득한 지식 중 일부를 사용할 수 있는 용기를 갖고 최선을 다해 말하십시오. JS의 비동기 프로그래밍. 내가 설명한 개념이나 용어 중 일부에 오류가 있는 경우 독자는 나에게 문제를 지적해 달라고 요청하며 변경 사항은 즉시 수정하겠습니다.

동기화 및 비동기

우리는 그것이 브라우저 측에 있는지, 서버에 있는지 알고 있습니다. (노드) 측에서는 JS가 단일 스레드에서 실행됩니다. 브라우저의 JS 실행 스레드를 예로 들어 보겠습니다. 이 스레드에서는 JS가 사용됩니다. 엔진은 실행 컨텍스트 스택을 생성한 다음 코드는 후입선출(last in first out)에 따라 실행 컨텍스트 스택의 일련의 작업과 같은 실행 컨텍스트(전역, 함수, 평가)로 사용됩니다. LIFO) 메소드가 순차적으로 실행됩니다. 동기화의 가장 큰 특징은 이때 JS 등 후속 작업의 실행을 차단한다는 것입니다. 이때 많은 양의 계산이 수행되고 있어 스레드가 차단되어 페이지 렌더링 및 로딩이 일관되지 않게 됩니다(브라우저 측 이벤트 루프). 실행 스택의 각 작업이 실행된 후 이벤트 큐의 작업이 비어 있을 때까지 이벤트 큐의 작업이 확인되고 실행됩니다. 이벤트 큐의 작업은 마이크로 큐와 매크로 큐로 구분됩니다. 마이크로 큐 매크로 큐에 있는 작업은 실행이 완료된 후에만 실행되며 브라우저의 GUI 스레드는 페이지 렌더링(UI 렌더링)을 수행합니다. 이는 실행 스택에서 많은 계산이 수행될 때 페이지 렌더링이 차단되는 이유를 설명합니다.

Asynchrony는 동기화와 반대로 비동기 작업이 완료된 후 수행되는 작업으로 이해될 수 있습니다. 일반적으로 콜백 함수를 사용하거나 Promise 형태를 이벤트 큐에 넣은 후 이벤트 루프(Event Loop) 메커니즘은 폴링될 때마다 비동기 작업이 완료되었는지 확인합니다. 완료되면 해당 작업이 이벤트 큐의 실행 규칙에 따라 순서대로 실행됩니다. 비동기 작업이 동기 작업처럼 완전히 차단되지 않는 것은 바로 이벤트 루프 메커니즘의 존재 덕분입니다. JS 실행 스레드.

비동기 작업에는 일반적으로 네트워크 요청, 파일 읽기 및 데이터베이스 처리가 포함됩니다.

비동기 작업에는 일반적으로 setTimout / setInterval, Promise, requestAnimationFrame(브라우저에 고유), setImmediate(Node Unique)가 포함됩니다. ), process.nextTick(노드에 고유함) 등...

참고: 브라우저 측 및 노드 측의 이벤트 루프 메커니즘은 다릅니다. 아래에 제공된 두 그림은 이 기사의 초점이 JS가 아니기 때문에 다양한 환경에서 이벤트 루프의 작동 메커니즘을 간략하게 보여줍니다. 비동기 프로그래밍은 이를 기반으로 하므로 도움이 필요한 독자에게 도움이 되기를 바라며 아래에 해당 읽기 링크가 제공됩니다. ### ######브라우저 사이드### ## ## ## ## ## ## ## ## ## ## 🎜🎜##### 노드 터미널

JavaScript 비동기 프로그래밍에 대한 자세한 소개(예제 포함)읽기 링크

분석 Node.js의 이벤트 루프 메커니즘

자바스크립트 브라우저의 이벤트 루프 메커니즘에 대한 자세한 설명JavaScript 비동기 프로그래밍에 대한 자세한 소개(예제 포함)

For 비동기 원시 JS 구문

최근 몇 년간의 역사를 되돌아보면 ECMAScript 표준은 거의 매년 업데이트됩니다. 오늘날 JS가 8102에 도달한 것은 바로 ES6와 같은 언어 기능의 주요 업데이트 때문입니다. Java의 비동기 프로그래밍은 콜백 함수만 있던 고대에 비해 큰 발전을 이루었습니다. 다음으로 콜백, 프로미스, 제너레이터, 비동기를 소개하겠습니다. /await의 기본 사용법과 비동기 프로그래밍에서 이를 사용하는 방법입니다.

callback

콜백 함수는 JS에서 구문은 아니지만 비동기 프로그래밍 문제를 해결하기 위해 가장 일반적으로 사용되는 방법이므로 여기서 언급할 필요가 있습니다. 누구나 한 눈에 이해할 수 있는 예가 있습니다.

const foo = function (x, y, cb) {
    setTimeout(() => {
        cb(x + y)
    }, 2000)
}

// 使用 thunk 函数,有点函数柯里化的味道,在最后处理 callback。
const thunkify = function (fn) {
    return function () {
        let args = Array.from(arguments)
        return function (cb) {
            fn.apply(null, [...args, cb])
        }
    }
}

let fooThunkory = thunkify(foo)

let fooThunk1 = fooThunkory(2, 8)
let fooThunk2 = fooThunkory(4, 16)

fooThunk1((sum) => {
    console.log(sum) // 10
})

fooThunk2((sum) => {
    console.log(sum) // 20
})
로그인 후 복사
Promise
ES6 출시 이전에는 비동기 프로그래밍의 주력인 콜백 함수가 콜백 지옥, 어려운 코드 등 여러 이유로 비판을 받아왔습니다. 실행 순서 추적, 나중에 코드가 너무 복잡해 유지 관리 및 업데이트 등이 이루어졌습니다. Promise의 출현으로 이전 딜레마가 크게 바뀌었습니다. 더 이상 고민할 필요 없이 코드로 직접 가서 그 매력을 미리 느껴보고, Promise에서 중요하다고 생각하는 몇 가지 사항을 요약하겠습니다.
const foo = function () {
    let args = [...arguments]
    let cb = args.pop()
    setTimeout(() => {
        cb(...args)
    }, 2000)
}

const promisify = function (fn) {
    return function () {
        let args = [...arguments]
        return function (cb) {
            return new Promise((resolve, reject) => {
                fn.apply(null, [...args, resolve, reject, cb])
            })
        }
    }
}

const callback = function (x, y, isAdd, resolve, reject) {
    if (isAdd) {
        resolve(x + y)
    } else {
        reject('Add is not allowed.')
    }
}

let promisory = promisify(foo)

let p1 = promisory(4, 16, false)
let p2 = promisory(2, 8, true)

p1(callback)
.then((sum) => {
    console.log(sum)
}, (err) => {
    console.error(err) // Add is not allowed.
})
.finally(() => {
    console.log('Triggered once the promise is settled.')
})

p2(callback)
.then((sum) => {
    console.log(sum) // 10
    return 'evil '
})
.then((unknown) => {
    throw new Error(unknown)
})
.catch((err) => {
    console.error(err) // Error: evil 
})
로그인 후 복사

포인트 1: 제어 반전 방지(우려사항 분리)

什么是反控制反转呢?要理解它我们应该先弄清楚控制反转的含义,来看一段伪代码。

const request = require('request')

// 某购物系统获取用户必要信息后执行收费操作
const purchase = function (url) {
    request(url, (err, response, data) => {
        if (err) return console.error(err)
        if (response.statusCode === 200) {
            chargeUser(data)
        }
    })
}

purchase('https://cosmos-alien.com/api/getUserInfo')
로그인 후 복사

显然在这里 request 模块属于第三方库是不能够完全信任的,假如某一天该模块出了 bug , 原本只会向目标 url 发送一次请求却变成了多次,相应的我们的 chargeUser 函数也就是收费操作就会被执行多次,最终导致用户被多次收费,这样的结果完全就是噩梦!然而这就是控制反转,即把自己的代码交给第三方掌控,因此是不可完全信任的。

那么反控制反转现在我们可以猜测它的含义应该就是将控制权交还到我们自己写的代码中,而要实现这点通常我们会引入一个第三方协商机制,在 Promise 之前我们会通过事件监听的形式来解决这类问题。现在我们将代码更改如下:

const request = require('request')
const events = require('events')

const listener = new events.EventEmitter()

listener.on('charge', (data) => {
    chargeUser(data)
})

const purchase = function (url) {
    request(url, (err, response, data) => {
        if (err) return console.error(err)
        if (response.statusCode === 200) {
            listener.emit('charge', data)
        }
    })
}

purchase('https://cosmos-alien.com/api/getUserInfo')
로그인 후 복사

更改代码之后我们会发现控制反转的恢复其实是更好的实现了关注点分离,我们不用去关心 purchase 函数内部具体发生了什么,只需要知道它在什么时候完成,之后我们的关注点就从 purchase 函数转移到了 listener 对象上。我们可以把 listener 对象提供给代码中多个独立的部分,在 purchase 函数完成后,它们同样也能收到通知并进行下一步的操作。以下是维基百科上关于关注点分离的一部分介绍。

关注点分离的价值在于简化计算机程序的开发和维护。当关注点分开时,各部分可以重复使用,以及独立开发和更新。具有特殊价值的是能够稍后改进或修改一段代码,而无需知道其他部分的细节必须对这些部分进行相应的更改。

一一    维基百科

显然在 Promise 中 new Promise() 返回的对象就是关注点分离中分离出来的那个关注对象。

要点二:不可变性 ( 值得信任 )

细心的读者可能会发现,要点一中基于事件监听的反控制反转仍然没有解决最重要的信任问题,收费操作仍旧可以因为第三方 API 的多次调用而被触发且执行多次。幸运的是现在我们拥有 Promise 这样强大的机制,才得以让我们从信任危机中解脱出来。所谓不可变性就是:

Promise 只能被决议一次,如果代码中试图多次调用 resolve(..) 或者 reject(..) ,Promise 只会接受第一次决议,决议后就是外部不可变的值,因此任何通过 then(..) 注册的回调只会被调用一次。

现在要点一中的示例代码就可以最终更改为:

const request = require('request')

const purchase = function (url) {
    return new Promise((resolve, reject) => {
        request(url, (err, response, data) => {
            if (err) reject(err)
            if (response.statusCode === 200) {
                resolve(data)
            }
        })
    })
}

purchase('https://cosmos-alien.com/api/getUserInfo')
.then((data) => {
    chargeUser(data)
})
.catch((err) => {
    console.error(err)
})
로그인 후 복사

要点三:错误处理及一些细节

还记得最开始讲 Promise 时的那一段代码吗?我们把打印结果的那部分代码再次拿出来看看。

p1(callback)
.then((sum) => {
    console.log(sum)
}, (err) => {
    console.error(err) // Add is not allowed.
})
.finally(() => {
    console.log('Triggered once the promise is settled.')
})

p2(callback)
.then((sum) => {
    console.log(sum) // 10
    return 'evil '
})
.then((unknown) => {
    throw new Error(unknown)
})
.catch((err) => {
    console.error(err) // Error: evil 
})
로그인 후 복사

首先我们说下 then(..) ,它的第一个参数作为函数接收 promise 对象中 resolve(..) 的值,第二个参数则作为错误处理函数处理在 Promise 中可能发生的错误。

而在 Promise 中有两种错误可能会出现,一种是显式 reject(..) 抛出的错误,另一种则是代码自身有错误会被 Promise 捕捉,通过 then(..) 中的错误处理函数我们可以接收到它前面 promise 对象中出现的错误,而如果在 then(..) 接收 resolve(..) 值的函数中也出现错误,该错误则会被下一个 then(..) 的错误处理函数所接收 ( 有两个前提,第一是要写出这个 then(..) 否则该错误最终会在全局抛出,第二个则是要确保前一个 then(..) 在它的 Promise 决议后调用的是第一个参数即接收 resolve(..) 值的函数而不是错误处理函数 )。

一些值得注意的细节:

catch(..) 相当于 then(..) 中的错误处理函数 ,只是省略了第一个参数。

finally(..) 在 Promise 一旦决议后 ( 无论是 resolve 还是 reject ) 都会被执行。

then(..) 、catch(..) 、finally(..) 都是异步调用,作为 Event Loop 里事件队列中的微队列任务执行。

generator

generator 也叫做生成器,它是 ES6 中引入的一种新的函数类型,在函数内部它可以多次启动和暂停,从而形成阻塞同步的代码。下面我将先讲述它的基本用法然后是它在异步编程中的使用最后会简单探究一下它的工作原理。

生成器基本用法

let a = 2

const foo = function *(x, y) {
    let b = (yield x) + a
    let c = (yield y) + b
    console.log(a + b + c)
}

let it = foo(6, 8)

let x = it.next().value
a++
let y = it.next(x * 5).value
a++

it.next(x + y) // 84
로그인 후 복사

从上面的代码我们可以看到与普通的函数不同,生成器函数执行后返回的是一个迭代器对象,用来控制生成器的暂停和启动。在常见的设计模式中就有一种模式叫做迭代器模式,它指的是提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器对象 it 包含一个 next(..) 方法且在调用之后返回一个 { done: .. , value: .. } 对象,现在我们先来自己实现一个简单的迭代器。

const iterator = function (obj) {
    let current = -1
    return {
        [Symbol.iterator]() {
            return this
        },
        next() {
            current++
            return { done: current <p>可以看到我们自己实现的迭代器不仅能够手动进行迭代,还能被 for..of 自动迭代展开,这是因为在 ES6 中只要对象具有 Symbol.iterator 属性且该属性返回的是一个迭代器对象,就能够被 for..of 所消费。</p><p>回头来看最开始的那个 generator 示例代码中生成器产生的迭代器对象 it ,似乎它比普通的迭代器有着更强大的功能,其实就是与 yield 表达式紧密相连的消息双向传递。现在我先来总结一下自己认为在生成器中十分重要的点,然后再来分析下那段示例代码的完整执行过程。</p><p>每次调用 it.next() 后生成器函数内的代码就会启动执行且返回一个 { done: .. , value: .. } 对象,一旦遇到 yield 表达式就会暂停执行,如果此时 yield 表达式后面跟有值例如 yield val,那么这个 val 就会被传入返回对象中键名 value 对应的键值,当再次调用 it.next() 时 yield 的暂停效果就会被取消,如果此时的 next 为形如 it.next(val) 的调用,yield 表达式就会被 val 所替换。这就是生成器内部与迭代器对象外部之间的消息双向传递。</p><p>弄清了生成器中重要的特性后要理解开头的那段代码就不难了,首先执行第一个 it.next().value ,遇到第一个 yield 后生成器暂停执行,此时变量 x 接受到的值为 6。在全局环境下执行 a++ 后再次执行 it.next(x * 5).value 生成器继续执行且传入值 30,因此变量 b 的值就为 33,当遇到第二个 yield 后生成器又暂停执行,并且将值 8 传出给变量 y 。再次执行 a++ ,然后执行 it.next(x + y) 恢复生成器执行并传入值 14,此时变量 c 的值就为 47,最终计算 a + b + c 便可得到值 84。</p><p><strong>在异步编程中使用生成器</strong></p><p>既然现在我们已经知道了生成器内部拥有能够多次启动和暂停代码执行的强大能力,那么将它用于异步编程中也便是理所当然的事情了。先来看一个异步迭代生成器的例子。</p><pre class="brush:php;toolbar:false">const request = require('request')

const foo = function () {
    request('https://cosmos-alien.com/some.url', (err, response, data) => {
        if (err) it.throw(err)
        if (response.statusCode === 200) {
            it.next(data)
        }
    })
}

const main = function *() {
    try {
        let result = yield foo()
        console.log(result)
    }
    catch (err) {
        console.error(err)
    }
}

let it = main()

it.next()
로그인 후 복사

这个例子的逻辑很简单,调用 it.next() 后生成器启动,遇到 yield 时生成器暂停运行,但此时 foo 函数已经执行即网络请求已经发出,等到有响应结果时如果出错则调用 it.throw(err) 将错误抛回生成器内部由 try..catch 同步捕获,否则将返回的 data 作为传回生成器的值在恢复执行的同时将 data 赋值给变量 result ,最后打印 result 得到我们想要的结果。

在 ES6 中最完美的世界就是生成器 ( 看似同步的异步代码 ) 和 Promise ( 可信任可组合 ) 的结合,因此我们现在再来看一个由生成器 + Promise 实现异步操作的例子。

const axios = require('axios')

const foo = function () {
    return axios({
        method: 'GET',
        url: 'https://cosmos-alien.com/some.url'
    })
}

const main = function *() {
    try {
        let result = yield foo()
        console.log(result)
    }
    catch (err) {
        console.error(err)
    }
}

let it = main()

let p = it.next().value

p.then((data) => {
    it.next(data)
}, (err) => {
    it.throw(err)
})
로그인 후 복사

这个例子跟前面异步迭代生成器的例子几乎是差不多的,唯一不同的就是 yield 传递出去的是一个 promise 对象,之后我们在 then(..) 中来恢复执行生成器里下一步的操作或是抛出一个错误。

生成器工作原理

在讲了那么多关于 generator 生成器的使用后,相信读者也跟我一样想知道生成器究竟是如何实现能够控制函数内部代码的暂停和启动,从而形成阻塞同步的效果。

我们先来简单了解下有限状态机 ( FSM ) 这个概念,维基百科上给出的解释是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。简单的来说,它有三个主要特征:

  1. 状态总数 ( state ) 是有限的

  2. 任一时刻,只处在一种状态之中

  3. 某种条件下,会从一种状态转变 ( transition ) 到另一种状态

其实生成器就是通过暂停自己的作用域 / 状态来实现它的魔法的,下面我们就以上文的生成器 + Promise 的例子为基础,用有限状态机的方式来阐述生成器的基本工作原理。

let stateRequest = {
    done: false,
    transition(message) {
        this.state = this.stateResult
        console.log(message)
        // state 1
        return foo()
    }
}

let stateResult = {
    done: true,
    transition(data) {
        // state 2
        let result = data
        console.log(result)
    }
}

let stateError = {
    transition(err) {
        // state 3
        console.error(err)
    }
}

let it = {
    init() {
        this.stateRequest = Object.create(stateRequest)
        this.stateResult = Object.create(stateResult)
        this.stateError = Object.create(stateError)
        this.state = this.stateRequest
    },
    next(data) {
        if (this.state.done) {
            return {
                done: true,
                value: undefined
            }
        } else {
            return {
                done: this.state.done,
                value: this.state.transition.call(this, data)
            }
        }
    },
    throw(err) {
        return {
            done: true,
            value: this.stateError.transition(err)
        }
    }
}

it.init()
it.next('The request begins !')
로그인 후 복사

在这里我使用了行为委托模式和状态模式实现了一个简单的有限状态机,而它却展现了生成器中核心部分的工作原理,下面我们来逐步分析它是如何运行的。

首先这里我们自己创建的 it 对象就相当于生成器函数执行后返回的迭代器对象,我们把上文生成器 + Promise 示例中的 main 函数代码分为了三个状态并将跟该状态有关的行为封装到了 stateRequest 、stateResult 、stateError 三个对象中。然后我们再调用 init(..) 将 it 对象上的行为委托到这三个对象上并初始化当前的状态对象。在准备工作完成后调用 next(..) 启动生成器,这个时候我们就进入了状态一,即执行 foo 函数发出网络请求。在 foo 函数内部当得到请求响应数据后就执行 it.next(data) 触发状态机内部的状态改变,此时执行状态二内部的代码即打印网络请求返回的结果。如果网络请求中出现错误就会执行 it.throw(err) ,这个时候的状态就会转换到状态三即错误处理状态。

在这里我们似乎忽略了一个很重要的地方,就是生成器是如何做到将其内部的代码分为多个状态的,当然我们知道这肯定是 yield 表达式的功劳,但是其内部又是怎么实现的呢?由于本人能力还不够,而且还有很多东西来不及去学习和了解,因此暂时无法解决这个问题,但我还是愿意把这个问题提出来,如果读者确实有兴趣能够通过查阅资料找到答案或者已经知道它的原理还是可以分享出来,毕竟经历这样刨根问底的过程还是满有趣的。

async / await

终于讲到最后一个异步语法了,作为压轴的身份出场,据说 async / await 是 JS 异步编程中的终极解决方案。话不多说,先直接上代码看看它的基本用法,然后我们再来探讨一下它的实现原理。

const foo = function (time) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(time + 200)
        }, time)
    })
}

const step1 = time => foo(time)
const step2 = time => foo(time) 
const step3 = time => foo(time)

const main = async function () {
    try {
        console.time('run')
        let time1 = 200
        let time2 = await step1(time1)
        let time3 = await step2(time2)
        await step3(time3)
        console.log(`All steps took ${time1 + time2 + time3} ms.`)
        console.timeEnd('run')
    } catch(err) {
        console.error(err)
    }
}

main()
// All steps took 1200 ms.
// run: 1222.87939453125ms
로그인 후 복사

我们可以看到 async 函数跟生成器函数极为相似,只是将之前的 * 变成了 async ,yield 变成了 await 。其实它就是一个能够自动执行的 generator 函数,我们不用再通过手动执行 it.next(..) 来控制生成器函数的暂停与启动。

await 帮我们做到了在同步阻塞代码的同时还能够监听 Promise 对象的决议,一旦 promise 决议,原本暂停执行的 async 函数就会恢复执行。这个时候如果决议是 resolve ,那么返回的结果就是 resolve 出来的值。如果决议是 reject ,我们就必须用 try..catch 来捕获这个错误,因为它相当于执行了 it.throw(err) 。

下面直接给出一种主流的 async / await 语法版本的实现代码:

const runner = function (gen) {
    return new Promise((resolve, reject) => {
        var it = gen()
        const step = function (execute) {
            try {
                var next = execute()
            } catch (err) {
                reject(err)
            }
            
            if (next.done) return resolve(next.value)
            
            Promise.resolve(next.value)
            .then(val => step(() => it.next(val)))
            .catch(err => step(() => it.throw(err)))
        }
        step(() => it.next())
    })
}

async function fn() {
    // ...
}

// 等同于

function fn() {
    const gen = function *() {
        // ...
    }
    runner(gen)
}
로그인 후 복사

从上面的代码我们可以看出 async 函数执行后返回的是一个 Promise 对象,然后使用递归的方法去自动执行生成器函数的暂停与启动。如果调用 it.next().value 传出来的是一个 promise ,则用 Promise.resolve() 方法将其异步展开,当这个 promise 决议时就可以重新启动执行生成器函数或者抛出一个错误被 try..catch 所捕获并最终在 async 函数返回的 Promise 对象的错误处理函数中处理。

关于 async / await 的执行顺序

下面给出一道关于 async / await 执行顺序的经典面试题,网上给出的解释给我感觉似乎很含糊。在这里我们结合上文所讲的 generator 函数运行机制和 async / await 实现原理来具体阐述下为什么执行顺序是这样的。

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

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

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
})

async1()

new Promise((resolve) => {
    console.log('promise1')
    resolve()
})
.then(() => {
    console.log('promise2')
})

console.log('script end')
로그인 후 복사

将这段代码放在浏览器中运行,最终的结果这样的:

script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout
로그인 후 복사

其实最主要的地方还是要分清在执行栈中同步执行的任务与事件队列中异步执行的任务。首先我们执行同步任务,打印 script start ,调用函数 async1 ,在我们遇到 await 表达式后就会暂停函数 async1 的执行。因为在这里它相当于 yield async2() ,根据上文的 async / await 原理实现代码可以看出,当自动调用 it.next() 时遇到第一个 yield 后会暂停执行,但此时函数 async2 已经执行。上文还提到过 async 函数在执行完后会返回一个 Promise 对象,故此时 it.next().value 的值就是一个 promise 。接下来要讲的就是重点啦 !!!

我们用 Promise.resolve() 去异步地展开一个 promise ,因此第一个放入事件队列中的微队列任务其实就是这个 promise 。之后我们再继续运行执行栈中剩下的同步任务,此时打印出 promise1 和 script end ,同时第二个异步任务被加入到事件队列中的微队列。同步的任务执行完了,现在来执行异步任务,首先将微队列中第一个放入的那个 promise 拿到执行栈中去执行,这个时候之前 Promise.resolve() 后面注册的回调任务才会作为第三个任务加入到事件队列中的微队列里去。然后我们执行微队列中的第二个任务,打印 promise2,再执行第三个任务即调用 step(() => it.next(val)) 恢复 async 函数的执行,打印 async1 end 。最后,因为微队列总是抢占式的在宏队列之前插入执行,故只有当微队列中没有了任务以后,宏队列中的任务才会开始执行,故最终打印出 setTimeout 。

常见异步模式

在软件开发中有着设计模式这一专业术语,通俗一点来讲设计模式其实就是在某种场合下针对某个问题的一种解决方案。

在 JS 异步编程的世界里,很多时候我们也会遇到因为是异步操作而出现的特定问题,而针对这些问题所提出的解决方案 ( 逻辑代码 ) 就是异步编程的核心,似乎在这里它跟设计模式的概念很相像,所以我把它叫做异步模式。下面我将介绍几种常见的异步模式在实际场景下的应用。

并发交互模式

当我们在同时执行多个异步任务时,这些任务返回响应结果的时间往往是不确定的,因而会产生以下两种常见的需求:

  1. 多个异步任务同时执行,等待所有任务都返回结果后才开始进行下一步的操作。

  2. 多个异步任务同时执行,只返回最先完成异步操作的那个任务的结果然后再进行下一步的操作。

场景一:

同时读取多个含有英文文章的 txt 文件内容,计算其中单词 of 的个数。

  1. 等待所有文件中的 of 个数计算完毕,再计算输出总的 of 数。

  2. 直接输出第一个计算完 of 的个数。

const fs = require('fs')
const path = require('path')

const addAll = (result) => console.log(result.reduce((prev, cur) => prev + cur))

let dir = path.join(__dirname, 'files')

fs.readdir(dir, (err, files) => {
    if (err) return console.error(err)
    let promises = files.map((file) => {
        return new Promise((resolve, reject) => {
            let fileDir = path.join(dir, file)
            fs.readFile(fileDir, { encoding: 'utf-8' }, (err, data) => {
                if (err) reject(err)
                let count = 0
                data.split(' ').map(word => word === 'of' ? count++ : null)
                resolve(count)
            })
        })
    })
    Promise.all(promises).then(result => addAll(result)).catch(err => console.error(err))
    Promise.race(promises).then(result => console.log(result)).catch(err => console.error(err))
})
로그인 후 복사

并发控制模式

有时候我们会遇到大量异步任务并发执行而且还要处理返回数据的情况,即使拥有事件循环 ( Event Loop ) 机制,在并发量过高的情况下程序仍然会崩溃,所以这个时候就应该考虑并发控制。

场景二:

利用 Node.js 实现图片爬虫,控制爬取时的并发量。一是防止 IP 被封掉 ,二是防止并发请求量过高使程序崩溃。

const fs = require('fs')
const path = require('path')
const request = require('request')
const cheerio = require('cheerio')

const target = `http://www.zimuxia.cn/${encodeURIComponent('我们的作品')}`

const isError = (err, res) => (err || res.statusCode !== 200) ? true : false

const getImgUrls = function (pages) {
    return new Promise((resolve) => {
        let limit = 8, number = 0, imgUrls = []
        const recursive = async function () {
            pages = pages - limit
            limit = pages >= 0 ? limit : (pages + limit)
            let arr = []
            for (let i = 1; i  {
                        request(target + `?set=${number++}`, (err, res, data) => {
                            if (isError(err, res)) return console.log('Request failed.')
                            let $ = cheerio.load(data)
                            $('.pg-page-wrapper img').each((i, el) => {
                                let imgUrl = $(el).attr('data-cfsrc')
                                imgUrls.push(imgUrl)
                                resolve()
                            })
                        })
                    })
                )
            }
            await Promise.all(arr)
            if (limit === 8) return recursive()
            resolve(imgUrls)
        }
        recursive()
    })
}

const downloadImages = function (imgUrls) {
    console.log('\n Start to download images. \n')
    let limit = 5
    const recursive = async function () {
        limit = imgUrls.length - limit >= 0 ? limit : imgUrls.length
        let arr = imgUrls.splice(0, limit)
        let promises = arr.map((url) => {
            return new Promise((resolve) => {
                let imgName = url.split('/').pop()
                let imgPath = path.join(__dirname, `images/${imgName}`)
                request(url)
                .pipe(fs.createWriteStream(imgPath))
                .on('close', () => {
                    console.log(`${imgName} has been saved.`)
                    resolve()
                })
            })
        })
        await Promise.all(promises)
        if (imgUrls.length) return recursive()
        console.log('\n All images have been downloaded.')
    }
    recursive()
}

request({
    url: target,
    method: 'GET'
}, (err, res, data) => {
    if (isError(err, res)) return console.log('Request failed.')
    let $ = cheerio.load(data)
    let pageNum = $('.pg-pagination li').length
    console.log('Start to get image urls...')
    getImgUrls(pageNum)
    .then((result) => {
        console.log(`Finish getting image urls and the number of them is ${result.length}.`)
        downloadImages(result)
    })
})
로그인 후 복사

发布 / 订阅模式

我们假定,存在一个"信号中心",当某个任务执行完成,就向信号中心"发布" ( publish ) 一个信号,其他任务可以向信号中心"订阅" ( subscribe ) 这个信号,从而知道什么时候自己可以开始执行,当然我们还可以取消订阅这个信号。

我们先来实现一个简单的发布订阅对象:

class Listener {
    constructor() {
        this.eventList = {}
    }
    on(event, fn) {
        if (!this.eventList[event]) this.eventList[event] = []
        if (fn.name) {
            let obj = {}
            obj[fn.name] = fn
            fn = obj
        }
        this.eventList[event].push(fn)
    }
    remove(event, fn) {
        if (!fn) return console.error('Choose a named function to remove!')
        this.eventList[event].map((item, index) => {
            if (typeof item === 'object' && item[fn.name]) {
                this.eventList[event].splice(index, 1)
            }
        })
    }
    emit(event, data) {
        this.eventList[event].map((fn) => {
            if (typeof fn === 'object') {
                Object.values(fn).map((f) => f.call(null, data))
            } else {
                fn.call(null, data)
            }
        })
    }
}

let listener = new Listener()

function foo(data) { console.log('Hello ' + data) }

listener.on('click', (data) => console.log(data))

listener.on('click', foo)

listener.emit('click', 'RetroAstro')

// Hello
// Hello RetroAstro

listener.remove('click', foo)

listener.emit('click', 'Barry Allen')

// Barry Allen
로그인 후 복사

场景三:

监听 watch 文件夹,当里面的文件有改动时自动压缩该文件并保存到 done 文件夹中。

// gzip.js
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')

const gzipFile = function (file) {
    let dir = path.join(__dirname, 'watch')
    fs.readdir(dir, (err, files) => {
        if (err) console.error(err)
        files.map((filename) => {
            let watchFile = path.join(dir, filename)
            fs.stat(watchFile, (err, stats) => {
                if (err) console.error(err)
                if (stats.isFile() && file === filename) {
                    let doneFile = path.join(__dirname, `done/${file}.gz`)
                    fs.createReadStream(watchFile)
                    .pipe(zlib.createGzip())
                    .pipe(fs.createWriteStream(doneFile))
                }
            })
        })
    })
}

module.exports = {
    gzipFile: gzipFile
}
로그인 후 복사

开始监听 watch 文件夹

// watch.js
const fs = require('fs')
const path = require('path')

const { gzipFile } = require('./gzip')
const { Listener } = require('./listener')

let listener = new Listener()

listener.on('gzip', (data) => gzipFile(data))

let dir = path.join(__dirname, 'watch')

let wait = true

fs.watch(dir, (event, filename) => {
    if (filename && event === 'change' && wait) {
        wait = false
        setTimeout(() => wait = true, 100)
        listener.emit('gzip', filename)
    }
})
로그인 후 복사

结语

对于 JavaScript 异步编程在这里我就讲这么多了,当然还有很多东西自己没有了解和学习到,因此在本篇文章中没有涉及。最后还是给出上面三个场景代码的 GitHub 地址 ,总之在前端学习的路上还得继续加油嘞 。

위 내용은 JavaScript 비동기 프로그래밍에 대한 자세한 소개(예제 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

AI Hentai Generator

AI Hentai Generator

AI Hentai를 무료로 생성하십시오.

인기 기사

R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 최고의 그래픽 설정
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25 : Myrise에서 모든 것을 잠금 해제하는 방법
3 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)

WebSocket 및 JavaScript: 실시간 모니터링 시스템 구현을 위한 핵심 기술 WebSocket 및 JavaScript: 실시간 모니터링 시스템 구현을 위한 핵심 기술 Dec 17, 2023 pm 05:30 PM

WebSocket과 JavaScript: 실시간 모니터링 시스템 구현을 위한 핵심 기술 서론: 인터넷 기술의 급속한 발전과 함께 실시간 모니터링 시스템이 다양한 분야에서 널리 활용되고 있다. 실시간 모니터링을 구현하는 핵심 기술 중 하나는 WebSocket과 JavaScript의 조합입니다. 이 기사에서는 실시간 모니터링 시스템에서 WebSocket 및 JavaScript의 적용을 소개하고 코드 예제를 제공하며 구현 원칙을 자세히 설명합니다. 1. 웹소켓 기술

PHP와 Vue: 프런트엔드 개발 도구의 완벽한 조합 PHP와 Vue: 프런트엔드 개발 도구의 완벽한 조합 Mar 16, 2024 pm 12:09 PM

PHP와 Vue: 프론트엔드 개발 도구의 완벽한 조합 오늘날 인터넷이 빠르게 발전하는 시대에 프론트엔드 개발은 점점 더 중요해지고 있습니다. 사용자가 웹 사이트 및 애플리케이션 경험에 대한 요구 사항이 점점 더 높아짐에 따라 프런트 엔드 개발자는 보다 효율적이고 유연한 도구를 사용하여 반응형 및 대화형 인터페이스를 만들어야 합니다. 프론트엔드 개발 분야의 두 가지 중요한 기술인 PHP와 Vue.js는 함께 사용하면 완벽한 도구라고 볼 수 있습니다. 이 기사에서는 독자가 이 두 가지를 더 잘 이해하고 적용할 수 있도록 PHP와 Vue의 조합과 자세한 코드 예제를 살펴보겠습니다.

프론트엔드 면접관이 자주 묻는 질문 프론트엔드 면접관이 자주 묻는 질문 Mar 19, 2024 pm 02:24 PM

프론트엔드 개발 인터뷰에서 일반적인 질문은 HTML/CSS 기초, JavaScript 기초, 프레임워크 및 라이브러리, 프로젝트 경험, 알고리즘 및 데이터 구조, 성능 최적화, 크로스 도메인 요청, 프론트엔드 엔지니어링, 디자인 패턴, 새로운 기술 및 트렌드. 면접관 질문은 후보자의 기술적 능력, 프로젝트 경험, 업계 동향에 대한 이해를 평가하기 위해 고안되었습니다. 따라서 지원자는 자신의 능력과 전문성을 입증할 수 있도록 해당 분야에 대한 충분한 준비를 갖추어야 합니다.

간단한 JavaScript 튜토리얼: HTTP 상태 코드를 얻는 방법 간단한 JavaScript 튜토리얼: HTTP 상태 코드를 얻는 방법 Jan 05, 2024 pm 06:08 PM

JavaScript 튜토리얼: HTTP 상태 코드를 얻는 방법, 특정 코드 예제가 필요합니다. 서문: 웹 개발에서는 서버와의 데이터 상호 작용이 종종 포함됩니다. 서버와 통신할 때 반환된 HTTP 상태 코드를 가져와서 작업의 성공 여부를 확인하고 다양한 상태 코드에 따라 해당 처리를 수행해야 하는 경우가 많습니다. 이 기사에서는 JavaScript를 사용하여 HTTP 상태 코드를 얻는 방법과 몇 가지 실용적인 코드 예제를 제공합니다. XMLHttpRequest 사용

Django는 프론트엔드인가요, 백엔드인가요? 확인 해봐! Django는 프론트엔드인가요, 백엔드인가요? 확인 해봐! Jan 19, 2024 am 08:37 AM

Django는 빠른 개발과 깔끔한 ​​방법을 강조하는 Python으로 작성된 웹 애플리케이션 프레임워크입니다. Django는 웹 프레임워크이지만 Django가 프런트엔드인지 백엔드인지에 대한 질문에 답하려면 프런트엔드와 백엔드의 개념에 대한 깊은 이해가 필요합니다. 프론트엔드는 사용자가 직접 상호작용하는 인터페이스를 의미하고, 백엔드는 HTTP 프로토콜을 통해 데이터와 상호작용하는 서버측 프로그램을 의미합니다. 프론트엔드와 백엔드가 분리되면 프론트엔드와 백엔드 프로그램을 독립적으로 개발하여 각각 비즈니스 로직과 인터랙티브 효과, 데이터 교환을 구현할 수 있습니다.

Go 언어 프런트엔드 기술 탐색: 프런트엔드 개발을 위한 새로운 비전 Go 언어 프런트엔드 기술 탐색: 프런트엔드 개발을 위한 새로운 비전 Mar 28, 2024 pm 01:06 PM

빠르고 효율적인 프로그래밍 언어인 Go 언어는 백엔드 개발 분야에서 널리 사용됩니다. 그러나 Go 언어를 프런트엔드 개발과 연관시키는 사람은 거의 없습니다. 실제로 프런트엔드 개발에 Go 언어를 사용하면 효율성이 향상될 뿐만 아니라 개발자에게 새로운 지평을 열어줄 수도 있습니다. 이 기사에서는 프런트엔드 개발에 Go 언어를 사용할 수 있는 가능성을 살펴보고 독자가 이 영역을 더 잘 이해할 수 있도록 구체적인 코드 예제를 제공합니다. 전통적인 프런트엔드 개발에서는 사용자 인터페이스를 구축하기 위해 JavaScript, HTML, CSS를 사용하는 경우가 많습니다.

Golang과 프런트엔드 기술의 결합: Golang이 프런트엔드 분야에서 어떤 역할을 하는지 살펴보세요. Golang과 프런트엔드 기술의 결합: Golang이 프런트엔드 분야에서 어떤 역할을 하는지 살펴보세요. Mar 19, 2024 pm 06:15 PM

Golang과 프런트엔드 기술의 결합: Golang이 프런트엔드 분야에서 어떤 역할을 하는지 살펴보려면 구체적인 코드 예제가 필요합니다. 인터넷과 모바일 애플리케이션의 급속한 발전으로 인해 프런트엔드 기술이 점점 더 중요해지고 있습니다. 이 분야에서는 강력한 백엔드 프로그래밍 언어인 Golang도 중요한 역할을 할 수 있습니다. 이 기사에서는 Golang이 프런트엔드 기술과 어떻게 결합되는지 살펴보고 특정 코드 예제를 통해 프런트엔드 분야에서의 잠재력을 보여줍니다. 프론트엔드 분야에서 Golang의 역할은 효율적이고 간결하며 배우기 쉬운 것입니다.

JavaScript에서 HTTP 상태 코드를 쉽게 얻는 방법 JavaScript에서 HTTP 상태 코드를 쉽게 얻는 방법 Jan 05, 2024 pm 01:37 PM

JavaScript에서 HTTP 상태 코드를 얻는 방법 소개: 프런트 엔드 개발에서 우리는 종종 백엔드 인터페이스와의 상호 작용을 처리해야 하며 HTTP 상태 코드는 매우 중요한 부분입니다. HTTP 상태 코드를 이해하고 얻는 것은 인터페이스에서 반환된 데이터를 더 잘 처리하는 데 도움이 됩니다. 이 기사에서는 JavaScript를 사용하여 HTTP 상태 코드를 얻는 방법을 소개하고 구체적인 코드 예제를 제공합니다. 1. HTTP 상태 코드란 무엇입니까? HTTP 상태 코드는 브라우저가 서버에 요청을 시작할 때 서비스가

See all articles