Promise 规范及应用

240 阅读22分钟

Promise 规范及应用

主讲:路白 && 云隐

课程目标

  • 进程和线程
  • Promise A+ 规范
  • 实现 Promise
  • 常见问题
// 什么是异步???

// 异步执行
let count = 1;
let timer = setTimeout(() => {
  count++;
  console.log('in', count);
}, 1000);
console.log('out', count);

// 循环执行 + 终止
let count = 1;
let timer = setInterval(() => {
  count++;
  console.log('in', count);
}, 1000);
console.log('out', count);

setTimeout(() => {
  clearInterval(timer);
  console.log('in', count);
}, 5000);

// 看不见的队列,存放着他需要默默执行的命令

进程和线程

概念与区别

进程是 CPU 资源分配的最小单位,线程是 CPU 调度的最小单位。

概念

  • 进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位;
  • 线程:是进程的一个执行单元,是进程内科调度实体,比进程更小的独立运行的基本单位,线程也被称为轻量级进程;
  • 协程:是一种比线程更加轻量级的存在,一个线程也可以拥有多个协程,其执行过程更类似于子例程,或者说不带返回值的函数调用;

进程和线程的区别

  • 地址空间:线程共享本进程的地址空间,而进程之间是独立的地址空间;
  • 资源:线程共享本进程的资源如内存、I/OCPU 等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护;
  • 健壮性:多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉;
  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大,但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小;
  • 可并发性:两者均可并发执行;

面试题

  • 映射到前端:浏览器,例如:Chrome 浏览器新开一个窗口,tab 页是进程还是线程?
    • 是进程(回答问题的时候要从区别概念上出发)
  • 发散点:
    • 方向一:窗口(进程间)通信?浏览器的存储 - storage(localStorage)cookie => 多种存储的区别
    • 方向二:浏览器原理(中高级岗位居多)- 参考图
      • 进程只是起到调度、实施、整理的作用;
      • 实际执行的时候是通过线程的;

原理图:www.processon.com/diagraming/…

进程和线程原理.png

EVENT-LOOP

执行栈

  • JS 单线程语言,单步执行
function run() {
  func1();
}
function func1() {
  func2();
}
function func2() {
  throw new Error('plz check ur call stack');
}
run();

面试题

  • JS 堆栈的执行顺序与堆栈溢出 => 引申到性能优化
// 爆栈 Uncaught RangeError: Maximum call stack size exceeded
function func() {
  func();
}
func();
  • 执行顺序题
setTimeout(() => {
  // 异步 macro
  console.log('Time out');
});

Promise.resolve(1).then(() => {
  // 异步 micro
  console.log('promise');
});

console.log('hi'); // 同步 macro

// hi promise Time out
// 追问:为什么?怎么思考的

Promise A+ 规范

讲解 PromiseA+ 规范前,咱们先来了解一下这些术语,以便在后续提到的时候有明确且统一的概念。

术语

  1. promise 是一个有 then 方法的对象或者是函数,行为遵循本规范;
  2. new Promise 执行器 executor(),执行器参数是? - resolvereject
  3. thenable 是一个有 then 方法的对象或者是函数;
  4. valuepromise 状态成功时的值,也就是 resolve 的参数,包括各种数据类型,也包括 undefined/thenable/promise
  5. reasonpromise 状态失败时的值,也就是 reject 的参数,表示拒绝的原因;
  6. exception 是一个使用 throw 抛出的异常值;

规范

接下来分几部分来讲解 PromiseA+ 规范。

Promise States

promise 应该有三种状态,要注意他们之间的流转关系:

1、pending

  1. 初始的状态,可改变;
  2. 一个 promiseresolve 或者 reject 前都处于这个状态;
  3. 可以通过 resolve -> fulfilled 状态;
  4. 可以通过 reject -> rejected 状态;

2、fulfilled

  1. 最终态,不可变;
  2. 一个 promiseresolve 之后会变成这个状态;
  3. 必须拥有一个 value 值;如果 resolve() 不传值,那就是 undefined

3、rejected

  1. 最终态,不可变;
  2. 一个 promisereject 之后会变成这个状态;
  3. 必须拥有一个 reason

