Skip to content

简介:回调

JavaScript 主机(host)环境提供了许多函数,这些函数允许我们计划 异步 行为(action)—— 也就是在我们执行一段时间后才自行完成的行为。

  1. onload:通常会在脚本加载和执行完成后执行一个函数。
  2. 在回调中回调
  3. 处理 Error
JavaScript
	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);
	}
  1. 厄运金字塔
    1. 随着调用嵌套的增加,代码层次变得更深,维护难度也随之增加

Promise

  1. “生产者代码(producing code)”会做一些事儿,并且会需要一些时间。例如,通过网络加载数据的代码。它就像一位“歌手”。

  2. “消费者代码(consuming code)”想要在“生产者代码”完成工作的第一时间就能获得其工作成果。许多函数可能都需要这个结果。这些就是“粉丝”。

  3. Promise 是将“生产者代码”和“消费者代码”连接在一起的一个特殊的 JavaScript 对象。用我们的类比来说:这就是就像是“订阅列表”。“生产者代码”花费它所需的任意长度时间来产出所承诺的结果,而 “promise” 将在它(译注:指的是“生产者代码”,也就是下文所说的 executor)准备好时,将结果向所有订阅了的代码开放。

  4. Promise 对象的构造器(constructor)语法

JavaScript
	let promise = new Promise(function(resolve,reject){
		//生产者代码
	})
	//当 `new Promise` 被创建,executor 会自动运行。
	- `resolve(value)` —— 如果任务成功完成并带有结果 `value`
	- `reject(error)` —— 如果出现了 error,`error` 即为 error 对象。

只有一个结果或一个 error
executor 只能调用一个 `resolve` 或一个 `reject`。任何状态的更改都是最终的。


以 `Error` 对象 reject:如果什么东西出了问题,executor 应该调用 `reject`


executor 通常是异步执行某些操作,并在一段时间后调用 `resolve/reject`,但这不是必须的。我们还可以立即调用 `resolve` 或 `reject`


`state` 和 `result` 都是内部的
Promise 对象的 `state` 和 `result` 属性都是内部的。我们无法直接访问它们。
  1. 消费者:then,catch
    1. Promise 对象充当的是 executor(“生产者代码”)和消费函数之间的连接,后者将接收结果或 error。可以通过使用 .then 和 .catch 方法注册消费函数。
    2. then
      1. .then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。
      2. .then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。
    3. catch
      1. 如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的
JavaScript
let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) 与 promise.then(null, f) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!"
  1. 清理:finally
    1. 调用 .finally(f) 类似于 .then(f, f),因为当 promise settled 时 f 就会执行:无论 promise 被 resolve 还是 reject。
    2. finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。
    3. finally 处理程序(handler)没有参数。在 finally 中,我们不知道 promise 是否成功。没关系,因为我们的任务通常是执行“常规”的完成程序(finalizing procedures)。
    4. finally 处理程序将结果或 error “传递”给下一个合适的处理程序。
    5. finally 处理程序也不应该返回任何内容。如果它返回了,返回的值会默认被忽略。
      • 当 finally 抛出 error 时,执行将转到最近的 error 的处理程序。
    6. 我们可以对 settled 的 promise 附加处理程序
      1. 如果 promise 为 pending 状态,.then/catch/finally 处理程序(handler)将等待它的结果。
      2. 当我们向一个 promise 添加处理程序时,它可能已经 settled 了
  2. 示例:loadScript
JavaScript
//基本回调函数
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);
} 

//用 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 链

JavaScript
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;

});

运行流程:
1. 初始 promise 在 1 秒后 resolve `(*)`
2. 然后 `.then` 处理程序被调用 `(**)`,它又创建了一个新的 promise(以 `2` 作为值 resolve)。
3. 下一个 `then` `(***)` 得到了前一个 `then` 的值,对该值进行处理(*2)并将其传递给下一个处理程序。
4. 以此类推

新手常犯的一个经典错误:从技术上讲,我们也可以将多个 .then 添加到一个 promise 上。但这并不是 promise 链(chaining)。

  1. 返回 promise
    1. .then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。
  2. 示例:loadScript
JavaScript
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();
  });

处理程序返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 —— 一个具有方法 `.then` 的任意对象。它会被当做一个 promise 来对待。
  1. 更复杂的示例:fetch
JavaScript
	fetch('/article/promise-chaining/user.json')
	  // 当远程服务器响应时,下面的 .then 开始执行
	  .then(function(response) {
	    // 当 user.json 加载完成时,response.text() 会返回一个新的 promise
	    // 该 promise 以加载的 user.json 为 result 进行 resolve
	    return response.text();
	  })
	  .then(function(text) {
	    // ……这是远程文件的内容
	    alert(text); // {"name": "iliakan", "isAdmin": true}
	  });

