美文网首页
异步编程的实现方式?

异步编程的实现方式?

作者: dingFY | 来源:发表于2020-08-21 16:58 被阅读0次

    一、JavaScript为什么要异步?

    Javascript语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

    二、同步模式

    "同步模式" 指后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的

    三、异步模式

    "异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。 "异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

    四、异步编程的实现方式

    1、回调函数

    优点:简单、容易理解
    缺点:不利于维护,代码耦合高,多个异步操作下容易形成回调地狱。

    2、事件监听

    优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
    缺点:事件驱动型,流程不够清晰

    3、发布/订阅(观察者模式)

    类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者

    4、Promise

    优点:可以利用then方法,进行链式写法;可以书写错误时的回调函数;
    缺点:编写和理解,相对比较难

    5、Generation

    优点:函数体内外的数据交换、错误处理机制
    缺点:流程管理不方便

    6、async/await

    优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
    缺点:错误处理机制

    五、回调函数

    传说中的 "callback hell" 就是来自回调函数。回调函数简单理解就是一个函数被作为参数传递给另一个函数,而回调函数也是最基础最常用的处理js异步操作的办法。我们来看一个简单的例子:

        function fn1() {
          console.log('Function 1')
        }
    
        function fn2() {
          setTimeout(() => {
            console.log('Function 2')
          }, 500)
        }
    
        function fn3() {
          console.log('Function 3')
        }
        fn1();
        fn2();
        fn3();
    
        // 结果:
        // Function 1
        // Function 3
        // Function 2
    

    其在fn2可以视作一个延迟了500毫秒执行的异步函数。现在我希望可以依次执行fn1,fn2,fn3。为了保证fn3在最后执行,我们可以把它作为fn2的回调函数:

        function fn1() {
          console.log('Function 1')
        }
    
        function fn2(callback) {
          setTimeout(() => {
            console.log('Function 2')
            callback()
          }, 500)
        }
    
        function fn3() {
          console.log('Function 3')
        }
        fn1();
        fn2(fn3);
    
        // 结果:
        // Function 1
        // Function 2
        // Function 3
    

    回调函数是异步编程最基本的方法,其优点是简单、容易理解和部署。回调函数最大的缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),如果有多个类似的函数,很有可能会出现fn1(fn2(fn3(fn4(...))))这样的情况。

    六、事件监听

    采用事件驱动模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生,事件监听最常用的常见在于DOM元素事件绑定触发,如果我们想在DOM元素与用户进行鼠标或其他交互之后执行某些逻辑,就可以使用事件监听了

        $('body').on('done', fn2 )
        function fn1() {
          setTimeout(() => {
            console.log('Function 1')
            $('body').trigger('done')
          }, 500);
        }
        function fn2() {
          console.log('Function 2')
        }
        fn1()
        // 结果:
        // Function 1
        // Function 2
    

    上述代码中,我们使用jq的on监听了一个自定义事件done,传入了fn2回调函数,表示事件触发后立即执行函数fn2。在函数fn1中使用setTimeout模拟了耗时任务,setTimeout回调中使用trigger触发了done事件。我们可以使用on来绑定多个事件,每个事件可以指定多个回调函数

    七、发布/订阅(观察者模式)

    发布/订阅模式是利用一个消息中心,发布者发布一个消息给消息中心,订阅者从消息中心订阅该消息。订阅/发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某发布者对象。这个发布者对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。类似于 vue 的父子组件之间的传值。

        //创建一个主题发布类
        let Publisher = function () {
          this.subscribers = []
        }
        Publisher.prototype.publish = function (data) {
          this.subscribers.forEach(fn=>{
            fn(data)
          })
        }
    
        //订阅 —— 在Function上挂载这个些方法,所有的函数都可以调用这些方法表示所有函数都可以订阅/取消订阅相关的主题发布
        Function.prototype.subscribe = function (publisher) {
          let that = this;
          let isExist = publisher.subscribers.some(function (el) {
            if (el === that) {
              return true
            }
          })
          if (!isExist) {
            publisher.subscribers.push(that)
          }
          //return this是为了支持链式调用
          return this
        }
    
        //取消订阅
        Function.prototype.unsubscribe = function (publisher) {
          let that = this;
    
          //就是将函数从发布者的订阅者列表中进行删除
          publisher.subscribers = publisher.subscribers.filter(function (el) {
            if (el !== that) {
              return true
            }
          })
          return this
        }
    
        let publisher = new Publisher();
        let subscriberObj = function (data) {
          console.log(data)
        }
        subscriberObj.subscribe(publisher)
    

    八、Promise

    Promise 是异步编程的一种解决方案,比传统的解决方案【回调函数】和【事件】更合理、更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。Promise说得通俗一点就是一种写代码的方式,并且是用来写JavaScript编程中的异步代码的。(详情请查看我的另一篇原文:Promise: 给我一个承诺,我还你一个承诺

    【8.1】promise三种状态

    • pending:进行中
    • fulfilled :已成功
    • rejected 已失败

    只有异步操作的结果才能确定当前处于哪种状态,任何其他操作都不能改变这个状态,这也是Promise(承诺)的由来。

    Promise对象的状态改变,只有两种可能:

    • 从pending变为fulfilled
    • 从pending变为rejected

    这两种情况只要发生,状态就凝固了,不会再变了,这时就称为resolved(已定型)

    【8.2】promise缺点

    1. 无法取消Promise,一旦新建它就会立即执行,无法中途取消
    2. 如果不设置回调函数(没有捕获错误),Promise内部抛出的错误,不会反应到外部
    3. 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

    【8.3】promise API

    我们先把Promise打印出来,会发现Promise是一个构造函数,自己身上有all、reject、resolve、race等方法,原型上有then、catch、finally等方法。

    ※ Promise.prototype.constructor() ※

    它的基本用法如下:

        let promise = new Promise((resolve, reject) => {
          // 在这里执行异步操作
          if (/*异步操作成功*/) {
            resolve(success)
          } else {
            reject(error)
          }
        })
    

    Promise接收一个函数作为参数,函数里有resolve和reject两个参数:

    1. resolve方法的作用是将Promise的pending状态变为fulfilled,在异步操作成功之后调用,可以将异步返回的结果作为参数传递出去。
    2. reject方法的作用是将Promise的pending状态变为rejected,在异步操作失败之后调用,可以将异步返回的结果作为参数传递出去。

    他们之间只能有一个被执行,不会同时被执行,因为Promise只能保持一种状态。

    ※ Promise.prototype.then() ※

    Promise实例确定后,可以用then方法分别指定fulfilled状态和rejected状态的回调函数。它的基本用法如下:

        promise.then((success) => {
          // 异步操作成功在这里执行
          // 对应于上面的resolve(success)方法
        }, (error) => {
          // 异步操作失败在这里执行
          // 对应于上面的reject(error)方法
        })
    
        // 还可以写成这样 (推荐使用这种写法)
        promise.then((success) => {
          // 异步操作成功在这里执行
          // 对应于上面的resolve(success)方法
        }).catch((error) => {
          // 异步操作失败在这里执行
          // 对应于上面的reject(error)方法
        })
    

    then(onfulfilled,onrejected)方法中有两个参数,两个参数都是函数,第一个参数执行的是resolve()方法(即异步成功后的回调方法),第二参数执行的是reject()方法(即异步失败后的回调方法)(第二个参数可选)。它返回的是一个新的Promise对象。

    ※ Promise.prototype.catch() ※

    catch方法是.then(null,onrejected)的别名,用于指定发生错误时的回调函数。作用和then中的onrejected一样,不过它还可以捕获onfulfilled抛出的错,这是onrejected所无法做到的:

        function createPromise(p, arg) {
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              if (arg === 0) {
                reject(p + ' fail')
              } else {
                resolve(p + ' ok')
              }
            }, 0);
          })
        }
    
        createPromise('p1', 1).then((success) => {
          console.log(success) // p1 ok
          return createPromise('p2', 0)
        }).catch((error) => {
          console.log(error) // p2 fail
        })
    
        createPromise('p1', 1).then((success) => {
          console.log(success) // p1 ok
          return createPromise('p2', 0)
        }, (error) => {
          console.log(error) // Uncaught (in pomise) p2 fail
        })
    
    

    Promise错误具有"冒泡"的性质,如果不被捕获会一直往外抛,直到被捕获为止;而无法捕获在他们后面的Promise抛出的错。

    ※ Promise.prototype.finally() ※

    finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。该方法是 ES2018 引入的标准:

        createPromise('p1', 0).then((success) => {
          console.log(success)
        }).catch((error) => {
          console.log(error) // p1 fail
        }).finally(() => {
          console.log('finally') // finally
        })
    
        createPromise('p1', 1).then((success) => {
          console.log(success) // p1 ok
        }).catch((error) => {
          console.log(error)
        }).finally(() => {
          console.log('finally') // finally
        })
    

    finally方法不接受任何参数,故可知它跟Promise的状态无关,不依赖于Promise的执行结果。

    ※ Promise.all() ※

    Promise.all方法接受一个数组作为参数,但每个参数必须是一个Promise实例。Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作都执行完毕后才执行回调,只要其中一个异步操作返回的状态为rejected那么Promise.all()返回的Promise即为rejected状态,此时第一个被reject的实例的返回值,会传递给Promise.all的回调函数:

        function createPromise(p, arg) {
          return new Promise((resolve, reject) => {
            setTimeout(() => {
              if (arg === 0) {
                reject(p + ' fail')
              } else {
                resolve(p + ' ok')
              }
            }, 0);
          })
        }
    
        // test: 两个Promise都成功
        Promise.all([createPromise('p1', 1), createPromise('p2', 1)])
          .then((success) => {
            console.log(success) // ['p1 ok', 'p2 ok']
          }).catch((error) => {
            console.log(error)
          })
    
        // test: 其中一个Promise失败
        Promise.all([createPromise('p1', 0), createPromise('p2', 1)])
          .then((success) => {
            console.log(success)
          }).catch((error) => {
            console.log(error) // p1 fail 
          })
    
        // test: 两个Promise都失败
        Promise.all([createPromise('p1', 0), createPromise('p2', 0)])
          .then((success) => {
            console.log(success)
          }).catch((error) => {
            console.log(error) // p1 fail 只打印第一个失败的异步操作信息
          })
    
    

    ※ Promise.race() ※

    Promise的race方法和all方法类似,都提供了并行执行异步操作的能力。顾名思义,race就是赛跑的意思,意思就是说Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态,以下就是race的执行过程:

        let p1 = new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve('success')
          }, 1000)
        })
    
        let p2 = new Promise((resolve, reject) => {
          setTimeout(() => {
            reject('failed')
          }, 500)
        })
    
        Promise.race([p1, p2]).then((success) => {
          console.log(success)
        }).catch((error) => {
          console.log(error)  // failed
        })
    

    ※ Promise.resolve() ※

    有时需要将现有对象转为 Promise 对象Promise.resolve()方法就起到这个作用。

    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
    

    ※ Promise.reject() ※

    Promise.reject()方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

    const p = Promise.reject('出错了');
    // 等同于
    const p = new Promise((resolve, reject) => reject('出错了'))
    

    【8.4】例子

    Promise 翻译成中文为“承诺, 诺言”, 例如: 你承诺这个月挣钱了给你老婆买一个包, 那么你先去挣钱, 等挣钱了就立刻给老婆买包, 实现你的诺言, 没挣到钱就立马道歉。换成代码就是:

      // 买包就是一个Promise,Promise的意思就是承诺
      // 这时候老公给老婆一个承诺
      // 在未来的一个月,不管挣没挣到钱,都会给老婆一个答复
    
      let buyBag = new Promise((resolve, reject) => {
        // Promise 接受两个参数
        // resolve: 异步事件成功时调用(挣到钱)
        // reject: 异步事件失败时调用(没挣到钱)
    
        // 模拟挣钱概率事件
        let result = function makeMoney() {
          return Math.random() > 0.5 ? '挣到钱' : '没挣到钱'
        }
    
        // 下面老公给出承诺,不管挣没挣到钱,都会给老婆一个答复
        if (result == '挣到钱')
          resolve('我买包了')
        else
          reject('不好意思,我这个月没挣到钱')
      })
    
      buyBag().then(res => {
        // 返回 "我买包了"
        console.log(res)
      }).catch(err => {
        // 返回 "不好意思,我这个月没挣到钱"
        console.log(err)
      })
    

    解释一下

    第一段调用了Promise构造函数,第二段是调用了promise实例的.then方法

    1. 构造实例

    • 构造函数接受一个函数作为参数
    • 调用构造函数得到实例buyBag的同时,作为参数的函数会立即执行
    • 参数函数接受两个回调函数参数resolve和reject
    • 在参数函数被执行的过程中,如果在其内部调用resolve会将buyBag的状态变成fulfilled,或者调用reject会将buyBag的状态变成rejected

    2. 调用.then

    • 调用.then可以为实例buyBag注册两种状态回调函数
    • 当实例buyBag的状态为fulfilled,会触发第一个回调函数执行
    • 当实例buyBag的状态为rejected,则触发第二个回调函数执行

    九、Generation

    顾名思义,Generation 是一个生成器,它也是一个状态机,内部拥有值及相关的状态,生成器返回一个迭代器Iterator对象,我们可以通过这个迭代器,手动地遍历相关的值、状态,保证正确的执行顺序。Generation最大特点就是可以交出函数的执行权(即暂停执行)

    【9.1】声明

    Generator的声明方式类似一般的函数声明,只是多了个*号,并且一般可以在函数内看到yield关键字

        function* showWords() {
          yield 'one';
          yield 'two';
          return 'three';
        }
    
        let show = showWords();
        console.log(show.next()) // {done: false, value: "one"}
        console.log(show.next()) // {done: false, value: "two"}
        console.log(show.next()) // {done: true, value: "three"}
        console.log(show.next()) // {done: true, value: undefined}
    

    如上代码,定义了一个showWords的生成器函数,调用之后返回了一个迭代器对象(即show)。调用next方法后,函数内执行第一条yield语句,输出当前的状态done(迭代器是否遍历完成)以及相应值(一般为yield关键字后面的运算结果)。每调用一次next,则执行一次yield语句,并在该处暂停,return完成之后,就退出了生成器函数,后续如果还有yield操作就不再执行了

    【9.2】yield和yield*

    yield就是说明next函数调用时返回的值,yield还有一个有趣的地方,就是在每个yield调用之后,后面的代码都会停止执行。其实从某种程度来说,yield和return是非常相似的。有时候,我们会看到yield之后跟了一个*号,它是什么,有什么用呢?我们修改一下上面的代码:

        function* showWords() {
          yield 'one';
          yield showNumbers();
          return 'four';
        }
    
        function* showNumbers() {
          yield 2;
          yield 3;
        }
    
        let show = showWords();
        console.log(show.next()) // {done: false, value: "one"}
        console.log(show.next()) // {done: false, value: showNumbers}
        console.log(show.next()) // {done: true, value: "three"}
        console.log(show.next()) // {done: true, value: undefined}
    

    增添了一个生成器函数showNumbers(),我们在showWords中调用一次showNumbers()之后发现并没有执行函数里面的yield,因为yield只能原封不动地返回右边运算后的值,但现在的showNumbers()不是一般的函数调用,返回的是迭代器对象,所以换个yield* 让它自动遍历进该对象。修改代码如下:

        function* showWords() {
          yield 'one';
          yield* showNumbers();
          return 'four';
        }
    
        function* showNumbers() {
          yield 2;
          yield 3;
        }
    
        let show = showWords();
        console.log(show.next()) // {done: false, value: "one"}
        console.log(show.next()) // {done: false, value: 2}
        console.log(show.next()) // {done: false, value: 3}
        console.log(show.next()) // {done: true, value: "three"}
    

    yield和yield* 只能在generator函数内部使用,一般的函数内使用会报错

        // 普通函数中使用yield
        function showWords() {
          yield 'one';
        }
        showWords() // Uncaught SyntaxError: Unexpected string
    
        // 普通函数中使用yield*
        function showNums() {
          yield* 1;
        }
        let show = showNums();
        console.log(show.next()) // Uncaught ReferenceError: yield is not defined
    
    

    【9.3】 next()传参

    参数值有注入的功能,可改变上一个yield的返回值

    function* showNumbers() {
        let one = yield 1;
        let two = yield 2 * one;
        yield 3 * two;
    }
    
    let show = showNumbers(); 
    console.log(show.next().value) // 1
    console.log(show.next().value) // NaN
    console.log(show.next(2).value) // 6
    

    第一次调用next之后返回值one为1,但在第二次调用next的时候one其实是undefined的,因为generator不会自动保存相应变量值,我们需要手动的指定,这时two值为NaN,在第三次调用next的时候执行到yield 3 * two,通过传参将上次yield返回值two设为2,得到结果为6。 若是传入3返回的就是 9

    【9.4】 for...of循环代替.next()

    除了使用.next()方法遍历迭代器对象外,通过ES6提供的新循环方式for...of也可遍历,但与next不同的是,它会忽略return返回的值

    function* showNumbers() {
        yield 1;
        yield 2;
        return 3;
    }
    
    let show = showNumbers();
    
    for (let n of show) {
        console.log(n) // 1 2
    }
    

    除了for...of循环,具有调用迭代器接口的方法方式也可遍历生成器函数,如扩展运算符...的使用

    function* showNumbers() {
        yield 1;
        yield 2;
        return 3;
    }
    
    let show = showNumbers();
    console.log([...show]) // [1, 2]
    

    【9.5】 注意

    生成器函数不能当构造器使用

    function* Person() {}
    let person = new Person; // throws "TypeError: Person is not a constructor"
    
    

    yield是不能穿透函数的,不能使用forEach代替for循环遍历

    function* showNumbers(array){
      // 正确写法
      for( let i=0; i<array.length; i++ ){
        yield array[i]
      }
    
      // 错误写法
      // array.forEach(item=>{
      //   yield item
      // })
    }
    
    let show = showNumbers([2,5,7]);
    console.log(show.next()) // {value: 2, done: false}
    console.log(show.next()) // {value: 5, done: false}
    console.log(show.next()) // {value: 7, done: false}
    console.log(show.next()) // {value: undefined, done: true}
    

    可以使用变量来定义函数,也就是函数表达式。但是不能用箭头函数进行创建

        let showNumbers = function* () {
          yield 1;
          yield 2;
          return 3;
        }
    
        let show = showNumbers();
        console.log([...show]) // [1, 2]
    

    十、async/await

    【10.1】async关键字

    async 是 ES7 才有的与异步操作有关的关键字,和 Promise,Generator 有很大关联的。

    ※ 特点 ※

    1、建立在 promise 之上。所以,不能把它和回调函数搭配使用。但它会声明一个异步函数,并隐式地返回一个Promise。因此可以直接return变量,无需使用 Promise.resolve 进行转换。

    2、和 promise 一样,是非阻塞的。但不用写 then 及其回调函数,这减少代码行数,也避免了代码嵌套。而且,所有异步调用,可以写在同一个代码块中,无需定义多余的中间变量。

    3、它的最大价值在于,可以使异步代码,在形式上,更接近于同步代码。

    4、它总是与 await 一起使用的。并且await 只能在 async 函数体内。

    5、await 是个运算符,用于组成表达式,它会阻塞后面的代码。如果等到的是 Promise 对象,则得到其 resolve 值。否则,会得到一个表达式的运算结果。

    ※ 用法 ※

    先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 下面我们就来写一个async 函数

        async function test() {
          return 'Hello World';
        }
        console.log(test())
    

    语法很简单,就是在函数前面加上async 关键字,来表示它是异步的,那怎么调用呢?async 函数也是函数,平时我们怎么使用函数就怎么使用它,直接加括号调用就可以了

    查看控制台打印结果

    原来async 函数返回的是一个promise 对象,如果要获取到promise 返回值,我们应该用then 方法, 继续修改代码

        async function test() {
          return 'Hello World';
        }
        test().then(res=>{
          console.log(res) // Hello World
        })
        console.log('我在后面,但是我先执行')
    

    上面代码中通过then()方法获取到promise的返回值,假设promise内部抛出异常,我们同样可以通过catch()方法来捕获异常。

    我们获取到了"Hello World', 同时test()异步函数的执行也没有阻塞后面代码的执行,"我在后面,但是我先执行",这条语句会先执行

    看到这,小伙伴们可能要纳闷了,就是封装一个Promise的对象返回而已,要这有个鬼用啊。别急,接下来有请async黄金搭档 await关键字闪亮登场。

    【10.2】await关键字

    await是等待的意思,那么它等待什么呢,它后面跟着什么呢?

    正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。await表达式会使async函数暂停执行,等待promise的结果出来,然后恢复async的执行并返回解析值(resolved)

    注意,await 关键字仅仅在async 函数中才有效,如果在async函数外使用await,则会抛出一个语法错误(SyntaxError)

        function testAwait() {
          return new Promise((resolve) => {
            setTimeout(function () {
              console.log("Test Await");
              resolve();
            }, 1000);
          });
        }
    
        async function test() {
          await testAwait();
          console.log("Hello World");
        }
        test();
        // Test Await
        // Hello World
    

    我们来分析下上面这段代码

    现在我们看看代码的执行过程,调用test函数,它里面遇到了await, await 表示等一下,代码就暂停到这里,不再向下执行了,它等什么呢?等后面的testAwait函数中的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。执行console.log语句。

    注意:await 命令后面的 Promise 对象,运行结果不一定都是resolve,也可能是 rejected。当promise返回结果为rejected状态时,会终止后面的代码执行。所以最好把 await 命令放在 try...catch 代码块中。异常被try...catch捕获后,继续执行下面的代码,不会导致中断

          function testAwait() {
            return new Promise((resolve) => {
              setTimeout(function () {
                console.log("Test Await");
                resolve();
              }, 1000);
            });
          }
    
          async function test() {
            try {
              await testAwait();
            } catch (err) {
              console.log(err)
            }
            console.log("Hello World");
          }
          test();
    

    文章每周持续更新,可以微信搜索「 前端大集锦 」第一时间阅读,回复【视频】【书籍】领取200G视频资料和30本PDF书籍资料

    相关文章

      网友评论

          本文标题:异步编程的实现方式?

          本文链接:https://www.haomeiwen.com/subject/rsxpjktx.html