Tips:总结一下,就是 promise 的状态流转是这样的:

pending -> resolve(value) -> fulfilled

pending -> reject(reason) -> rejected

then

promise 应该提供一个 then 方法,用来访问最终的结果,无论是 value 还是 reason

const promise = new Promise();

promise.then(onFulfilled, onRejected);

1、参数要求

  1. onFulfilled 必须是函数类型,如果不是函数,应该被 忽略
  2. onRejected 必须是函数类型,如果不是函数,应该被 忽略

2、onFulfilled 特性

  1. promise 变成 fulfilled 时,应该调用 onFulfilled,参数是 value
  2. promise 变成 fulfilled 之前,不应该被调用;
  3. 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数);

3、onRejected 特性

  1. promise 变成 rejected 时,应该调用 onRejected,参数是 reason
  2. promise 变成 rejected 之前,不应该被调用;
  3. 只能被调用一次(所以在实现的时候需要一个变量来限制执行次数);

4、onFulfilled 和 onRejected 应该是微任务

这里用 queueMicrotask() 来实现微任务的调用。

5、then 方法可以被调用多次

const promise = new Promise();

promise.then(cb1, cb2);
promise.then(cb1, cb2);
promise.then(cb1, cb2);
promise.then(cb1, cb2);
  1. promise 状态变成 fulfilled 后,所有的 onFulfilled 回调都需要按照 then 的顺序执行,也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个 onFulfilled 的回调);
  2. promise 状态变成 rejected 后,所有的 onRejected 回调都需要按照 then 的顺序执行,也就是按照注册顺序执行(所以在实现的时候需要一个数组来存放多个 onRejected 的回调);

6、返回值

then 应该返回一个 promise,一个新的 promise

const promise1 = new Promise();

const promise2 = promise1.then(onFulfilled, onRejected);
  1. onFulfilledonRejected 执行的结果为 x,调用 resolvePromise(这里大家可能难以理解,可以先保留疑问,下面详细讲一下 resolvePromise 是什么东西);
  2. 如果 onFulfilled 或者 onRejected 执行时抛出异常 epromise2 需要被 reject
  3. 如果 onFulfilled 不是一个函数,promise2promise1value 触发 fulfilled
  4. 如果 onRejected 不是一个函数,promise2promise1reason 触发 rejected

7、resolvePromise

resolvePromise(promise2, x, resolve, reject);
  1. 如果 promise2x 相等(promise2 === x),那么抛出异常 reject TypeError

  2. 如果 x 是一个 promise

    • 如果 xpending 状态,那么 promise 必须要在 pending 状态等待,直到 x 变成 fulfilled 或者 rejected
    • 如果 xfulfilledfulfill promise with the same value
    • 如果 xrejectedreject promise with the same reason
  3. 如果 x 是一个 object 或者是一个 function

    let then = x.then;
    
    • 如果 x.then 这步出错,那么 reject promise with the same reason
    • 如果 then 是一个函数,then.call(x, resolvePromise, rejectPromise) => x.then()
      • resolvePromise 的入参是 y,执行 resolvePromise(promise2, y, resolve, reject);
      • rejectPromise 的入参是 rreject promise with r
      • 如果 resolvePromiserejectPromise 都调用了,那么第一个调用优先,后面的调用忽略;
      • 如果调用 then 抛出异常 e
        • 如果 resolvePromiserejectPromise 已经被调用,那么忽略
        • 则,reject promise with e as the reason
    • 如果 then 不是一个 functionfulfill promise with x

这段描述看起来非常的空洞乏味,最重要的是看不懂! 所以待会实现代码的时候,同学们注意一下 resolvePromise 函数具体的实现,结合代码来看会好很多。

一步步实现一个 Promise

平常用 promise 的时候,是通过 new 关键字来 new Promise(),所以咱们应该用构造函数或者 class 来实现。都 2021 年了,咱们就用 class 来实现一下吧。

1、初始化 class

class MyPromise {
  constructor() {}
}

2、定义三种状态类型

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

3、设置初始状态和值

