Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promise 必知必会 #91

Open
shanejix opened this issue Jan 17, 2022 · 0 comments
Open

Promise 必知必会 #91

shanejix opened this issue Jan 17, 2022 · 0 comments
Labels

Comments

@shanejix
Copy link
Owner

shanejix commented Jan 17, 2022

同步链接: https://www.shanejix.com/posts/Promise 必知必会/

回调

🚩 异步 行为(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.与最初的 “pending” promise 相反,一个 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 的“常规”值

// romise.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

作者:shanejix
出处:https://www.shanejix.com/posts/Promise 必知必会/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

@shanejix shanejix reopened this Feb 10, 2022
@shanejix shanejix changed the title Implementation Promise Promise 必知必会 Feb 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant