乐趣区

关于javascript:Promise知识汇总和面试情况

写在后面

Javascript异步编程先后经验了四个阶段,别离是 Callback 阶段,Promise阶段,Generator阶段和 Async/Await 阶段。Callback很快就被发现存在回调天堂和控制权问题,Promise就是在这个工夫呈现,用以解决这些问题,Promise并非一个新事务,而是依照一个标准实现的类,这个标准有很多,如 Promise/APromise/BPromise/D 以及 Promise/A 的升级版 Promise/A+,最终 ES6 中采纳了 Promise/A+ 标准。起初呈现的 Generator 函数以及 Async 函数也是以 Promise 为根底的进一步封装,可见 Promise 在异步编程中的重要性。

对于 Promise 的材料曾经很多,但每个人了解都不一样,不同的思路也会有不一样的播种。这篇文章会着重写一下 Promise 的实现以及笔者在日常应用过程中的一些心得体会。

实现 Promise

标准解读

Promise/A+ 标准次要分为术语、要求和注意事项三个局部,咱们重点看一下第二局部也就是要求局部,以笔者的了解大略阐明一下,具体细节参照完整版 Promise/A+ 规范。

1、Promise有三种状态 pendingfulfilledrejected。(为了一致性,此文章称 fulfilled 状态为 resolved 状态)

  • 状态转换只能是 pendingresolved或者 pendingrejected
  • 状态一旦转换实现,不能再次转换。

