网上说 Promise 的文章一大堆,但是我还是要写一点作为自己的总结,看的东西不要多,主要是看怎么去实现了解其原理过程那就可以了,太复杂的说实话现在还没遇到碰到,就用这篇做一下记录笔记,日后翻看。

了解 Promise 之前先来看一下下面的问题。

什么是宏任务和微任务?

我们都知道 JavaScript 程序是单线程的,由上往下进行,如果碰到一些加载过慢导致延迟堵塞的情况下,就会考虑做成异步操作,比如说使用最多的 setTimeout 函数,为了不在同一时间执行的代码过多导致堵塞,把需要执行的代码延迟执行,已解决效率提升速度。有了异步当然还少不了同步,这两个模式也分别称为同步模式(Synchronous)和异步模式(Asynchronous)

事实上异步的模式下还有两个任务,就是上面问 ”宏任务和微任务“ 。在 ES6 的规范中,宏任务(Macrotask)称为 Task,微任务(Microtask) 称为 Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由 JS 自身发起。

下面的简单比较?

宏任务微任务
setTimeoutrequestAnimationFrame
setIntervalPromise
JavaScript代码块MutationObserver

什么是 EventLoop?

这应该是大部分面试都会被问到的问题了,用图片看就清晰了。看下图;

EventLoop

JavaScript 运行浏览器异步模块时会去检查

是否有宏任务队列,如果有,就执行最早进入宏队列的任务,往下

是否有微任务队列,如果有,就执行最早进入微队列的任务,接着继续 继续检查微任务队列空不空,如果没有,就往下一步

因为首次加载代码块是执行的宏任务,所以在执行完 JavaScript 代码后,立即执行微任务,等所有的微任务走完之后,就是重新渲染。看下面的题目:

console.log('0');

setTimeout(function() {
  console.log('4');
}, 0);

Promise.resolve().then(function() {
  console.log('2');
}).then(function() {
  console.log('3');
});

console.log('1');

结合上面所说的执行想一下是答案

返回结果是 0,1,2,3,4

解析:按上面所说的,首先应该进入 JS 执行的主线程(宏任务),按顺序执行,遇到异步跳过,然后再加载异步的微任务(Promise),最后再去找宏任务的 setTimeout() 执行。

大致就是 代码块(宏)->同步->微任务->刷新->setTimeout(宏)

大致说了 EventLoop 的简单原理,下面就要开始手写 Promise

手写Promise

开始写之前先了解 Promise 原有的几个特点

  1. 创建时需要传递一个立即执行的函数。
  2. 设置传入函数的两个回调(失败 reject 或者成功 resolve )。
  3. Promise 有三个状态,Pending 等待,Fulfilled 完成,Rejected 失败。
  4. ...

暂时做的简单版介绍

新建一个 MyPromise 类

传入一个立即执行的函数,执行函数设置两个回调函数,一个是失败 reject 一个是成功 resolve

class MyPromise {
  constructor(handle) {
    // 设置开始状态
    this.status = 'pending'
    // 绑定一下 this 指向
    handle(this.resolve.bind(this), this.reject.bind(this))
  }
  
  resolve() {}
  
  reject() {}
}

改变状态

优化一下代码,开始或者成功是每个状态是不一样的,所以成功或者失败都伴随一个状态的改变。在这里可以定义三个状态,Pending 等待,Fulfilled 完成,Rejected 失败。

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

class MyPromise {
  constructor(handle) {
    // 设置开始状态
    this.status = PENDING
    // 成功之后返回的信息
    this.value = null
    // 失败之后返回的信息
    this.error = null
    // 绑定一下 this 指向
    handle(this._resolve.bind(this), this._reject.bind(this))
  }
  
  _resolve(value) {
    // 只有 'pending' 的状态时,才执行状态修改
    if(this.status === PENDING) {
      // 修改状态为 成功
      this.status = FULFILLED
      // 再保存成功之后的 值
      this.value = value
    }
  }
  
  _reject(error) {
    // 只有 'pending' 的状态时,才执行状态修改
    if(this.status === PENDING) {
      // 修改状态为 失败
      this.status = REJECTED
      // 再保存失败之后的 值
      this.error = error
    }
  }
}

then 的简单实现

上面的代码可以简单的介绍了 Promise 的初始化阶段和准备,下面还要加上内置的 then 方法使用才算完整。

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