class MyPromise {
  constructor() {
    // 设置初始状态和值
    // 初始状态为 pending
    this.status = PENDING;
    this.value = null;
    this.reason = null;
  }
}

4、resolve 和 reject 方法

  • 根据刚才的规范,这两个方法是要更改 status 的,从 pending 改到 fulfilled/rejected
  • 注意:两个函数的入参分别是 valuereason
class MyPromise {
  constructor() {
    // 设置初始状态和值
    // 初始状态为 pending
    this.status = PENDING;
    this.value = null;
    this.reason = null;
  }

  resolve(value) {
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
    }
  }

  reject(reason) {
    if (this.status === PENDING) {
      this.reason = reason;
      this.status = REJECTED;
    }
  }
}

5、promise 构造函数的入参

是不是发现咱们的 promise 少了入参,咱们来加一下;

  • 入参是一个函数,函数接收 resolvereject 两个参数;
  • 注意:在初始化 promise 的时候(new Promise()),就要执行这个函数,并且有任何报错都要通过 reject 抛出去;
class MyPromise {
  constructor(executor) {
    // 设置初始状态和值
    // 初始状态为 pending
    this.status = PENDING;
    this.value = null;
    this.reason = null;

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

  resolve(value) {
    if (this.status === PENDING) {
      this.value = value;
      this.status = FULFILLED;
    }
  }

  reject(reason) {
    if (this.status === PENDING) {
      this.reason = reason;
      this.status = REJECTED;
    }
  }
}

6、then 方法

  • then 接收两个参数,onFulfilledonRejected
then(onFulfilled, onRejected) {}
  • 检查并处理参数,之前提到的如果不是 function,就 忽略,这个忽略指的是原样返回 value 或者 reason(也就是“透传”);
then(onFulfilled, onRejected) {
  const realOnFulfilled = this.isFunction(onFulfilled)
    ? onFulfilled
    : value => {
      return value;
    };

  const realOnRejected = this.isFunction(onRejected)
    ? onRejected
    : reason => {
      throw reason;
    };
}

isFunction(param) {
  return typeof param === 'function';
}
  • 要知道 .then 的返回值整体是一个 promise,所以咱们先用 promise 来包裹一下,其他逻辑待会再实现;
then(onFulfilled, onRejected) {
  const realOnFulfilled = this.isFunction(onFulfilled)
    ? onFulfilled
    : value => {
      return value;
    };

  const realOnRejected = this.isFunction(onRejected)
    ? onRejected
    : reason => {
      throw reason;
    };

  const promise2 = new MyPromise((resolve, reject) => {});

  return promise2;
}
  • 根据当前 promise 的状态,调用不同的函数;
then(onFulfilled, onRejected) {
  const realOnFulfilled = this.isFunction(onFulfilled)
    ? onFulfilled
    : value => {
      return value;
    };

  const realOnRejected = this.isFunction(onRejected)
    ? onRejected
    : reason => {
      throw reason;
    };

  const promise2 = new MyPromise((resolve, reject) => {
    switch (this.status) {
      case FULFILLED: {
        realOnFulfilled(this.value);
        break;
      }

      case REJECTED: {
        realOnRejected(this.reason);
        break;
      }
    }
  });

  return promise2;
}
  • 此时到这里,所有的代码执行都是同步的,这个时候有的同学要问了,你这样写,是在 then 函数被调用的瞬间就会执行,那这时候如果 status 还没变成 fulfilled 或者 rejected 怎么办,很有可能还是 pending 的。所以我们需要一个状态的监听机制,当状态变成 fulfilled 或者 rejected 后,再去执行 callback

    • 那么我们首先要拿到所有的 callback,然后才能在某个时机去执行他,新建两个数组,来分别存储成功和失败的回调,调用 then 的时候,如果还是 pending 就存入数组;
    class MyPromise {
      FULFILLED_CALLBACK_LIST = [];
      REJECTED_CALLBACK_LIST = [];
    
      constructor(executor) {
        // ...
      }
    
      // ...
    
