JavaScript 异步操作进化史

贤儒

特别说明

  • 这篇博客是我个人JavaScript 异步操作总结归类
  • 通过这篇文章我也希望读者可以从 宏观 的角度看待 JavaScript 异步操作是如何演化的。
  • 但是如果想要通过这篇博客全面掌握 promise 或者 async 函数等其他技术的全部知识,还是不太现实的。
  • 推荐大家精读阮一峰老师的 ECMAScript 6 入门 - Promise 对象 ,和尼古拉斯老师的《深入理解 ES6》的第十一章(Page 219)。

摘要

  用有一点哲学调调的话说:JavaScript 异步操作进化的终极目标就是让异步的代码 
看起来更像同步的代码。
  JS 这门语言单线程运作的天性,并没有让使用它的人觉得它是鸡肋的,反而让程序 
员们创造出了各式各样的工具来提高它的性能。
  从回调函数到 Promise 对象,再到被认为是 JS 异步操作最终解决方案的 async 函数。
这其中的每一次进化都是从无到有,从社区到标准。
  这篇博客将从源头出发,先探讨一下为什么 JS 需要异步操作。接着再解释一些概念性的
名词。最后捋一捋 JS 异步操作的发展过程。

关键词

Synchronous | Asynchronous | Event Loop | CallBack | Promise | Generator | Async/Await

JavaScript 为什么需要异步操作

单线程

  • JavaScript 这么语言设计的初衷是为了解决用户与浏览器的交互问题
  • 这其中有一项重头戏就是 DOM 操作。试想一下某个代码块是在修改 DOM,另外还有一个代码块需要删除 DOM。那么应该听谁的?
  • 为了避免出现比较复杂的线程同步问题,JS 执行环境中负责执行代码的线程只有一个。这就是我们常说的 JavaScript 单线程工作模式。

工作模式

  • 有的时候在执行一些很耗时的任务时,就需要等待当前任务执行完才能进入下一个任务。这样程序就会出现假死现象,也就是我们常说的阻塞
  • 为了避免这样的情况发生,JS 将工作模式主要分为两类:同步模式异步模式

一些概念

Synchronous

同步模式执行的代码会在执行栈中排队执行。也就是我们常说的压栈运行,当运行完了以后就会被弹出栈闭包函数可以把某一变量持久保存在执行栈中。

Asynchronous

异步模式的代码不会进入主线程,也就是我们的执行栈。而是进入任务队列或者说消息队列中。当 执行栈 中所有 同步任务 执行完毕,系统就会去读取 任务队列 ,那些 异步代码 就会结束等待,进入执行栈,开始执行。

Note同步还是异步是指运行环境提供的 API 是以 同步异步 模式的方式工作。

同步 API:console.log()
异步 API:setTimeOut()

Stack

执行栈。主线程运行的时候,产生堆和栈,栈中的代码调用各种外部 API,它们在任务队列中加入各种事件。

Message queue

消息队列。

Web API

浏览器所提供的各种 API 接口。

Event loop

只要中的代码执行完毕,主线程就会从消息队列中读取异步操作,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环 -- Event Loop

关于 stack、message queue、event loop 和 web api 的关系可参考下图:

image.png

image.png

进化史

CallBack

回调函数是最早的异步操作实现方式。它是由调用者定义,交给执行者执行的函数。几种常见的应用有:事件机制发布-订阅模式等。
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function () { ... } // 在接收到完整的响应数据时触发的回调函数
xhr.onerror = function () { ... } // 在请求发送错误时触发的回调函数
xhr.send()

缺陷:当我们需要发送多个请求,并且要在这些请求全部都返回成功的时候去对所有的请求结果做某些处理时,不免要想一些特殊的技巧。最简单的方式就是把每个请求都嵌套起来,当一个请求成功时再去执行下一个请求。这样实现的问题首先会很浪费时间,其次也会形成我们常说的回调地狱的情况,使代码既不美观很难维护

