vlambda博客
学习文章列表

异步编程的实现方式?


异步编程的实现方式?


0 1
JavaScript为什么要异步?

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

0 2
同步模式

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

0 3
异步模式

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

0 4
异步编程的实现方式

1、回调函数 

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

2、事件监听 

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

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

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

4、Promise 

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

5、Generation 

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

6、async/await

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

0 5
回调函数

传说中的 "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(...))))这样的情况。

0 6
事件监听

采用事件驱动模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生,事件监听最常用的常见在于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来绑定多个事件,每个事件可以指定多个回调函数

0 7
发布/订阅(观察者模式)

发布/订阅模式是利用一个消息中心,发布者发布一个消息给消息中心,订阅者从消息中心订阅该消息。订阅/发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某发布者对象。这个发布者对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。类似于 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)

0 8
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,则触发第二个回调函数执行



0 9
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) // 1console.log(show.next().value) // NaNconsole.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]

10
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();



异步编程的实现方式?


- End -


扫描二维码

关注一下哦

前端大集锦


图片来自网络,侵权删


好看的人都点了 "在看"