class MyPromise {
  constructor(handle) {
    // 设置开始状态
    this.status = PENDING
    // 成功之后返回的信息
    this.value = null
    // 失败之后返回的信息
    this.error = null
    // 绑定一下 this 指向
    handle(this._resolve.bind(this), this._reject.bind(this))
  }
  
  then(resolve, reject) {
    // 只有完成的状态才能使用 then
    if (this.status === FULFILLED) {
      resolve(this.value)
    } else if (this.status === REJECTED) {
      reject(this.error)
    }
  }
  
  // 私有方法
  _resolve(value) {
    // 只有 'pending' 的状态时,才执行状态修改
    if(this.status === PENDING) {
      // 修改状态为 成功
      this.status = FULFILLED
      // 再保存成功之后的 值
      this.value = value
    }
  }
  
  _reject(error) {
    // 只有 'pending' 的状态时,才执行状态修改
    if(this.status === PENDING) {
      // 修改状态为 失败
      this.status = REJECTED
      // 再保存失败之后的 值
      this.error = error
    }
  }
}

测试代码

完成上述代码之后,浏览器测试一下预期效果:

// 引入 MyPromise.js 
const p = new MyPromise((resolve, reject) => {
  resolve('success')
  reject('reject')
})

p.then((res) => {
  console.log(res)
})

// 执行结果: success

很好,完美的实现了最简单的 Promise

给 Promise 加入异步的逻辑处理

上面的代码中只是实现了简单同步执行回调,并没有接入异步的处理程序,想要异步获取数据用 Promise 包裹起来,然后再使用。比如说下面加入 setTimeout 之后的代码,想一下会是什么样的效果;

// 引入 MyPromise.js 
const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 2000)
})

p.then((res) => {
  console.log(res)
})

// 没有打印信息

为什么会出现没有打印信息的情况?分析一下原因:

当主线程代码进行时,遇到 setTimeout 异步函数,会进入异步队列中,但是 then 会马上执行,这时候异步队列中 resolve() 没有准备完成或者说没有执行完,所以现在的执行状态还是 'pending',因为没有在 resolve 设置 this.status = FULFILLED ,然而 then 函数中执行 pending 的时候没有判断,这时候就导致没有打印的信息出来。

下面就来修改一下之前的代码,把 then 函数中的 pending 也加上。

缓存成功和失败的回调

constructor(handle) {
  // 设置开始状态
  this.status = PENDING
  // 成功之后返回的信息
  this.value = null
  // 失败之后返回的信息
  this.error = null
  
  /** 新增 **/
  // 存储成功回调函数
  this.onResolveCallback = null
  // 存储失败回调函数
  this.onRejectCallback = null
  
  
  // 绑定一下 this 指向
  handle(this._resolve.bind(this), this._reject.bind(this))
}

then 方法中加入 pending 的处理

then(resolve, reject) {
  // 只有完成的状态才能使用 then
  if (this.status === FULFILLED) {
    resolve(this.value)
  } else if (this.status === REJECTED) {
    reject(this.error)
  } else if (this.status === PENDING) {
    // 把 pending 的状态下的成功和失败的回调保存起来
    this.onResolveCallback = resolve
    this.onRejectCallback = reject
  }
}

在 resolve 与 reject 中调用保存的回调

_resolve(value) {
  // 只有 'pending' 的状态时,才执行状态修改
  if(this.status === PENDING) {
    // 修改状态为 成功
    this.status = FULFILLED
    // 再保存成功之后的 值
    this.value = value
    
    // 判断是否有成功的这个函数
    this.onResolveCallback && this.onResolveCallback(value)
  }
}

_reject(error) {
  // 只有 'pending' 的状态时,才执行状态修改
  if(this.status === PENDING) {
    // 修改状态为 失败
    this.status = REJECTED
    // 再保存失败之后的 值
    this.error = error
    
    // 判断是否有失败的这个函数
    this.onRejectCallback && this.onRejectCallback(error)
  }
}

这样一优化补充了代码之后,再去执行之前的代码,就会发现等 2s 之后会打印出 ”success“。

但是就上面的代码实现,还不能满足对原生 Promise 的实现,还有 then 方法的链式调用和 all 方法等等,具体可以看下一篇思路代码。

本文参考以下下文章:
从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节