Promise 必知必会

Jan 17, 2022
·

回调

🚩 异步 行为(action):现在开始执行的行为,但它们会在稍后完成(例如,setTimeout 函数就是一个这样的函数;例如加载脚本和模块)

实际中的异步行为的示例:

/**
 * 使用给定的 src 加载脚本
 * @param src
 **/
function loadScript(src) {
  // 创建一个 <script> 标签,并将其附加到页面
  // 这将使得具有给定 src 的脚本开始加载,并在加载完成后运行
  let script = document.createElement("script")
  script.src = src
  document.head.append(script)
}

可以像这样使用这个函数:

// 在给定路径下加载并执行脚本
loadScript("/my/script.js")
 
// loadScript 下面的代码
// 不会等到脚本加载完成才执行
// ...
 
// 💡脚本是“异步”调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行。如果在 loadScript(…) 下面有任何其他代码,它们不会等到脚本加载完成才执行

假设需要在新脚本加载后立即使用它,这将不会有效:

loadScript("/my/script.js") // 这个脚本有 "function newFunction() {…}"
 
newFunction() // 没有这个函数!

😭 到目前为止,loadScript 函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已。但是希望了解脚本何时加载完成,以使用其中的新函数和变量

💡 添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:

function loadScript(src, callback) {
  let script = document.createElement("script")
  script.src = src
  script.onload = () => callback(script)
  document.head.append(script)
}
 
loadScript(
  "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js",
  (script) => {
    // 在脚本加载完成后,回调函数才会执行
    alert(`Cool, the script ${script.src} is loaded`)
    alert(_) // 所加载的脚本中声明的函数
  },
)

这就是被称为“基于回调”的异步编程风格: 异步执行某项功能的函数应该提供一个 callback 参数用于在相应事件完成时调用

🚩 回调地狱

如何依次加载两个脚本:第一个,然后是第二个?第三个?

loadScript("/my/script.js", function (script) {
  loadScript("/my/script2.js", function (script) {
    loadScript("/my/script3.js", function (script) {
      // ...加载完所有脚本后继续
    })
  })
})

加入处理 Error:

loadScript("1.js", function (error, script) {
  if (error) {
    handleError(error)
  } else {
    // ...
    loadScript("2.js", function (error, script) {
      if (error) {
        handleError(error)
      } else {
        // ...
        loadScript("3.js", function (error, script) {
          if (error) {
            handleError(error)
          } else {
            // ...加载完所有脚本后继续 (*)
          }
        })
      }
    })
  }
})

这就是著名的回调地狱或“厄运金字塔”

💡 可以通过使每个行为都成为一个独立的函数来尝试减轻这种问题

loadScript("1.js", step1)
 
function step1(error, script) {
  if (error) {
    handleError(error)
  } else {
    // ...
    loadScript("2.js", step2)
  }
}
 
function step2(error, script) {
  if (error) {
    handleError(error)
  } else {
    // ...
    loadScript("3.js", step3)
  }
}
 
function step3(error, script) {
  if (error) {
    handleError(error)
  } else {
    // ...加载完所有脚本后继续 (*)
  }
}

优缺点

- 没有深层的嵌套,独立为顶层函数

- 可读性差

- 没有重用

最好的方法之一就是 promise

Promise

🚩 语法

let promise = new Promise(function (resolve, reject) {
  // executor
  // 当 promise 被构造完成时,executor自动执行此函数
  // executor 通常是异步任务
  // ...
})
  // handler
  .then(
    (result) => {
      // ...
    },
    (error) => {
      // ...
    },
  )
1. new Promise 被创建executor 被自动且立即调用
 
2. new Promise 构造器返回的 promise 对象具有以下内部属性
 
    - state最初是 "pending"然后在 resolve 被调用时变为 "fulfilled"或者在 reject 被调用时变为 "rejected"
 
    - result最初是 undefined然后在 resolve(value) 被调用时变为 value或者在 reject(error) 被调用时变为 error
 
3.与最初的pendingpromise 相反一个 resolved  rejected  promise 都会被称为settled
 
4.executor 只能调用一个 resolve 或一个 reject任何状态的更改都是最终的不可逆

🚩 立即 resolve/reject 的 Promise

// executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject,但这不是必须的;还可以立即调用 resolve 或 reject
 
// 💡当开始做一个任务时,但随后看到一切都已经完成并已被缓存时,可能就会发生这种情况。这挺好😀
 
let promise = new Promise(function (resolve, reject) {
  // 不花时间去做这项工作
  resolve(123) // 立即给出结果:123
})