      then(onFulfilled, onRejected) {
        const realOnFulfilled = this.isFunction(onFulfilled)
          ? onFulfilled
          : value => {
              return value;
            };
    
        const realOnRejected = this.isFunction(onRejected)
          ? onRejected
          : reason => {
              throw reason;
            };
    
        const promise2 = new MyPromise((resolve, reject) => {
          switch (this.status) {
            case FULFILLED: {
              realOnFulfilled(this.value);
              break;
            }
    
            case REJECTED: {
              realOnRejected(this.reason);
              break;
            }
    
            case PENDING: {
              this.FULFILLED_CALLBACK_LIST.push(realOnFulfilled);
              this.REJECTED_CALLBACK_LIST.push(realOnRejected);
            }
          }
        });
    
        return promise2;
      }
    }
    
    • status 发生变化的时候,就执行所有的回调,这里咱们用一下 ES6gettersetter,这样更符合语义,当 status 改变时,去做什么事情(当然也可以顺序执行,在给 status 赋值后,下面再加一行 forEach);
    _status = PENDING;
    
    get status() {
      return this._status;
    }
    set status(newStatus) {
      this._status = newStatus;
    
      switch (newStatus) {
        case FULFILLED: {
          this.FULFILLED_CALLBACK_LIST.forEach(callback => callback(this.value));
          break;
        }
    
        case REJECTED: {
          this.REJECTED_CALLBACK_LIST.forEach(callback => callback(this.reason));
          break;
        }
      }
    }
    

7、then 的返回值

上面只是简单说了下 then 的返回值是一个 Promise,那么接下来具体讲一下返回 promisevaluereason是什么。