/* 第一层 */
$.ajax({
    url: URL1,
    /* callback1 */
    success: function () {
        /* 第二层 */
        $.ajax({
            url: URL2,
            /* callback2 */
            success: function () {
                ...
                /* 第 n 层 */
                $.ajax({ ... })
            }
        })
    }
})

Promise

Promise 是一个对象,是为 异步操作结果所准备的占位符。用来表示 异步任务 结束之后是成功还是失败。Promise 的状态一旦确定以后,就不可以被修改
  • Promise 的生命周期:在执行 异步操作 时,会承诺给出一个结果,在最后给出结果之前叫做 pending 状态,给出的结果有两种,成功的 fulfilled 状态和失败的 rejected 状态。在给出结果之后需要作出一些反应(交代任务),与之对应的就是 onFulfilledonRejected

image.png

  • then() 方法:可以使用 then() 方法在 Promise 的状态改变时执行一些特定操作。

    Promise 的本质是使用回调函数定义异步任务结束后所需执行的任务。
    function ajax (url) {
      return new Promise(function (resolve, reject) {
          var xhr = new XMLHttpRequest()
          xhr.responseType = 'json'
          xhr.onload = function () {
              if (this.status === 200) {
                  resolve(this.response)
              } else {
                  reject(new Error(this.statusText))
              }
          }
          xhr.send()
      })
    }
    
    ajax('/api/user.json').then(function (res) {
      console.log(res)
    }, function (error) {
      console.log(error)
    })
  • 串联 Promise

    • Promise 对象的 then() 方法会返回一个全新的 Promise 对象
    • 后面的 then() 方法就是为上一个 then 返回的 Promise 注册回调
    • 前面 then() 方法中回调函数的返回值会作为后面 then() 方法回调的参数
    • 如果回调中返回的是 Promise ,那后面的 then() 方法的回调会等待它的结束
    • 因此 Promise 是可以进行链式调用的。每一个 then() 方法实际上都是在为上一个 then() 方法返回的 Promise 对象添加状态明确过后的回调
    let p1 = new Promise(function(resolve, reject) {
      resolve(42);
    })
    
    p1.then(function (value) {
      console.log(value)
    }).then(function () {
      console.log("Finished")
    })
    
    // 42
    // Finished
  • 捕获错误

    • onRejected 回调在 Promise 失败或者出现异常时都会被执行
    • catch() 方法相当于 then() 方法所接受的第二个参数,但是区别在于 -- catch() 方法在 Promise 链中允许我们捕获前一个 Promise 的完成或拒绝处理函数中发生的错误。
    let p1 = new Promise(function(resolve, reject) {
      throw new Error("Explosion")
    })
    
    p1.catch(function (error) {
      console.log(error.message)
      throw new Error("Boom")
    }).catch(function (error) {
      console.log(error.message)
    })
  • Promise 静态方法

    /* Promise.resolve() */
    let promise = Promise.resolve(42)
    
    promise.then(function (value) {
      console.log(value)  // 42
    })
    
    /* Promise.reject() */
    let promise = Promise.reject(42)
    
    promise.catch(function (value) {
      console.log(value)  // 42
    })
    /* 下面这两种写法是等价的 */
    Promise.resolve('foo')
    .then(function (value) {
      console.log(value)
    })
    
    new Promise(function (resolve, reject) {
      resolve('foo')
    })
  • Promise 并行执行

    /* Promise.all()
     * 该方法接收单个可迭代对象(例如数组)作为参数,并返回一个 Promise。 
     * 所有的可迭代 Promise 元素都完成后,所返回的 Promise 才会被完成。
     */
    let p1 = new Promise(function (resolve, reject) {
      resolve(42)
    })
    
    let p2 = new Promise(function (resolve, reject) {
      reject(43)
    })
    
    let p = Promise.all([p1, p2])
    
    p.catch(function (value) {
      console.log(value)  // 43
    })
    /* Promise.race()
     * 该方法也接收一个 Promise 可迭代对象,并返回一个新的 Promise。
     * 一旦来源 Promise 中有一个被解决,所返回的 Promise 就会立即被解决。
     */
     let p1 = Promise.resolve(42)
     
     let p2 = new Promise(function (resolve, reject) {
       resolve(43)
     })
     
     let p = Promise.race([p1, p2])
     
     p.then(function (value) {
       console.log(value)  // 42
     })

