美文网首页
理解 JavaScript(ECMAScript 6)—— 异步

理解 JavaScript(ECMAScript 6)—— 异步

作者: rollingstarky | 来源:发表于2020-09-19 19:08 被阅读0次

    JavaScript 作为主要面向 Web 编程而创建的语言,其诞生初期即具有了应对异步的用户交互(如点击鼠标、按下键盘等)的能力。后续的 Node.js 引入了 callbacks 作为除事件模型以外的另一种实现异步编程的方式,而之后的 Promise 又使得 JavaScript 处理异步需求的能力更为强大。

    一、异步编程基础

    Event Model

    当用户点击鼠标或按下键盘上的某个按键时,一个对应的特殊事件(比如 onclick)触发,该事件关联的一系列响应动作即被添加到工作队列中最终被执行。这是 JavaScript 中最基本的异步编程方式。

    <button id="my-btn">Click Me</button>
     <script>
       let button = document.getElementById("my-btn")
       button.onclick = function(event) {
         console.log("Clicked")
       }
     </script>
    
    callback

    callback 模式与基于事件模型的异步编程类似,异步代码都是在后续的某个特定时间点执行。不同的是,其异步执行的操作(函数)需要作为参数传递。

    let fs = require("fs")
    
    fs.readFile("example.txt", { encoding: "utf8" }, function(err, contents){
      if (err) {
        throw err
      }
      console.log(contents)
    })
    
    console.log("Hi!")
    
    // Hi!
    // This is an example text file
    

    上面的例子使用了经典的 Node.js error-first 回调函数模式。readFile() 函数从硬盘读取某个文件,在文件读取完成之后执行 callback 函数。如果读取文件时发生错误,则传递给回调函数的 err 参数为错误对象;未发生错误则 content 参数中包含了读取的文件内容。

    在 callback 模式中,readFile() 会立即开始执行,并且在文件读取的进度开启之后暂停。紧接着 readFile() 后面的 console.log("Hi!") 会立即执行并输出 Hi! 到屏幕上(此时 readFile() 处于暂停状态,回调函数中的 console.log(contents) 也并未执行)。
    文件读取结束后,一个新的任务(即 callback 函数以及传递给它的参数)被添加到任务队列中等待最终被执行。
    从输出中可以看到,Hi! 要先于 contents 被打印。

    callback 模式的问题在于,当有过多的 callback 函数嵌套时,会出现称为 callback hell 的情况:

    method1(function(err, result) {
      if (err) {
        throw err;
      }
      method2(function(err, result) {
        if (err) {
          throw err;
        }
        method3(function(err, result) {
          if (err) {
            throw err;
          }
          method4(function(err, result) {
            if (err) {
              throw err;
            }
            method5(result);
          });
        });
      });
    });
    

    二、Promise

    除了前面提到的 callback hell 会使代码变得过于繁杂以至于难以理解和调试之外,callback 模式对于处理某些较复杂的逻辑也有一定的局限性。
    比如希望两个异步操作并行执行,在双方都完成之后提醒用户;或者两个异步任务同时开始但是只获取第一个任务执行完后的结果。在这些情景下,就需要同时追踪多个 callback 函数的状态。
    promise 则针对以上情况做了相应的提升。

    promise 是一种对应异步操作执行结果的“占位符”。

    // readFile “保证”会在未来的某个时间点完成
    let promise = readFile("example.txt")
    

    readFile() 并不会立即开始读取文件。相反,它会直接返回一个 promise 对象表示异步的读取操作,方便在后续的代码中通过这个 promise 对象访问读取任务的结果。该 promise 代表的结果是否可用取决于其生命周期所处的阶段。

    Promise 生命周期

    promise 的生命周期起始于 pending (unsettled) 状态,表明对应的异步操作还未完成。比如前面的 let promise = readFile("example.txt"),在 readFile() 函数返回后 promise 就立即进入了 pending 状态。
    而异步操作最终完成时,promise 则进入 settled 状态,具体包含两种情况:

    • Fulfilled:promise 对应的异步操作成功执行完毕
    • Rejected:promise 对应的异步操作未执行完毕(出现错误或其他情况)
    Promise lifecycle

    内部属性 [[PromiseState]] 用来标记其生命周期状态(如 pendingfulfilledrejected),该属性不对 promise 对象外部暴露,因此不可以人为修改 promise 对象的生命周期。但是可以在 promise 的状态改变时通过 then() 方法自动触发一系列动作。

    所有的 promise 对象都具有 then() 方法,该方法可以接收两个函数作为参数。第一个参数为当 promise 状态为 fulfilled 时调用的函数,所有与异步操作相关的数据都会被传递给该函数;第二个参数为当 promise 状态为 rejected 时调用的函数。这两个参数都是可选的。

    let promise = readFile("example.txt");
    
    promise.then(function(contents) {
      // fulfillment
      console.log(contents)
    }, function(err) {
      // rejection
      console.error(err.message)
    });
    
    promise.then(function(contents) {
      // fulfillment
      console.log(contents);
    });
    
    promise.then(null, function(err) {
      // rejection
      console.error(err.message);
    });
    
    promise.catch(function(err) {
      // rejection
    console.error(err.message);
    });
    
    创建 Promise

    promise 可以使用 Promise 构造器创建,该构造器接收一个称为 executor 的函数作为参数,包含了初始化 promise 的代码。
    executor 接收 resolve()reject() 两个函数作为参数,resolve() 将在 executor 执行成功后调用,传递 promise 已做好准备的信号;executor 执行失败了则调用 reject()

    一个 Promise 的完整示例:

    let fs = require("fs")
    
    function readFile(filename) {
      return new Promise(function(resolve, reject) {
        fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {
          // check for errors
          if (err) {
            reject(err)
            return
          }
          // the read succeeded
          resolve(contents)
        })
      })
    }
    
    let promise = readFile("example.txt")
    
    // listen for both fulfillment and rejection
    promise.then(function(contents) {
      // fulfillment
      console.log(contents)
    }, function(err) {
      // rejection
      console.error(err.message)
    })
    
    console.log("Hi!")
    // Hi!
    // This is an example text file
    
    Promise 的执行流程

    参考如下代码:

    console.log("At code start")
    
    var delayedPromise = new Promise((resolve, reject) => {
      console.log("delayedPromise executor")
      setTimeout(() => {
        console.log("Resolving delayedPromise")
        resolve("Hello")
      }, 1000)
    })
    
    console.log("After creating delayedPromise")
    
    delayedPromise.then(contents => {
      console.log("delayedPromise resolve handled with", contents)
    })
    
    const immediatePromise = new Promise((resolve, reject) => {
      console.log("immediatePromise executor")
      resolve("World")
    })
    
    immediatePromise.then(contents => {
      console.log("immediatePromise resolve handled with", contents)
    })
    
    console.log("At code end")
    // At code start
    // delayedPromise executor
    // After creating delayedPromise
    // immediatePromise executor
    // At code end
    // immediatePromise resolve handled with World
    // Resolving delayedPromise
    // delayedPromise resolve handled with Hello
    

    具体的执行逻辑为:

    • 代码开始执行,通过 Promise 构造器创建一个 delayedPromise,其中的 console.log()setTimeout()(也可以是其他异步操作)函数立即执行
    • delayedPromise 创建之后,其最终的结果和状态(是否成功执行)不能立即知晓,因此处于 pending 状态
    • 调用 delayedPromisethen 方法,将一个当 promise 成功 resolve 后才执行的 callback 函数放到执行计划中
    • 继续创建另一个 immediatePromise,该 promise 会在创建的过程中立即 resolve,因此其创建完成后即处于 resolved 状态
    • 调用 immediatePromisethen 方法,注册一个当 promise 成功 resolve 后才执行的 callback 函数

    从最终的结果中可以看出,即便 immediatePromise 在创建后即处于 resolved 状态,At code end 实际上是先于前面的 immediatePromise.then() 输出的。
    原因是 promise 被设计成专门针对异步操作,then() 方法中的 callback 会永远在当前事件循环中所有代码执行完后才开始触发。

    因此实际的执行顺序为:
    At code start -> 创建 delayedPromise -> 通过 then() 注册 delayPromise 状态为 resolved 时触发的 callback -> 创建 immediatePromise -> 通过 then() 注册 immediatePromise 状态为 resolved 时触发的 callback -> At code end -> immediatePromise 先 resolved,其关联的 callback 执行 -> delayedPromise resolved,其关联的 callback 执行

    三、Chaining Promises

    截止到前面的介绍,promise 看起来只不过在 callback 的基础上做了一点点有限的提升。实际 promise 支持多种形式的连接,足以完成更加复杂的异步逻辑。

    每次对 promise 的 then()catch() 方法的调用,实际上都会创建和返回另一个 promise 对象。第二个 promise 对象只有在第一个 promise fulfilled 或 rejected 后才会被 resolve。

    let p1 = new Promise(function(resolve, reject) {
      resolve(42)
    })
    
    p1.then(function(value) {
      console.log(value)
    }).then(function() {
      console.log("Finished")
    })
    
    // 42
    // Finished
    

    unchained 版本:

    let p1 = new Promise(function(resolve, reject) {
      resolve(42)
    })
    
    let p2 = p1.then(function(value) {
      console.log(value)
    })
    
    p2.then(function() {
      console.log("Finished")
    })
    

    p2.then() 也会返回一个 promise 对象,只不过它没有在代码中使用。

    错误捕获

    Promise chaining 允许用户捕获之前的 promise 中出现的错误。

    let p1 = new Promise(function(resolve, reject) {
      resolve(42)
    })
    
    p1.then(function(value) {
      throw new Error("Boom!")
    }).catch(function(error) {
      console.log(error.message)
    })
    // Boom!
    

    p1 的 fulfillment handler 抛出异常,第二个 promise 的 catch() 方法通过它的 rejection handler 接收到该异常。同样的方式也适用于 rejection handler 抛出异常:

    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)
    })
    // Explosion!
    // Boom!
    

    executor 抛出异常触发 p1 的 rejection handler,该 handler 又抛出另一个异常触发第二个 promise 的 rejection handler。

    Promise Chain 中的返回值

    Promise Chain 中另一个很重要的特性即在两个 promise 之间传递数据。之前的代码中,可以通过 executor 中的 resovle() 函数将值传递给该 promise 的 fulfillment handler。此外,还可以通过为 fulfillment handler 指定一个返回值,将该值沿着 promise chain 传递。

    let p1 = new Promise(function(resolve, reject) {
      resolve(42)
    })
    
    p1.then(function(value) {
      console.log(value)
      return value + 1
    }).then(function(value) {
      console.log(value)
    })
    
    // 42
    // 43
    

    同样的操作也可以用在 rejection handler 上:

    let p1 = new Promise(function(resolve, reject) {
      reject(42)
    })
    
    p1.catch(function(value) {
      console.log(value)
      return value + 1
    }).then(function(value) {
      console.log(value)
    })
    
    // 42
    // 43
    

    四、响应多个 Promise

    之前的代码中都是一次只响应一个 promise,但是有时候需要监控多个 promise 的状态并决定之后的动作。ECMAScript 6 提供了两种方法(Promise.all()Promise.race)应对这些情况。

    Promise.all()

    Promise.all() 方法只接收一个包含所有需要监控的 promise 的可迭代对象(如列表)作为参数,并且只有当这些需要监控的 promise 全部 resolved 时,Promise.all() 返回的 promise 才会 resolved。

    let p1 = new Promise(function(resolve, reject) {
      resolve(42)
    })
    
    let p2 = new Promise(function(resolve, reject) {
      resolve(43)
    })
    
    let p3 = new Promise(function(resolve, reject) {
      resolve(44)
    })
    
    let p4 = Promise.all([p1, p2, p3])
    
    p4.then(function(value) {
      console.log(Array.isArray(value))  // true
      console.log(value[0])  // 42
      console.log(value[1])  // 43
      console.log(value[2])  // 44
    })
    

    Promise.all() 创建了 promise p4。只有当列表中的 promise p1,p2,p3 全部 fulfilled 之后,p4 最终才会 fulfilled。
    前面 3 个 promise resolve 的数字组成列表传递给 p4 的 fulfillment handler,这些数字与生产它们的 promise 的位置是一一对应的。

    如果任意一个传入 Promise.all() 的 promise 状态是 rejected,则 Promise.all() 返回的 promise 也会立即 rejected,不会等待其他 promise 结束。

    let p1 = new Promise(function(resolve, reject) {
      resovle(42)
    })
    
    let p2 = new Promise(function(resolve, reject) {
      reject(43)
    })
    
    let p3 = new Promise(function(resolve, reject) {
      resolve(44)
    })
    
    let p4 = Promise.all([p1, p2, p3])
    p4.catch(function(value) {
      console.log(Array.isArray(value))  // false
      console.log(value)  // 43
    })
    

    在上面的代码中,p2 的状态为 rejected,p4 的 rejection handler 会立即调用,不会等待 p1 和 p3 执行完毕(p1 和 p3 最终会执行完毕,只是 p4 不会等它们)。

    Promise.race()

    Promise.race() 同样接收一个包含需要监控的多个 promise 的可迭代对象,返回一个新的 promise。但是不同于 Promise.all() 会等待所有监控中的 promise resolved,Promise.race() 会在列表中任意一个 promise resolve 后立即返回。

    let p1 = new Promise(function(resolve, reject) {
      setTimeout(resolve, 500, 42)
    
    })
    
    let p2 = new Promise(function(resolve, reject) {
      setTimeout(resolve, 100, 43)
    })
    
    let p3 = new Promise(function(resolve, reject) {
        setTimeout(resolve, 200, 44)
    
    })
    
    let p4 = Promise.race([p1, p2, p3])
    
    p4.then(function(value) {
        console.log(value)
    })
    
    // 43
    

    传递给 Promise.race() 的 promise 像是处在一个赛道中,看哪一个先执行完毕。如果第一个运行完的 promise 状态为 fulfilled,则最后返回的 promise 状态为 fulfilled;如果第一个运行完的 promise 状态为 rejected,则最后返回的 promise 状态为 rejected。

    参考资料

    Understanding ECMAScript 6
    Secrets of the JavaScript Ninja, Second Edition

    相关文章

      网友评论

          本文标题:理解 JavaScript(ECMAScript 6)—— 异步

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