  • 如果 onFulfilled 或者 onRejected 抛出一个异常 e,则 promise2 必须拒绝执行,并返回拒因 e(这样的话,我们就需要手动 catch 代码,遇到报错就 reject);
then(onFulfilled, onRejected) {
  const realOnFulfilled = this.isFunction(onFulfilled)
    ? onFulfilled
    : value => {
      return value;
    };

  const realOnRejected = this.isFunction(onRejected)
    ? onRejected
    : reason => {
      throw reason;
    };

  const promise2 = new MyPromise((resolve, reject) => {
    const fulfilledMicrotask = () => {
      try {
        realOnFulfilled(this.value);
      } catch (error) {
        reject(error);
      }
    };

    const rejectedMicrotask = () => {
      try {
        realOnRejected(this.reason);
      } catch (error) {
        reject(error);
      }
    };

    switch (this.status) {
      case FULFILLED: {
        fulfilledMicrotask();
        break;
      }

      case REJECTED: {
        rejectedMicrotask();
        break;
      }

      case PENDING: {
        this.FULFILLED_CALLBACK_LIST.push(fulfilledMicrotask);
        this.REJECTED_CALLBACK_LIST.push(rejectedMicrotask);
      }
    }
  });

  return promise2;
}
  • 如果 onFulfilled 不是函数且 promise1 成功执行,promise2 必须成功执行并返回相同的值;
  • 如果 onRejected 不是函数且 promise1 拒绝执行,promise2 必须拒绝执行并返回相同的据因;

需要注意的是,如果 promise1onRejected 执行成功了,promise2 应该被 resolve,这里咱们其实已经在参数检查的时候做过了,也就是这段代码:

const realOnFulfilled = this.isFunction(onFulfilled)
  ? onFulfilled
  : value => {
      return value;
    };

const realOnRejected = this.isFunction(onRejected)
  ? onRejected
  : reason => {
      throw reason;
    };
  • 如果 onFulfilled 或者 onRejected 返回一个值 x,则运行 resolvePromise 方法;
then(onFulfilled, onRejected) {
  const realOnFulfilled = this.isFunction(onFulfilled)
    ? onFulfilled
    : value => {
        return value;
      };

  const realOnRejected = this.isFunction(onRejected)
    ? onRejected
    : reason => {
        throw reason;
      };

  const promise2 = new MyPromise((resolve, reject) => {
    const fulfilledMicrotask = () => {
      try {
        const x = realOnFulfilled(this.value);
        this.resolvePromise(promise2, x, resolve, reject);
      } catch (error) {
        reject(error);
      }
    };

    const rejectedMicrotask = () => {
      try {
        const x = realOnRejected(this.reason);
        this.resolvePromise(promise2, x, resolve, reject);
      } catch (error) {
        reject(error);
      }
    };

    switch (this.status) {
      case FULFILLED: {
        fulfilledMicrotask();
        break;
      }

      case REJECTED: {
        rejectedMicrotask();
        break;
      }

      case PENDING: {
        this.FULFILLED_CALLBACK_LIST.push(fulfilledMicrotask);
        this.REJECTED_CALLBACK_LIST.push(rejectedMicrotask);
      }
    }
  });

  return promise2;
}

8、resolvePromise

resolvePromise(promise2, x, resolve, reject) {
  // 如果 newPromise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 newPromise
  // 这是为了防止死循环
  if (promise2 === x) {
    return reject(new TypeError('The promise and the return value are the same'));
  }

  if (x instanceof MyPromise) {
    // 如果 x 为 Promise ,则使 newPromise 接受 x 的状态
    // 也就是继续执行 x,如果执行的时候拿到一个 y,还要继续解析 y
    queueMicrotask(() => {
      x.then(y => {
        this.resolvePromise(promise2, y, resolve, reject);
      }, reject);
    });
  } else if (typeof x === 'object' || this.isFunction(x)) {
    // 如果 x 为对象或者函数
    if (x === null) {
      // null 也会被判断为对象
      return resolve(x);
    }

    let then = null;
    try {
      // 把 x.then 赋值给 then
      then = x.then;
    } catch (error) {
      // 如果取 x.then 的值时抛出错误 e,则以 e 为据因拒绝 promise
      return reject(error);
    }

    // 如果 then 是函数
    if (this.isFunction(then)) {
      let called = false;
      // 将 x 作为函数的作用域 this 调用
      // 传递两个回调函数作为参数,第一个参数叫做 resolvePromise,第二个参数叫做 rejectPromise
      try {
        then.call(
          x,
          // 如果 resolvePromise 以值 y 为参数被调用,则运行 resolvePromise
          y => {
            // 需要有一个变量 called 来保证只调用一次
            if (called) return;
            called = true;
            this.resolvePromise(promise2, y, resolve, reject);
          },
          // 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } catch (error) {
        // 如果调用 then 方法抛出了异常 e
        if (called) return;

        // 否则以 e 为据因拒绝 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函数,以 x 为参数执行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不为对象或者函数,以 x 为参数执行 promise
    resolve(x);
  }
}

9、onFulfilled 和 onRejected 是微任务

咱们可以用 queueMicrotask 包裹执行函数;

const fulfilledMicrotask = () => {
  // 使用 queueMicrotask 微任务包裹一下
  queueMicrotask(() => {
    try {
      const x = realOnFulfilled(this.value);
      this.resolvePromise(promise2, x, resolve, reject);
    } catch (error) {
      reject(error);
    }
  });
};

const rejectedMicrotask = () => {
  // 使用 queueMicrotask 微任务包裹一下
  queueMicrotask(() => {
    try {
      const x = realOnRejected(this.reason);
      this.resolvePromise(promise2, x, resolve, reject);
    } catch (error) {
      reject(error);
    }
  });
};

10、简单写点代码测试一下

const test = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(111);
  }, 1000);
}).then(console.log);

console.log(test);

setTimeout(() => {
  console.log(test);
}, 2000);

/*
MyPromise {
  FULFILLED_CALLBACK_LIST: [],
  REJECTED_CALLBACK_LIST: [],
  _status: 'pending',
  value: null,
  reason: null
}
111
MyPromise {
  FULFILLED_CALLBACK_LIST: [],
  REJECTED_CALLBACK_LIST: [],
  _status: 'fulfilled',
  value: undefined,
  reason: null
}
*/

11、catch 方法

catch(onRejected) {
  return this.then(null, onRejected);
}

12、promise.resolve

将现有对象转为 Promise 对象,如果 Promise.resolve 方法的参数不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为 fulfilled

注意:这是一个静态方法,因为咱们是通过 Promise.resolve 调用的,而不是通过实例去调用的。

static resolve(value) {
  if (value instanceof MyPromise) {
    return value;
  }

  return new MyPromise(resolve => {
    resolve(value);
  });
}

