Tian Jiale's Blog

Promise 深入 + 自定义 Promise

前置知识准备

函数对象与实例对象

函数对象:将函数作为对象使用时, 简称为函数对象。

实例对象:new 函数产生的对象, 简称为实例对象。

function Fn() {
  // Fn函数
}
const fn = new Fn(); // Fn是构造函数  fn是实例对象(简称为对象)
console.log(Fn.prototype); // Fn是函数对象
Fn.call({}); // Fn是函数对象
$('#test'); // jQuery函数
$.get('/test'); // jQuery函数对象

两种类型的回调函数

同步回调

理解:立即执行, 完全执行完了才结束, 不会放入回调队列中

例子:数组遍历相关的回调函数 / Promise 的 excutor 函数

const arr = [1, 3, 5];
arr.forEach((item) => {
  // 遍历回调, 同步回调函数, 不会放入列队, 一上来就要执行完
  console.log(item);
});
console.log('forEach()之后');
// 1
// 3
// 5
// forEach()之后

异步回调

理解:不会立即执行, 会放入回调队列中将来执行

例子:定时器回调 / ajax 回调 / Promise 的成功|失败的回调

setTimeout(() => {
  // 异步回调函数, 会放入队列中将来执行
  console.log('timout callback()');
}, 0);
console.log('setTimeout()之后');
// setTimeout()之后
// timout callback()

程序错误

错误类型

  • Error: 所有错误的父类型

  • ReferenceError: 引用的变量不存在

  • TypeError: 数据类型不正确的错误

  • RangeError: 数据值不在其所允许的范围内

  • SyntaxError: 语法错误

/* ReferenceError: 引用的变量不存在 */
console.log(a)
// ReferenceError: a is not defined
console.log('-----') // 没有捕获error, 下面的代码不会执行

/* TypeError: 数据类型不正确的错误 */
let b
console.log(b.xxx)
// TypeError: Cannot read property 'xxx' of undefined
b = {}
b.xxx()
// TypeError: b.xxx is not a function


/* RangeError: 数据值不在其所允许的范围内 */
function fn() {
  fn()
}
fn()
// RangeError: Maximum call stack size exceeded