//加载用户个人资料显示头像
	// 发送一个对 user.json 的请求
	fetch('/article/promise-chaining/user.json')
	  // 将其加载为 JSON
	  .then(response => response.json())
	  // 发送一个到 GitHub 的请求
	  .then(user => fetch(`https://api.github.com/users/${user.name}`))
	  // 将响应加载为 JSON
	  .then(response => response.json())
	  // 显示头像图片(githubUser.avatar_url)3 秒(也可以加上动画效果)
	  .then(githubUser => {
	    let img = document.createElement('img');
	    img.src = githubUser.avatar_url;
	    img.className = "promise-avatar-example";
	    document.body.append(img);
	
	    setTimeout(() => img.remove(), 3000); // (*)
	  });

使用 promise 进行错误处理

当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序。

JavaScript
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 的处理程序周围有一个“隐式的 try..catch”。如果发生异常,它就会被捕获,并被视为 rejection 进行处理。

再次抛出(Rethrowing).catch 块正常完成。所以下一个成功的 .then 处理程序就会被调用。

未处理的 rejection 如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序。 但是如果error没有程序去处理,脚本就会死掉。 JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error。

Promise API

在 Promise 类中,有 6 种静态方法。

  1. Promise.all
    1. let promise = Promise.all(iterable);
    2. 希望并行执行多个 promise,并等待所有 promise 都准备就绪。
    3. Promise.all 接受一个可迭代对象(通常是一个数组项为 promise 的数组),并返回一个新的 promise。
    4. 如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。
    5. Promise.all(iterable) 允许在 iterable 中使用非 promise 的“常规”值
  2. Promise.allSettled
    1. Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组会是这样的
      • 对成功的响应,结果数组对应元素的内容为 {status:"fulfilled", value:result}
      • 对出现 error 的响应,结果数组对应元素的内容为 {status:"rejected", reason:error}
  3. Polyfill
    1. 如果浏览器不支持 Promise.allSettled,很容易进行 polyfill
    2. 这个处理程序将成功的结果 value 转换为 {status:'fulfilled', value},将 error reason 转换为 {status:'rejected', reason}。这正是 Promise.allSettled 的格式。
    3. 我们就可以使用 Promise.allSettled 来获取 所有 给定的 promise 的结果,即使其中一些被 reject。
  4. Promise.race
    1. 与 Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。
    2. let promise = Promise.race(iterable);
  5. Promise.any
    1. 1与 Promise.race 类似,区别在于 Promise.any 只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。
    2. let promise = Promise.any(iterable);
  6. Promise.resolve/reject
    1. 很少需要使用 Promise.resolve 和 Promise.reject 方法,因为 async/await 语法
      1. Promise.resolve
        1. Promise.resolve(value) 用结果 value 创建一个 resolved 的 promise。
        2. let promise = new Promise(resolve => resolve(value));
      2. Promise.reject
        1. Promise.reject(error) 用 error 创建一个 rejected 的 promise。
        2. let promise = new Promise((resolve, reject) => reject(error));

Promisification

将一个接受回调的函数转换为一个返回 promise 的函数。

JavaScript
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(...);

微任务(Microtask)

promise 的处理程序 .then.catch 和 .finally 都是异步的

  1. 微任务队列(Microtask queue)
    1. 异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(V8 术语)。
      • 队列(queue)是先进先出的:首先进入队列的任务会首先运行。
        • 只有在 JavaScript 引擎中没有其它任务在运行时,才开始执行任务队列中的任务。
    2. 如果执行顺序对我们很重要该怎么办?我们怎么才能让 code finished 在 promise done 之后出现呢?
      1. 使用 .then 将其放入队列
  2. 未处理的 rejection
    1. 如果一个 promise 的 error 未被在微任务队列的末尾进行处理,则会出现“未处理的 rejection”。

async、await

  1. async function
JavaScript
	async function f() {
	  return 1;
	}
	即这个函数总是返回一个 promise。其他值将自动被包装在一个 resolved 的 promise 中。
  1. await
JavaScript
	// 只在 async 函数内工作
	let value = await promise;
1. 关键字 `await` 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果。
2. 不能在普通函数中使用 `await`
3. 现代浏览器在 modules 里允许顶层的 `await`
4. `await` 接受 “thenables”
5. `async/await` 可以和 `Promise.all` 一起使用
6. Class 中的 async 方法
JavaScript
	class Waiter {
	  async wait() {
	    return await Promise.resolve(1);
	  }
	}
	
	new Waiter()
	  .wait()
	  .then(alert); // 1(alert 等同于 result => alert(result))
  1. Error 处理
JavaScript
	async function f() {
	  await Promise.reject(new Error("Whoops!"));
	}


//实际开发中
async function f() {
  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // 捕获到 fetch 和 response.json 中的错误
    alert(err);
  }
}
f();