13、promise.reject

返回一个新的 Promise 实例,该实例的状态为 rejectedPromise.reject 方法的参数 reason,会被传递给实例的回调函数。

static reject(reason) {
  return new MyPromise((resolve, reject) => {
    reject(reason);
  });
}

14、promise.race

const p = Promise.race([p1, p2, p3]);

该方法是将多个 Promise 实例,包装成一个新的 Promise 实例。

只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变,那个率先改变的 Promise 实例的返回值就传递给 p 的回调函数。

static race(promiseList) {
  return new MyPromise((resolve, reject) => {
    const length = promiseList.length;

    if (length === 0) {
      return resolve();
    } else {
      for (let i = 0; i < length; i++) {
        MyPromise.resolve(promiseList[i]).then(
          value => {
            return resolve(value);
          },
          reason => {
            return reject(reason);
          }
        );
      }
    }
  });
}

15、写段测试代码

const test = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(111);
  }, 3000);
});

const test2 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(222);
  }, 2000);
});

const test3 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(333);
  }, 1000);
});

MyPromise.race([test, test2, test3]).then(console.log);

/* 
  333
*/

16、promise.all

static all(promiseList) {
  return new MyPromise((resolve, reject) => {
    let res = [];
    let count = 0;
    const length = promiseList.length;

    for (let i = 0; i < length; i++) {
      MyPromise.resolve(promiseList[i])
        .then(value => {
        res[i] = value;
        count++;

        if (count === length) {
          resolve(res);
        }
      })
        .catch(reason => {
        reject(reason);
      });
    }
  });
}

17、finally 方法

finally 方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是 fulfilled 还是 rejected。这表明,finally 方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

finally(callback) {
  return this.then(
    value => {
      return MyPromise.resolve(callback()).then(() => value);
    },
    err => {
      return MyPromise.resolve(callback()).then(() => {
        throw err;
      });
    }
  );
}

几个问题

为什么 promise resolve 了一个 value,最后输出的 value 值确是 undefined

const test = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(111);
  }, 1000);
}).then(value => {
  console.log('then');
  // 同这样
  // return undefined;
});

setTimeout(() => {
  console.log(test);
}, 3000);

/* 
  then
  MyPromise {
    FULFILLED_CALLBACK_LIST: [],
    REJECTED_CALLBACK_LIST: [],
    _status: 'fulfilled',
    value: undefined,
    reason: null
  }
*/

答案:

因为现在这种写法,相当于在 .thenreturn undefined,所以最后的 valueundefined

如果显式 return 一个值,就不是 undefined 了;比如 return value

.then 返回的是一个新 Promise,那么原来 promise 实现的时候,用数组来存回调函数有什么意义?

这个问题提出的时候,应该是有一个假定条件,就是链式调用的时候;

这个时候,每一个 .then 返回的都是一个新 promise,所以每次回调数组 FULFILLED_CALLBACK_LIST 都是空数组;

针对这种情况,确实用数组来存储回调没意义,完全可以就用一个变量来存储;

const test = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(111);
  }, 1000);
})
  .then(value => {})
  .then(() => {});

但是还有一种 promise 使用的方式,这种情况下,promise 实例是同一个,数组的存在就有了意义;

const test = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(111);
  }, 1000);
});

test.then(() => {});
test.then(() => {});
test.then(() => {});
test.then(() => {});

为什么我在 catch 的回调里,打印 promise,显示状态是 pending

const test = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject(111);
  }, 1000);
}).catch(reason => {
  console.log(`[报错] reason = ${reason}`);
  console.log(test);
});

setTimeout(() => {
  console.log(test);
}, 3000);

/* 
  [报错] reason = 111
  MyPromise {
    FULFILLED_CALLBACK_LIST: [],
    REJECTED_CALLBACK_LIST: [],
    _status: 'pending',
    value: null,
    reason: null
  }
  MyPromise {
    FULFILLED_CALLBACK_LIST: [],
    REJECTED_CALLBACK_LIST: [],
    _status: 'fulfilled',
    value: undefined,
    reason: null
  }
*/
  1. catch 函数会返回一个新的 promise,而 test 就是这个新 promise
  2. catch 的回调里,打印 promise 的时候,整个回调还并没有执行完成(所以此时的状态是 pending),只有当整个回调完成了,才会更改状态;
  3. catch 的回调函数,如果成功执行完成了,会改变这个新 Promise 的状态为 fulfilled