/* SyntaxError: 语法错误 */
const c = """"
// SyntaxError: Unexpected string

错误处理

捕获错误:try … catch

try {
  let d;
  console.log(d.xxx);
} catch (error) {
  console.log(error.message);
  console.log(error.stack);
}
console.log('出错之后');
// Cannot read property 'xxx' of undefined
// TypeError: Cannot read property 'xxx' of undefined
//    at <anonymous>:3:19
// 出错之后

抛出错误:throw error

function something() {
  if (Date.now() % 2 === 1) {
    console.log('当前时间为奇数, 可以执行任务');
  } else {
    // 如果时间是偶数抛出异常, 由调用来处理
    throw new Error('当前时间为偶数无法执行任务');
  }
}

// 捕获处理异常
try {
  something();
} catch (error) {
  alert(error.message);
}

错误对象

message 属性: 错误相关信息

stack 属性: 函数调用栈记录信息

Promise 深入

含义

Promise异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

特点

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

基本使用

// 1. 创建一个新的promise对象
const p = new Promise((resolve, reject) => {
  // 执行器函数  同步回调
  console.log('执行 excutor');
  // 2. 执行异步操作任务
  setTimeout(() => {
    const time = Date.now(); // 如果当前时间是偶数就代表成功, 否则代表失败
    // 3.1. 如果成功了, 调用resolve(value)
    if (time % 2 == 0) {
      resolve('成功的数据, time=' + time);
    } else {
      // 3.2. 如果失败了, 调用reject(reason)
      reject('失败的数据, time=' + time);
    }
  }, 1000);
});

console.log('new Promise()之后');

p.then(
  (value) => {
    // 接收得到成功的value数据    onResolved
    console.log('成功的回调', value);
  },
  (reason) => {
    // 接收得到失败的reason数据   onRejected
    console.log('失败的回调', reason);
  }
);

// 执行 excutor
// new Promise()之后
// 失败的回调 失败的数据, time=1615801107751

为什么要用 Promise

  1. 指定回调函数的方式更加灵活:

    ​ 旧的:必须在启动异步任务前指定

    ​ promise:启动异步任务 => 返回 promie 对象 => 给 promise 对象绑定回调函数(甚至可以在异步任务结束后指定)

  2. 支持链式调用, 可以解决回调地狱问题

    什么是回调地狱? 回调函数嵌套调用, 外部回调函数异步执行的结果是嵌套的回调函数执行的条件

    回调地狱的缺点? 不便于阅读 / 不便于异常处理

    解决方案? promise 链式调用

    终极解决方案? async/await

    详见 JavaScript 中的异步编程

Promise 的 API

  1. Promise 构造函数: Promise (excutor) {}

    excutor 函数: 同步执行 (resolve, reject) => {}

    resolve 函数: 内部定义成功时我们调用的函数 value => {}

    reject 函数: 内部定义失败时我们调用的函数 error => {}

    说明: excutor 会在 Promise 内部立即同步回调,异步操作在执行器中执行

  2. Promise.prototype.then 方法: (onResolved, onRejected) => {}

    onResolved 函数: 成功的回调函数 (value) => {}

    onRejected 函数: 失败的回调函数 (error) => {}

    说明: 指定用于得到成功 value 的成功回调和用于得到失败 error 的失败回调

    返回一个新的 promise 对象

  3. Promise.prototype.catch 方法: (onRejected) => {}

    onRejected 函数: 失败的回调函数 (error) => {}

    说明: then()的语法糖, 相当于: then(undefined, onRejected)

  4. Promise.resolve 方法: (value) => {}

    value: 成功的数据或 promise 对象

    说明: 返回一个成功/失败的 promise 对象

  5. Promise.reject 方法: (error) => {}

    error: 错误内容

    说明: 返回一个失败的 promise 对象

  6. Promise.all 方法: (promises) => {}

    promises: 包含 n 个 promise 的数组

    说明: 返回一个新的 promise, 只有所有的 promise 都成功才成功, 只要有一个失败了就直接失败

  7. Promise.race 方法: (promises) => {}

    promises: 包含 n 个 promise 的数组

    说明: 返回一个新的 promise, 第一个完成的 promise 的结果状态就是最终的结果状态

Promise 的几个关键问题

  1. 如何改变 promise 的状态?

    (1)resolve(value): 如果当前是 pendding 就会变为 resolved

    (2)reject(error): 如果当前是 pendding 就会变为 rejected

    (3)抛出异常: 如果当前是 pendding 就会变为 rejected

    const p = new Promise((resolve, reject) => {
      resolve(1); // promise变为resolved成功状态
      reject(2); // promise变为rejected失败状态
      throw new Error('出错了'); // 抛出异常, promse变为rejected失败状态, error为抛出的error
      throw 3; // 抛出异常, promse变为rejected失败状态, error为 抛出的3
    });
    
  2. 一个 promise 指定多个成功/失败回调函数, 都会调用吗?

    当 promise 改变为对应状态时都会调用

  3. 改变 promise 状态和指定回调函数谁先谁后?

    (1)都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调

    (2)如何先改状态再指定回调?

    ① 在执行器中直接调用 resolve()/reject()

    ② 延迟更长时间才调用 then()

    (3)什么时候才能得到数据?

    ① 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据

    ② 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据

  4. promise.then()返回的新 promise 的结果状态由什么决定?

    (1)简单表达: 由 then()指定的回调函数执行的结果决定

    (2)详细表达:

    ① 如果抛出异常, 新 promise 变为 rejected, reason 为抛出的异常

    ② 如果返回的是非 promise 的任意值, 新 promise 变为 resolved, value 为返回的值

    ③ 如果返回的是另一个新 promise, 此 promise 的结果就会成为新 promise 的结果

  5. promise 如何串连多个操作任务?

    (1)promise 的 then()返回一个新的 promise, 可以写成 then()的链式调用

    (2)通过 then 的链式调用串连多个同步/异步任务

  6. promise 异常传/穿透?

    (1)当使用 promise 的 then 链式调用时, 可以在最后指定失败的回调

    (2)前面任何操作出了异常, 都会传到最后失败的回调中处理

    // Promise.prototype.then()的默认调用
    Promise.prototype.then(
      (value) => Promise.resolve(calue),
      (error) => Promise.reject(error)
    );
    
  7. 中断 promise 链?

    (1)当使用 promise 的 then 链式调用时, 在中间中断, 不再调用后面的回调函数

    (2)办法: 在回调函数中返回一个 pendding 状态的 promise 对象

    return new Promise(() => {});
    

自定义(手写)Promise

代码

/**
 * 自定义Promise类
 */
class Promise {
  /**
   * Promise 构造函数
   * executor:执行器函数(同步执行)
   */
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.callbacks = [];

    const resolve = (value) => {
      if (this.status !== PENDING) {
        return;
      }
      this.status = RESOLVED;
      this.data = value;
      if (this.callbacks.length > 0) {
        setTimeout(() => {
          // 异步调用已在队列中的回调函数
          this.callbacks.forEach((callback) => {
            callback.onResolved(value);
          });
        });
      }
    };

    const reject = (error) => {
      if (this.status !== PENDING) {
        return;
      }
      this.status = REJECTED;
      this.data = error;
      if (this.callbacks.length > 0) {
        setTimeout(() => {
          // 异步调用已在队列中的回调函数
          this.callbacks.forEach((callback) => {
            callback.onRejected(error);
          });
        });
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  /**
   * Promise 原型对象的 then()
   * 指定成功和失败的回调函数
   * 返回一个新的 Promise 对象
   */
  then(onResolved, onRejected) {
    onResolved = typeof onResolved === 'function' ? onResolved : (value) => value;
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (error) => {
            throw error;
          };

    return new Promise((resolve, reject) => {
      const handle = (callback) => {
        try {
          const result = callback(this.data);
          if (result instanceof Promise) {
            result.then(resolve, reject);
          } else {
            resolve(result);
          }
        } catch (error) {
          reject(error);
        }
      };
      if (this.status === RESOLVED) {
        setTimeout(() => {
          handle(onResolved);
        });
      } else if (this.status === REJECTED) {
        setTimeout(() => {
          handle(onRejected);
        });
      } else {
        this.callbacks.push({
          onResolved: () => {
            handle(onResolved);
          },
          onRejected: () => {
            handle(onRejected);
          },
        });
      }
    });
  }

  /*
   * Promise 原型对象的 catch()
   * 指定失败的回调函数
   * 返回一个新的 Promise 对象
   */
  catch(onRejected) {
    return this.then(undefined, onRejected);
  }

  /**
   * Promise 函数的对象的 resolve 方法
   * 返回一个指定 error 的失败的 promise
   */
  static resolve(value) {
    if (value instanceof Promise) {
      return value;
    } else {
      return new Promise((resolve, reject) => {
        resolve(value);
      });
    }
  }

  /**
   * Promise 函数的对象的 reject 方法
   * 返回一个指定 error 的失败的 promise
   */
  static reject(error) {
    return new Promise((resolve, reject) => {
      reject(error);
    });
  }

  /**
   * Promise 函数对象的 all 方法
   * 返回一个 promise,只有当所有的 promise 都成功时才成功,否则只要有一个失败就失败
   */
  static all(promises) {
    return new Promise((resolve, reject) => {
      let values = [];
      promises.forEach((promise, index) => {
        promise = Promise.resolve(promise);
        promise.then(
          (value) => {
            values[index] = value;
            if (values.length === promises.length) {
              resolve(values);
            }
          },
          (error) => {
            reject(error);
          }
        );
      });
    });
  }

  /**
   * Promise 函数对象的 race 方法
   * 返回一个 promise,其结果由第一个完成的 promise 决定
   */
  static race(promises) {
    return new Promise((resolve, reject) => {
      promises.forEach((promise) => {
        promise = Promise.resolve(promise);
        promise.then(resolve, reject);
      });
    });
  }
}

遇到的问题

  1. this 的指向

    在 executor 中,因为设定了 setTimeout,因而 resolve 调用时的 this 指向为当前运行环境的 this(window),因此通过箭头函数,将 this 设置为函数创建时的 this 即 promise 实例可以解决 resolve 调用时 this 指向问题。

    在 then 方法中,handle 实际调用时 this 同样为 window,通过箭头函数解决 this 指向问题。

  2. 微任务和宏任务

    在 es6 中 then 中的函数会放入微任务中,而因为开发者无法向微任务队列中添加任务,所以用宏任务替代,即用 setTimeout 实现异步执行,详细问题见 JavaScript 中的异步编程

  3. then 方法如何书写?

    • 返回一个 Promise 实例,其中存储了 then 中代码运行状态的信息;
    • 运行状态信息受到 onResolved 和 onRejected 的执行结果影响;
    • then 同步执行,then 中的函数异步执行且受调用 then 的 promise 实例状态的控制;
    • 异常穿透;

面试题

面试题 1

setTimeout(() => {
  console.log(1);
}, 0);
Promise.resolve().then(() => {
  console.log(2);
});
Promise.resolve().then(() => {
  console.log(4);
});
console.log(3);

// 3 2 4 1

面试题 2

setTimeout(() => {
  console.log(1);
}, 0);
new Promise((resolve) => {
  console.log(2);
  resolve();
})
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(4);
  });
console.log(5);

// 2 5 3 4 1

面试题 3

const first = () =>
  new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
      console.log(7);
      setTimeout(() => {
        console.log(5);
        resolve(6);
      }, 0);
      resolve(1);
    });
    resolve(2);
    p.then((arg) => {
      console.log(arg);
    });
  });

first().then((arg) => {
  console.log(arg);
});
console.log(4);

// 3 7 4 1 2 5

注意:promise 状态一但变化就固定下来了。

面试题 4

setTimeout(() => {
  console.log('0');
}, 0);
new Promise((resolve, reject) => {
  console.log('1');
  resolve();
})
  .then(() => {
    console.log('2');
    new Promise((resolve, reject) => {
      console.log('3');
      resolve();
    })
      .then(() => {
        console.log('4');
      })
      .then(() => {
        console.log('5');
      });
  })
  .then(() => {
    console.log('6');
  });

new Promise((resolve, reject) => {
  console.log('7');
  resolve();
}).then(() => {
  console.log('8');
});

// 1 7 2 3 8 4 6 5 0

注意:then 中函数是否放入微任务队列取决于它前面的 Promise 的状态改变。