宏任务和微任务

回调队列中的任务被称为宏任务。宏任务执行过程中,可以临时加上一些额外需求。可以选择作为一个 新的宏任务 进入任务队列排队,也可以作为 当前任务微任务直接在当前任务结束过后立即执行微任务 可以提高整体的响应能力,Promise 的回调会被作为微任务执行。可以使用 setTimeOut() 添加 宏任务
console.log("global start")

setTimeOut(() => {
    console.log("setTimeOut")
}, 0)

Promise.resolve()
.then(() => {
    console.log("Promise")
})
.then(() => {
    console.log("Promise2")
})

console.log("global end")

// global start
// global end
// Promise
// Promise2
// setTimeOut

Generator

Generator 生成器执行过程:

  • 定义时在函数名前面有一个 *
  • 在调用 Generator 函数时并不会立即去执行这个函数,而是会得到一个生成器对象
  • 当我们调用这个 生成器对象next() 方法时才会去执行
  • 一直会执行到 yield 关键字所在的位置,并且把 yield 后面的值 返回出去,然后这个函数就会暂停执行。yield 返回值 会被接收到,形式是 { value: "foo", done: false }
  • 当我们再次调用 next() 方法,并且传入参数,那么函数就会继续往下执行,并且我们传入的参数会作为 yield 的返回值
  • 如果我们在外面调用的是生成器对象的 throw() 方法,那么函数将会得到这个异常。可以在函数内部使用 try...catch...的方式捕获异常

    function * main () {
      const users = yield ajax('/api/users.json')
      console.log(users)
      
      const posts = yield ajax('/api/posts.json')
      console.log(posts)
    }
    
    const g = main()
    
    const result = g.next()
    
    result.value.then(data => {
      const result2 = g.next(data)
      if (result2.done) return
      
      result2.value.then(data => {
          ...
      })
    })

Async/Await

  • 执行 async 函数,返回的都是 Promise 对象

    async function test1 () {
      return 1
    }
    
    async function test2 () {
      return Promise.resolve(2)
    }
    
    const result1 = test1()
    const result2 = test2()
    
    console.log('result1', result1)
    console.log('result1', result1)
  • Promise.then() 成功的情况,对应 await

    async function test3 () {
      const p3 = Promise.resolve(3)
      p3.then(data => {
          console.log('data', data)
      })
      
      const data = await p3
      console.log('data', data)
    }
    async function test4 () {
      const data4 = await 4
      console.log('data4', data4)
    }
    
    async function test5 () {
      const test5 = await test1()
      console.log('test5', test5)
    }
  • Promise.catch() 异常的情况,对应 try...catch

    有时我们希望即使前一个(异步)操作失败,也不要中断后面的(异步)操作。这时就可以使用 try...catch... 来捕获异常
    async function test6 () {
      const p6 = Promise.reject(6)
      try {
         const data6 = await p6
         console.log('data6', data6)
      } catch (e) {
          console.log('e', e)
      }
    }

鸣谢

  • 感谢每一位为 JavaScript 做出贡献的 programmer。也感谢每位正在做出努力的“潜伏者们”,期待着你们的爆发。
  • 阮一峰 老师
  • 尼古拉斯 老师
  • B 站 “IT课程大拿”
阅读 250
9 声望
0 粉丝
0 条评论
你知道吗?

9 声望
0 粉丝
文章目录
宣传栏