Generator 和 Async 简介

在讲 Generator 之前,咱们要来先了解另外一个概念,迭代器。

迭代器 Iterator

迭代器 IteratorES6 引入的一种新的遍历机制,同时也是一种特殊对象,它具有一些专门为迭代过程设计的专有接口。

每个迭代器对象都有一个 next() 方法,每次调用都返回一个当前结果对象。当前结果对象中有两个属性:

  1. value:当前属性的值;

  2. done:用于判断是否遍历结束,当没有更多可返回的数据时,返回 true

每调用一次 next() 方法,都会返回下一个可用的值,直到遍历结束。

生成器 Generator

生成器是一种返回迭代器的函数,通过 function 关键字后的星号(*)来表示,函数中会用到新的关键字 yield。星号可以紧挨着 function 关键字,也可以在中间添加一个空格。

function longTimeFn(time) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(time);
    }, time);
  });
}

function* generator() {
  const list = [1, 2, 3];
  for (let i of list) {
    yield i;
  }
}

let g = generator();

console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
console.log(g.next()); // { value: undefined, done: true }

特性

  1. 每当执行完一条 yield 语句后函数就会自动停止执行,直到再次调用next()

  2. yield 关键字只可在生成器内部使用,在其他地方使用会导致程序抛出错误;

  3. 可以通过函数表达式来创建生成器,但是不能使用箭头函数;

    let generator = function *(){}

Async 和 Await

这个就比较简单了,非常常用,就不多赘述了。

但是同学们想不想知道怎么封装一个函数,能够让 generator 自动执行到完毕?

function longTimeFn(time) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(time);
    }, time);
  });
}

function asyncFunc(generator) {
  const iterator = generator();
  // 接下来要执行 next
  // data 为第一次执行之后的返回结果,用于传给第二次执行
  const next = data => {
    // 第二次执行,并接收第一次的请求结果 value 和 done
    const { value, done } = iterator.next(data);

    // 执行完毕, 直接返回
    if (done) return;
    // 第一次执行 next 时,yield 返回的 promise 实例赋值给了 value
    value.then(data => {
      // 当第一次 value 执行完毕且成功时,执行下一步(并把第一次的结果传递下一步)
      next(data);
    });
  };
  next();
}

asyncFunc(function* () {
  let data = yield longTimeFn(1000);
  console.log(data);
  data = yield longTimeFn(2000);
  console.log(data);
  return data;
});

补充知识点

Promise 的四个静态方法的使用和区别

先提出几个问题

  • Promise.allPromise.allSettled 区别?
  • Promise.racePromise.any 的运行机制?两者有什么区别?
  • 四兄弟只能接受 数组 作为参数吗?
  • 四兄弟方法我们应该如何优雅完美的实现?

Promise.all

const p = Promise.all([p1, p2, p3]);

Promise.all 方法接受一个数组做参数,p1、p2、p3 都是 Promise 实例。如果不是 Promise 实例,则会先调用 Promise.resolve 方法将参数先转化为 Promise 实例,之后进行下一步处理。

返回值 p 的状态由 p1、p2、p3 决定,可以分成两种情况:

  • 只有 p1、p2、p3 的状态都变成 fulfilledp 的状态才会变成 fulfilled ,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数;
  • 只要 p1、p2、p3 之中有一个被 rejectedp 的状态就变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数;

总结:

  • p 状态由参数执行结果决定,全部成功则返回成功,存有一个失败则失败;
  • 参数为非 Promise 实例,会通过 Promise.resolve 转化成 Promise 实例;
  • 成功后返回一个数组,数组内数据按照参数顺序排列;
  • 短路效应:只会返回第一个失败信息;

Promise.allSettled

Promise.allSettled() 方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象。只有等到参数数组的所有 Promise 对象都发生状态变更(不管是 fulfilled 还是 rejected),返回的 Promise 对象才会发生状态变更。