🚩 示例:加载脚本的 loadScript 函数

基于回调函数的变体版本:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
 
  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));
 
  document.head.append(script);
}
 
// 用法:
 
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  // 在脚本加载完成后,回调函数才会执行
  alert(`${script.src} is loaded!`);
  alert( _ ); // 所加载的脚本中声明的函数
});

基于 Promise 重写的版本:

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    let script = document.createElement("script")
    script.src = src
 
    script.onload = () => resolve(script)
    script.onerror = () => reject(new Error(`Script load error for ${src}`))
 
    document.head.append(script)
  })
}
 
// 用法:
 
let promise = loadScript(
  "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js",
)
 
promise.then(
  (script) => alert(`${script.src} is loaded!`),
  (error) => alert(`Error: ${error.message}`),
)
 
promise.then((script) => alert("Another handler..."))

Promise 链

🚩Promise 链:回忆回调中,何依次加载两个脚本:第一个,然后是第二个?第三个?

// 💡Promise 提供了一些方案来做到这一点:Promise 链
 
// like this
 
new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000) // (*)
})
  .then(function (result) {
    // (**)
 
    alert(result) // 1
    return result * 2
  })
  .then(function (result) {
    // (***)
 
    alert(result) // 2
    return result * 2
  })
  .then(function (result) {
    alert(result) // 4
    return result * 2
  })
 
// 📌为什么可以?因为对 promise.then 的调用会返回了一个 promise,所以我们可以在其之上调用下一个 .then
 
// 当处理程序(handler)返回一个值时,它将成为该 promise 的 result,所以将使用它调用下一个 .then
 
// 💣`新手常犯的一个经典错误:从技术上讲,我们也可以将多个 .then 添加到一个 promise 上。但这并不是 promise 链(chaining)`
 
let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000)
})
 
promise.then(function (result) {
  alert(result) // 1
  return result * 2
})
 
promise.then(function (result) {
  alert(result) // 1
  return result * 2
})
 
promise.then(function (result) {
  alert(result) // 1
  return result * 2
})
 
// 💡这里所做的只是一个 promise 的几个处理程序(handler)。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务

🚩 返回 promise

- .then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise

- 在这种情况下,其他的处理程序(handler)将【等待它 settled 后再获得其结果(result)】

示例:promise 化的 loadScript

loadScript("/article/promise-chaining/one.js")
  .then((script) => loadScript("/article/promise-chaining/two.js"))
  .then((script) => loadScript("/article/promise-chaining/three.js"))
  .then((script) => {
    // 脚本加载完成,我们可以在这儿使用脚本中声明的函数
    one()
    two()
    three()
  })
 
// 💡注意:这儿每个 loadScript 调用都返回一个 promise,并且在它 resolve 时下一个 .then 开始运行。然后,它启动下一个脚本的加载。所以,脚本是一个接一个地加载的
 
// 💡并且代码仍然是“扁平”的 — 它向下增长,而不是向右
 
// ...
 
// 从技术上讲,可以向每个 loadScript 直接添加 .then,就像这样:
 
loadScript("/article/promise-chaining/one.js").then((script1) => {
  loadScript("/article/promise-chaining/two.js").then((script2) => {
    loadScript("/article/promise-chaining/three.js").then((script3) => {
      // 此函数可以访问变量 script1,script2 和 script3
      one()
      two()
      three()
    })
  })
})
 
// 💡这段代码做了相同的事儿:按顺序加载 3 个脚本。但它是“向右增长”的。所以会有和使用回调函数一样的问题
 
// 👍刚开始使用 promise 的人可能不知道 promise 链,所以他们就这样写了。通常,链式是首选

Thenables

- 确切地说,处理程序(handler)返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 — 一个具有方法 .then 的任意对象

- thenable对象会被当做一个 promise 来对待

- 这个想法是,第三方库可以实现自己的“promise 兼容(promise-compatible)”对象;它们可以具有扩展的方法集,但也与原生的 promise 兼容,因为它们实现了 .then 方法


- 这个特性允许将自定义的对象与 promise 链集成在一起,而不必继承自 Promise

示例:

class Thenable {
  constructor(num) {
    this.num = num
  }
  then(resolve, reject) {
    alert(resolve) // function() { native code }
    // 1 秒后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000) // (**)
  }
}
 
new Promise((resolve) => resolve(1))
  .then((result) => {
    return new Thenable(result) // (*)
  })
  .then(alert) // 1000ms 后显示 2

🚩 作为一个好的做法:异步行为应该始终返回一个 promise