2、Promise领有一个 then 办法,用以解决 resolvedrejected状态下的值。

  • then办法接管两个参数 onFulfilledonRejected,这两个参数变量类型是函数,如果不是函数将会被疏忽,并且这两个参数都是可选的。
  • then办法必须返回一个新的 promise,记作promise2,这也就保障了then 办法能够在同一个 promise 上屡次调用。(ps:标准只要求返回promise,并没有明确要求返回一个新的promise,这里为了跟 ES6 实现保持一致,咱们也返回一个新promise
  • onResolved/onRejected有返回值则把返回值定义为x,并执行[[Resolve]](promise2, x);
  • onResolved/onRejected运行出错,则把 promise2 设置为 rejected 状态;
  • onResolved/onRejected不是函数,则须要把 promise1 的状态传递上来。

3、不同的 promise 实现能够的交互。

  • 标准中称这一步操作为 promise 解决过程,函数标示为 [[Resolve]](promise, x),promise 为要返回的新 promise 对象,xonResolved/onRejected 的返回值。如果 xthen办法且看上去像一个 promise,咱们就把 x 当成一个promis e 的对象,即thenable 对象,这种状况下尝试让 promise 接管 x 的状态。如果 x 不是 thenable 对象,就用 x 的值来执行 promise
  • [[Resolve]](promise, x)函数具体运行规定:

    • 如果 promisex 指向同一对象,以 TypeError 为据因拒绝执行 promise;
    • 如果 xPromise,则使 promise 承受 x 的状态;
    • 如果 x 为对象或者函数,取 x.then 的值,如果取值时呈现谬误,则让 promise 进入 rejected 状态,如果 then 不是函数,阐明 x 不是 thenable 对象,间接以 x 的值 resolve,如果then 存在并且为函数,则把 x 作为 then 函数的作用域 this 调用,then办法接管两个参数,resolvePromiserejectPromise,如果resolvePromise 被执行,则以 resolvePromise 的参数 value 作为 x 持续调用 [[Resolve]](promise, value),直到x 不是对象或者函数,如果 rejectPromise 被执行则让 promise 进入 rejected 状态;
    • 如果 x 不是对象或者函数,间接就用 x 的值来执行promise

代码实现

标准解读第 1 条,代码实现:

class Promise {
  // 定义 Promise 状态,初始值为 pending
  status = 'pending';
  // 状态转换时携带的值,因为在 then 办法中须要解决 Promise 胜利或失败时的值,所以须要一个全局变量存储这个值
  data = '';

  // Promise 构造函数,传入参数为一个可执行的函数
  constructor(executor) {
    // resolve 函数负责把状态转换为 resolved
    function resolve(value) {
      this.status = 'resolved';
      this.data = value;
    }
    // reject 函数负责把状态转换为 rejected
    function reject(reason) {
      this.status = 'rejected';
      this.data = reason;
    }

    // 间接执行 executor 函数,参数为处理函数 resolve, reject。因为 executor 执行过程有可能会出错,谬误状况须要执行 reject
    try {executor(resolve, reject);
    } catch(e) {reject(e)
    }
  }
}

标准解读第 2 条,代码实现:

  /**
    * 领有一个 then 办法
    * then 办法提供:状态为 resolved 时的回调函数 onResolved,状态为 rejected 时的回调函数 onRejected
    * 返回一个新的 Promise
  */
  then(onResolved, onRejected) {
    // 设置 then 的默认参数,默认参数实现 Promise 的值的穿透
    onResolved = typeof onResolved === 'function' ? onResolved : function(v) {return e};
    onRejected = typeof onRejected === 'function' ? onRejected : function(e) {throw e};
    
    let promise2;
    
    promise2 =  new Promise((resolve, reject) => {
      // 如果状态为 resolved,则执行 onResolved
      if (this.status === 'resolved') {
        try {
          // onResolved/onRejected 有返回值则把返回值定义为 x
          const x = onResolved(this.data);
          // 执行[[Resolve]](promise2, x)
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {reject(e);
        }
      }
      // 如果状态为 rejected,则执行 onRejected
      if (this.status === 'rejected') {
        try {const x = onRejected(this.data);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {reject(e);
        }
      }
    });
    
    return promise2;
  }

当初咱们就依照标准解读第 2 条,实现了上述代码,上述代码很显著是有问题的,问题如下

  1. resolvePromise未定义;
  2. then办法执行的时候,promise可能依然处于 pending 状态,因为 executor 中可能存在异步操作(理论状况大部分为异步操作),这样就导致 onResolved/onRejected 失去了执行机会;
  3. onResolved/onRejected这两相函数须要异步调用 (官网Promise 实现的回调函数总是异步调用的)。

解决办法:

  1. 依据标准解读第 3 条,定义并实现 resolvePromise 函数;
  2. then办法执行时如果 promise 依然处于 pending 状态,则把处理函数进行贮存,等resolve/rejec t 函数真正执行的的时候再调用。
  3. promise.then属于微工作,这里咱们为了不便,用宏工作 setTiemout 来代替实现异步,具体细节特地举荐这篇文章。

好了,有了解决办法,咱们就把代码进一步欠缺:

class Promise {
  // 定义 Promise 状态变量,初始值为 pending
  status = 'pending';
  // 因为在 then 办法中须要解决 Promise 胜利或失败时的值,所以须要一个全局变量存储这个值
  data = '';
  // Promise resolve 时的回调函数集
  onResolvedCallback = [];
  // Promise reject 时的回调函数集
  onRejectedCallback = [];

  // Promise 构造函数,传入参数为一个可执行的函数
  constructor(executor) {
    // resolve 函数负责把状态转换为 resolved
    function resolve(value) {
      this.status = 'resolved';
      this.data = value;
      for (const func of this.onResolvedCallback) {func(this.data);
      }
    }
    // reject 函数负责把状态转换为 rejected
    function reject(reason) {
      this.status = 'rejected';
      this.data = reason;
      for (const func of this.onRejectedCallback) {func(this.data);
      }
    }

    // 间接执行 executor 函数,参数为处理函数 resolve, reject。因为 executor 执行过程有可能会出错,谬误状况须要执行 reject
    try {executor(resolve, reject);
    } catch(e) {reject(e)
    }
  }
  /**
    * 领有一个 then 办法
    * then 办法提供:状态为 resolved 时的回调函数 onResolved,状态为 rejected 时的回调函数 onRejected
    * 返回一个新的 Promise
  */
  then(onResolved, onRejected) {

    // 设置 then 的默认参数,默认参数实现 Promise 的值的穿透
    onResolved = typeof onResolved === 'function' ? onResolved : function(v) {return e};
    onRejected = typeof onRejected === 'function' ? onRejected : function(e) {throw e};

    let promise2;

    promise2 =  new Promise((resolve, reject) => {
      // 如果状态为 resolved,则执行 onResolved
      if (this.status === 'resolved') {setTimeout(() => {
          try {
            // onResolved/onRejected 有返回值则把返回值定义为 x
            const x = onResolved(this.data);
            // 执行[[Resolve]](promise2, x)
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {reject(e);
          }
        }, 0);
      }
      // 如果状态为 rejected,则执行 onRejected
      if (this.status === 'rejected') {setTimeout(() => {
          try {const x = onRejected(this.data);
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (e) {reject(e);
          }
        }, 0);
      }
      // 如果状态为 pending,则把处理函数进行存储
      if (this.status = 'pending') {this.onResolvedCallback.push(() => {setTimeout(() => {
            try {const x = onResolved(this.data);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {reject(e);
            }
          }, 0);
        });

        this.onRejectedCallback.push(() => {setTimeout(() => {
            try {const x = onRejected(this.data);
              this.resolvePromise(promise2, x, resolve, reject);
            } catch (e) {reject(e);
            }
          }, 0);
        });
      }

    });

    return promise2;
  }

  // [[Resolve]](promise2, x)函数
  resolvePromise(promise2, x, resolve, reject) {}}

至此,标准中对于 then 的局部就全副实现结束了。

标准解读第 3 条,代码实现:

// [[Resolve]](promise2, x)函数
  resolvePromise(promise2, x, resolve, reject) {
    let called = false;

    if (promise2 === x) {return reject(new TypeError('Chaining cycle detected for promise!'))
    }
    
    // 如果 x 依然为 Promise 的状况
    if (x instanceof Promise) {
      // 如果 x 的状态还没有确定,那么它是有可能被一个 thenable 决定最终状态和值,所以须要持续调用 resolvePromise
      if (x.status === 'pending') {x.then(function(value) {resolvePromise(promise2, value, resolve, reject)
        }, reject)
      } else { 
        // 如果 x 状态曾经确定了,间接取它的状态
        x.then(resolve, reject)
      }
      return
    }
  
    if (x !== null && (Object.prototype.toString(x) === '[object Object]' || Object.prototype.toString(x) === '[object Function]')) {
      try {
        // 因为 x.then 有可能是一个 getter,这种状况下屡次读取就有可能产生副作用,所以通过变量 called 进行管制
        const then = x.then 
        // then 是函数,那就阐明 x 是 thenable,继续执行 resolvePromise 函数,直到 x 为一般值
        if (typeof then === 'function') {then.call(x, (y) => {if (called) return;
            called = true;
            this.resolvePromise(promise2, y, resolve, reject);
          }, (r) => {if (called) return;
            called = true;
            reject(r);
          })
        } else { // 如果 then 不是函数,那就阐明 x 不是 thenable,间接 resolve x
          if (called) return ;
          called = true;
          resolve(x);
        }
      } catch (e) {if (called) return;
        called = true;
        reject(e);
      }
    } else {resolve(x);
    }
  }

这一步骤非常简单,只有依照标准转换成代码即可。

最初,残缺的 Promise 依照标准就实现结束了,是的,标准里并没有规定 catchPromise.resolvePromise.rejectPromise.all 等办法,接下来,咱们就看一看 Promise 的这些罕用办法。

Promise 其余办法实现

1、catch 办法

catch办法是对 then 办法的封装,只用于接管 reject(reason) 中的错误信息。因为在 then 办法中 onRejected 参数是可不传的,不传的状况下,错误信息会顺次往后传递,直到有 onRejected 函数接管为止,因而在写 promise 链式调用的时候,then办法不传 onRejected 函数,只须要在最开端加一个 catch() 就能够了,这样在该链条中的 promise 产生的谬误都会被最初的 catch 捕捉到。

  catch(onRejected) {return this.then(null, onRejected);
  }
2、done 办法

catchpromise 链式调用的开端调用,用于捕捉链条中的错误信息,然而 catch 办法外部也可能呈现谬误,所以有些 promise 实现中减少了一个办法 donedone 相当于提供了一个不会出错的 catch 办法,并且不再返回一个 promise,个别用来完结一个promise 链。

  done() {
    this.catch(reason => {console.log('done', reason);
      throw reason;
    });
  }
3、finally 办法

finally办法用于无论是 resolve 还是rejectfinall y 的参数函数都会被执行。

  finally(fn) {
    return this.then(value => {fn();
      return value;
    }, reason => {fn();
      throw reason;
    });
  };
4、Promise.all 办法

Promise.all办法接管一个 promise 数组,返回一个新 promise2,并发执行数组中的全副promise,所有promise 状态都为 resolved 时,promise2状态为 resolved 并返回全副 promise 后果,后果程序和 promise 数组程序统一。如果有一个 promiserejected状态,则整个 promise2 进入 rejected 状态。

  static all(promiseList) {return new Promise((resolve, reject) => {const result = [];
      let i = 0;
      for (const p of promiseList) {
        p.then(value => {result[i] = value;
          if (result.length === promiseList.length) {resolve(result);
          }
        }, reject);
        i++;
      }
    });
  }
5、Promise.race 办法

Promise.race办法接管一个 promise 数组, 返回一个新 promise2,程序执行数组中的promise,有一个promise 状态确定,promise2状态即确定,并且同这个 promise 的状态统一。

  static race(promiseList) {return new Promise((resolve, reject) => {for (const p of promiseList) {p.then((value) => {resolve(value);   
        }, reject);
      }
    });
  }
6、Promise.resolve 办法 /Promise.reject

Promise.resolve用来生成一个 rejected 实现态的 promisePromise.reject 用来生成一个 rejected 失败态的promise

  static resolve(value) {
    let promise;

    promise = new Promise((resolve, reject) => {this.resolvePromise(promise, value, resolve, reject);
    });
  
    return promise;
  }
  
  static reject(reason) {return new Promise((resolve, reject) => {reject(reason);
    });
  }

罕用的办法根本就这些,Promise还有很多扩大办法,这里就不一一展现,基本上都是对 then 办法的进一步封装,只有你的 then 办法没有问题,其余办法就都能够依赖 then 办法实现。

Promise 面试相干

面试相干问题,笔者只说一下我司这几年的状况,并不能代表全副状况,参考即可。
Promise是我司前端开发职位,nodejs开发职位,全栈开发职位,必问的一个知识点,次要问题会散布在 Promise 介绍、根底应用办法以及深层次的了解三个方面,问题个别在 3 - 5 个,依据面试者答复状况会适当增减。

1、简略介绍下 Promise。

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更正当和更弱小。它由社区最早提出和实现,ES6 将其写进了语言规范,对立了用法,原生提供了 Promise 对象。有了 Promise 对象,就能够将异步操作以同步操作的流程表达出来,防止了层层嵌套的回调函数。此外,Promise对象提供对立的接口,使得管制异步操作更加容易。
(当然了也能够简略介绍 promise 状态,有什么办法,callback存在什么问题等等,这个问题是比拟凋谢的)

  • 发问概率:99%
  • 评分标准:人性化判断即可,此问题个别作为引入问题。
  • 加分项:纯熟说出 Promise 具体解决了那些问题,存在什么毛病,利用方向等等。
2、实现一个简略的,反对异步链式调用的 Promise 类。

这个答案不是固定的,能够参考最简实现 Promise,反对异步链式调用

  • 发问概率:50%(手撸代码题,因为这类题目比拟消耗工夫,一场面试并不会呈现很多,所以呈现频率不是很高,但却是必备常识)
  • 加分项:基本功能实现的根底上有 onResolved/onRejected 函数异步调用,谬误捕捉正当等亮点。
3、Promise.then 在 Event Loop 中的执行程序。(能够间接问,也能够出具体题目让面试者答复打印程序)

JS中分为两种工作类型:macrotaskmicrotask,其中macrotask 蕴含:主代码块,setTimeoutsetIntervalsetImmediate等(setImmediate规定:在下一次 Event Loop(宏工作)时触发);microtask 蕴含:Promiseprocess.nextTick等(在 node 环境下,process.nextTick的优先级高于 Promise
Event Loop 中执行一个 macrotask 工作(栈中没有就从事件队列中获取)执行过程中如果遇到 microtask 工作,就将它增加到微工作的工作队列中,macrotask工作执行结束后,立刻执行以后微工作队列中的所有 microtask 工作(顺次执行),而后开始下一个 macrotask 工作(从事件队列中获取)
浏览器运行机制可参考这篇文章

  • 发问概率:75%(能够了解为 4 次面试中 3 次会问到,顺便能够考查面试者对 JS 运行机制的了解)
  • 加分项:扩大讲述浏览器运行机制。
4、论述 Promise 的一些静态方法。

Promise.deferredPromise.allPromise.racePromise.resolvePromise.reject

  • 发问概率:25%(绝对根底的问题,个别在其余问题答复不是很现实的状况下发问,或者为了引出下一个题目而发问)
  • 加分项:越多越好
5、Promise 存在哪些毛病。

1、无奈勾销 Promise,一旦新建它就会立刻执行,无奈中途勾销。
2、如果不设置回调函数,Promise 外部抛出的谬误,不会反馈到内部。
3、吞掉谬误或异样,谬误只能程序解决,即使在 Promise 链最初增加 catch 办法,仍然可能存在无奈捕获的谬误(catch外部可能会呈现谬误)
4、浏览代码不是一眼能够看懂,你只会看到一堆 then,必须本人在then 的回调函数外面理清逻辑。

  • 发问概率:25%(此问题作为进步题目,呈现概率不高)
  • 加分项:越多越正当越好(网上有很多说法,不一一佐证)

(此题目,欢送大家补充答案)

6、应用 Promise 进行程序(sequence)解决。

1、应用 async 函数配合 await 或者应用 generator 函数配合 yield
2、应用promise.then 通过 for 循环或者 Array.prototype.reduce 实现。

function sequenceTasks(tasks) {function recordValue(results, value) {results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {return promise.then(() => task).then(pushValue);
    }, Promise.resolve());
}
  • 发问概率:90%(我司发问概率极高的题目,即能考查面试者对 promise 的了解水平,又能考查编程逻辑,最初还有 bindreduce等办法的使用)
  • 评分标准:说出任意解决办法即可,其中只能说出 async 函数和 generator 函数的能够失去 20% 的分数,能够用 promise.then 配合 for 循环解决的能够失去 60% 的分数,配合 Array.prototype.reduce 实现的能够失去最初的 20% 分数。
7、如何进行一个 Promise 链?

在要进行的 promise 链地位增加一个办法,返回一个永远不执行 resolve 或者 rejectPromise,那么这个 promise 永远处于 pending 状态,所以永远也不会向下执行 thencatch了。这样咱们就进行了一个 promise 链。

    Promise.cancel = Promise.stop = function() {return new Promise(function(){})
    }
  • 发问概率:50%(此问题次要考查面试者罗辑思维)

(此题目,欢送大家补充答案)

8、Promise 链上返回的最初一个 Promise 出错了怎么办?

catchpromise 链式调用的开端调用,用于捕捉链条中的错误信息,然而 catch 办法外部也可能呈现谬误,所以有些 promise 实现中减少了一个办法 donedone 相当于提供了一个不会出错的 catch 办法,并且不再返回一个 promise,个别用来完结一个promise 链。

  done() {
    this.catch(reason => {console.log('done', reason);
      throw reason;
    });
  }
  • 发问概率:90%(同样作为出题率极高的一个题目,充沛考查面试者对 promise 的了解水平)
  • 加分项:给出具体的 done() 办法代码实现
9、Promise 存在哪些应用技巧或者最佳实际?

1、链式 promise 要返回一个 promise,而不只是结构一个promise
2、正当的应用Promise.allPromise.race等办法。
3、在写 promise 链式调用的时候,then办法不传 onRejected 函数,只须要在最开端加一 个 catch()就能够了,这样在该链条中的 promise 产生的谬误都会被最初的 catch 捕捉到。如果 catch() 代码有呈现谬误的可能,须要在链式调用的开端减少 done() 函数。

  • 发问概率:10%(出题概率极低的一个题目)
  • 加分项:越多越好

(此题目,欢送大家补充答案)

至此,我司对于 Promise 的一些面试题目就列举结束了,有些题目的答案是凋谢的,欢送大家一起补充欠缺。总结起来,Promise作为 js 面试必问局部还是绝对容易把握并通过的。

总结

Promise 作为所有 js 开发者的必备技能,其实现思路值得所有人学习,通过这篇文章,心愿小伙伴们在当前编码过程中能更加纯熟、更加明确的应用 Promise。

参考链接:

http://liubin.org/promises-book
https://github.com/xieranmaya/blog/issues/3
https://segmentfault.com/a/1190000016550260

退出移动版