异步编程的实现方式?

一、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书籍资料

猜你喜欢

转载自blog.csdn.net/qq_38128179/article/details/108076615