- 这样就可以使得之后计划后续的行为成为可能

- 即使现在不打算对链进行扩展,但之后可能会需要

示例:

function loadJson(url) {
  return fetch(url).then((response) => response.json())
}
 
function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`).then((response) =>
    response.json(),
  )
}
 
function showAvatar(githubUser) {
  return new Promise(function (resolve, reject) {
    let img = document.createElement("img")
    img.src = githubUser.avatar_url
    img.className = "promise-avatar-example"
    document.body.append(img)
 
    setTimeout(() => {
      img.remove()
      resolve(githubUser)
    }, 3000)
  })
}
 
// 使用它们:
loadJson("/article/promise-chaining/user.json")
  .then((user) => loadGithubUser(user.name))
  .then(showAvatar)
  .then((githubUser) => alert(`Finished showing ${githubUser.name}`))
// ...

错误处理

🚩Promise 链在错误(error)处理

- 当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序(handler);这在实际开发中非常方便

- .catch 不必是立即的;它可能在一个或多个 .then 之后出现

示例:

fetch("/article/promise-chaining/user.json")
  .then((response) => response.json())
  .then((user) => fetch(`https://api.github.com/users/${user.name}`))
  .then((response) => response.json())
  .then(
    (githubUser) =>
      new Promise((resolve, reject) => {
        let img = document.createElement("img")
        img.src = githubUser.avatar_url
        img.className = "promise-avatar-example"
        document.body.append(img)
 
        setTimeout(() => {
          img.remove()
          resolve(githubUser)
        }, 3000)
      }),
  )
  .catch((error) => alert(error.message))

🚩 隐式 try…catch

- Promise 的执行者(executor)和 promise 的处理程序(handler)周围有一个“隐式的 try..catch”

- 如果发生异常,它(译注:指异常)就会被捕获,并被视为 rejection 进行处理

示例:

// excutor 中
 
new Promise((resolve, reject) => {
  throw new Error("Whoops!")
}).catch(alert) // Error: Whoops!
 
// 等同于
 
new Promise((resolve, reject) => {
  reject(new Error("Whoops!"))
}).catch(alert) // Error: Whoops!
 
// ...
 
// handler 中
 
new Promise((resolve, reject) => {
  resolve("ok")
})
  .then((result) => {
    throw new Error("Whoops!") // reject 这个 promise
  })
  .catch(alert) // Error: Whoops!

🚩 再次抛出(Rethrowing)

- 如果在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序(handler)。如果处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序(handler)
// 执行流:catch -> then
new Promise((resolve, reject) => {
  throw new Error("Whoops!")
})
  .catch(function (error) {
    alert("The error is handled, continue normally")
  })
  .then(() => alert("Next successful handler runs"))
// 执行流:catch -> catch
new Promise((resolve, reject) => {
  throw new Error("Whoops!")
})
  .catch(function (error) {
    // (*)
 
    if (error instanceof URIError) {
      // 处理它
    } else {
      alert("Can't handle such error")
 
      throw error // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
    }
  })
  .then(function () {
    /* 不在这里运行 */
  })
  .catch((error) => {
    // (**)
 
    alert(`The unknown error has occurred: ${error}`)
    // 不会返回任何内容 => 执行正常进行
  })

🚩 未处理的 rejection

new Promise(function () {
  noSuchFunction() // 这里出现 error(没有这个函数)
}).then(() => {
  // 一个或多个成功的 promise 处理程序(handler)
}) // 尾端没有 .catch!
 
// ...
 
// 当一个 error 没有被处理会发生什么?
 
// 💡如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序(handler)。但是上面这个例子中并没有这样的处理程序(handler)。因此 error 会“卡住(stuck)”。没有代码来处理它
 
// 在实际开发中,就像代码中常规的未处理的 error 一样,这意味着某些东西出了问题
 
// 当发生一个常规的错误(error)并且未被 try..catch 捕获时会发生什么?脚本死了,并在控制台(console)中留下了一个信息。对于在 promise 中未被处理的 rejection,也会发生类似的事儿

JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error

- 在浏览器中,可以使用 unhandledrejection 事件来捕获这类 error
window.addEventListener("unhandledrejection", function (event) {
  // 这个事件对象有两个特殊的属性:
  alert(event.promise) // [object Promise] - 生成该全局 error 的 promise
  alert(event.reason) // Error: Whoops! - 未处理的 error 对象
})
 
new Promise(function () {
  throw new Error("Whoops!")
}) // 没有用来处理 error 的 catch

Promise API