总结:

  • allSettled 方法只会成功,不会失败;
  • 返回结果每个成员为对象,对象的格式固定:
    • 如果 promise 成功,对象属性值 status: fulfilledvalue 记录成功值;
    • 如果 promise 失败,对象属性值 status: rejectedreason 记录失败原因;
  • allSettled 方法也可以接受 Iterator 类型参数;

allSettled 方法与 all 方法最大的区别在于两点:

  • allSettled 方法没有失败情况;
  • allSettled 方法返回有固定格式;

Promise.race

Promise.race() 方法同样是接收多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面案例中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

Promise.any

ES2021 引入了 Promise.any() 方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。

any 方法与 race 方法很像,也存在短路特性,只要有一个实例变成 fulfilled 状态,就会返回成功的结果;如果全部失败,则返回失败情况。

总结:

  • any 方法也可以接受 Iterator 格式参数;
  • 当一个 promise 实例转变为 fulfilled 时,any 返回成功的 promise ,值为最早成功的 promise值;
  • promise 全部失败时,any 返回失败的 promise ,值固定为 AggregateError: All promises were rejected

窗口(进程间)通信

四种实现浏览器标签页数据通信方式:

  • cookie
  • localStorage
  • webworker
  • websocket

cookie

由于更新 cookie 并不能触发任何事件,因此我们需要通过定时器 setInterval 来主动监听 cookie 中的值是否改变。

存在的问题:定时器存在时间差,点击后有一定的延迟,cookie 本身的弊端;

localStorage

localStorage 也是浏览器多个页面共用的存储空间;而且 localStorage 在一个页面中添加、修改或者删除时,都会在 非当前页面中 被动触发一个 storage 事件,我们通过在其他页面中监听 storage 事件,即可拿到 storage 更新前后的值:

window.addEventListener('storage', () => {
  // ...
});

实现 Ajax

  • open(method, url, async) 方法需要三个参数:
    • method:发送请求所使用的方法(GETPOST);与 POST 相比,GET 更简单也更快,并且在大部分情况下都能用;然而,在以下情况中,请使用 POST 请求:
      • 无法使用缓存文件(更新服务器上的文件或数据库)
      • 向服务器发送大量数据(POST 没有数据量限制)
      • 发送包含未知字符的用户输入时,POSTGET 更稳定也更可靠
    • url:规定服务器端脚本的 URL
    • async:规定应当对请求进行异步(true)或同步(false)处理;true 是在等待服务器响应时执行其他脚本,当响应就绪后对响应进行处理;false 是等待服务器响应再执行
  • send() 方法可将请求送往服务器;
  • onreadystatechange:存有处理服务器响应的函数,每当 readyState 改变时,onreadystatechange 函数就会被执行;
  • readyState:存有服务器响应的状态信息:
    • 0:请求未初始化(代理被创建,但尚未调用 open 方法)
    • 1:服务器连接已建立(open 方法已经被调用)
    • 2:请求已接收(send方法已经被调用,并且头部和状态已经可获得)
    • 3:请求处理中(下载中,responseText 属性已经包含部分数据)
    • 4:请求已完成,且响应已就绪(下载操作已完成)
  • responseText:获得字符串形式的响应数据;
  • setRequestHeader()POST 传数据时,用来添加 HTTP 头,然后 send(data),注意 data 格式;GET 发送信息时直接加参数到 url 上就可以;
var Ajax = {
  get: function (url, callback) {
    // XMLHttpRequest 对象用于在后台与服务器交换数据
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.onreadystatechange = function () {
      // readyState === 4 说明请求已完成
      if (xhr.readyState === 4) {
        if (xhr.status == 200 || xhr.status === 304) {
          console.log(xhr.responseText);
          callback(xhr.responseText);
        }
      }
    };
    xhr.send();
  },

  post: function (url, data, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', url, false);
    // 添加 http 头,发送信息至服务器时内容编码类型
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200 || xhr.status === 304) {
          console.log(xhr.responseText);
          callback(xhr.responseText);
        }
      }
    };
    xhr.send(data);
  }
};