在 Promise 类中,有 5 种静态方法

- Promise.all([iterable])

- Promise.allSettled([iterable])

- Promise.race([iterable])

- Promise.resolve()

- Promise.reject()

🚩Promise.all

语法

// 接受一个 promise 数组(可以是任何可迭代的)作为参数并返回一个新的 promise
 
let promise = Promise.all([iterable])

注意

- 并行执行多个 promise,当所有给定的 promise 都被 成功 时,新的 promise 才会 resolve,并且其结果数组将成为新的 promise 的结果

- 结果数组中元素的顺序与其在源 promise 中的顺序相同(即使第一个 promise 花费了最长的时间)

- 如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error

🚩 如果出现 error,其他 promise 将被忽略

- 如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略

- 例如,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略

- Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念

🚩Promise.all(iterable) 允许在 iterable 中使用 non-promise 的“常规”值

// Promise.all(...) 接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数。但是,如果这些对象中的任何一个不是 promise,那么它将被“按原样”传递给结果数组
 
Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3,
]).then(alert) // 1, 2, 3

🚩Promise.allSettled

Promise.allSettled 等待所有的 promise 都被 settle无论结果如何,结果数组具有:
 
- {status:"fulfilled", value:result} 对于成功的响应
 
- {status:"rejected", reason:error} 对于 error

Polyfill

if (!Promise.allSettled) {
  const rejectHandler = (reason) => ({ status: "rejected", reason })
 
  const resolveHandler = (value) => ({ status: "fulfilled", value })
 
  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map((p) =>
      Promise.resolve(p).then(resolveHandler, rejectHandler),
    )
    return Promise.all(convertedPromises)
  }
}

🚩Promise.race

- 只等待第一个 settled 的 promise 并获取其结果(或 error)

示例

Promise.race([
  new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
  new Promise((resolve, reject) =>
    setTimeout(() => reject(new Error("Whoops!")), 2000),
  ),
  new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).then(alert) // 1

🚩Promise.resolve/reject

语法

// 结果 value 创建一个 resolved 的 promise
Promise.resolve(value)
 
// 等同于
 
let promise = new Promise((resolve) => resolve(value))
 
//...
 
// Promise.reject() 类似
- 当一个函数被期望返回一个 promise 时,这个方法用于兼容性

- 💡这里的兼容性是指,直接从缓存中获取了当前操作的结果 value,但是期望返回的是一个 promise,所以可以使用 Promise.resolve(value) 将 value “封装”进 promise,以满足期望返回一个 promise 的这个需求

示例:

let cache = new Map()
 
function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)) // (*)
  }
 
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      cache.set(url, text)
      return text
    })
}
 
// 💡可以使用 loadCached(url).then(…),因为该函数保证了会返回一个 promise。可以放心地在 loadCached 后面使用 .then。这就是 (*) 行中 Promise.resolve 的目的

Promisification

- “Promisification” 指将一个接受回调的函数转换为一个返回 promise 的函数

- 由于许多函数和库都是基于回调的,所以将基于回调的函数和库 promisify 是有意义的

示例:

function loadScript(src, callback) {
  let script = document.createElement("script")
  script.src = src
 
  script.onload = () => callback(null, script)
  script.onerror = () => callback(new Error(`Script load error for ${src}`))
 
  document.head.append(script)
}
 
// 用法:
// loadScript('path/script.js', (err, script) => {...})
 
// ...
 
// promisify
 
let loadScriptPromise = function (src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err)
      else resolve(script)
    })
  })
}
 
// 用法:
// loadScriptPromise('path/script.js').then(...)

新的函数是对原始的 loadScript 函数的包装,在实际开发中,可能需要 promisify 很多函数

🚩promisify

function promisify(f) {
  return function (...args) { // 返回一个包装函数(wrapper-function) (*)
    return new Promise((resolve, reject) => {
      function callback(err, result) { // 对 f 的自定义的回调 (**)
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }
 
      args.push(callback); // 将自定义的回调附加到 f 参数(arguments)的末尾
 
      f.call(this, ...args); // 调用原始的函数
    });
  };
}
 
// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

🚩promisification 函数的模块(module)

- https://github.com/digitaldesignlabs/es6-promisify

- 在 Node.js 中,有一个内建的 promisify 函数 util.promisify

🚩Promisification 场景

- Promisification 不是回调的完全替代

- 请记住,一个 promise 可能只有一个结果,但从技术上讲,一个回调可能被调用很多次

- 因此,promisification 仅适用于调用一次回调的函数。进一步的调用将被忽略

references