2024年了还不懂Promise?进来学!不就这么回事?

Promise是什么?

Promise对我来说,是一个当年非常困扰的知识点。在不用Promise的情况下,我好像也能通过传统的回调解决Promise能解决的所有问题,但我去搜索"Promise和普通回调有什么区别"的时候,答案总是告诉我:Promise可以通过链式调用避免陷入"回调地狱"的问题。

但这个问题其实对我的日常开发并没有构成非常大的影响,所以在很长时间中,我都没有动力把Promise语法和使用给学会。

途中也看过很多"带你手写Promise源码"的文章,总是看的一知半解:跟着文章把源码敲完了,仿佛能看懂每一行代码,也确实能运行起来,但就是不懂为什么要这么写。但是真正学会后,不得不说Promise用起来是真爽,语法简单易理解,代码简洁不冗余,代码质量一下上升了不少。

Promise的理解难点 在我看来,是使用思路上有一点绕,使用方式和传统的"回调函数 "有一些不同;涉及到的方法和属性又比较多一点,又是resolvereject啦,又是fulfilledrejected啦,又是.then啦,又是.then中传递的回调函数啦,它们之间还有一环套一环的逻辑关系。

但是别担心!这篇文章看完,包你"不仅学会Promise怎么用,还能学会Promise如何实现 "!全程无痛!面试工作都能用!

Promise怎么用?

我们先来看看,用传统的ajax发送请求是怎么发送的:

javascript 复制代码
// 发送请求
$.ajax({
    url: 'https://api.example.com/create',
    data: {
        name: 'John',
        age: 30
    },
    success: function (data) {
        console.log('请求成功', data);
    },
    error: function (error) {
        console.log('请求失败', error);
    }
});

看起来也很简洁,但如果我们需要在请求成功后再通过请求结果的值发送新的请求,代码就会成为这样:

javascript 复制代码
// 发送请求
$.ajax({
  url: 'https://api.example.com/create',
  data: {
    name: 'John',
    age: 30
  },
  success: function (data) {
    console.log('请求成功', data);
    $.ajax({
      url: 'https://api.example.com/query',
      data: data,
      success: function (data) {
        console.log('请求成功', data);
      },
      error: function (error) {
        console.log('请求失败', error);
      }
    });
  },
  error: function (error) {
    console.log('请求失败', error);
  }
});

此时代码就出现了嵌套,实际的项目中可能会有多级的嵌套请求,在代码中就会出现多级嵌套,这样的代码维护起来简直要命。

于是Promise出现了。我们如果用Promise来改造一下ajax,代码就会成为这样:

javascript 复制代码
ajaxPromise("https://api.example.com/create", {
  name: "John",
  age: 30,
})
  .then(function (data) {
    return ajaxPromise("https://api.example.com/query", data);
  })
  .catch(function (error) {
    console.log("请求失败", error);
  });

怎么样,是不是代码一下就简洁了很多,就算我们还要再循环嵌套多个请求,也无非就是:

javascript 复制代码
ajaxPromise("https://api.example.com/create", {
  name: "John",
  age: 30,
})
  .then(function (data) {
    return ajaxPromise("https://api.example.com/query", data);
  })
  .then(function (data) {
    return ajaxPromise("https://api.example.com/query1", data);
  })
  .then(function (data) {
    return ajaxPromise("https://api.example.com/query2", data);
  })
  .catch(function (error) {
    console.log("请求失败", error);
  });

依然有高可读性和高维护性,不会出现任何多余的代码嵌套。 那么,这个ajaxPromise是如何实现的呢?代码其实也很简单:

javascript 复制代码
function ajaxPromise(url, data) {
  return new Promise((resolve, reject) => {
    $.ajax({
      url: url,
      data: data,
      success: function (data) {
        resolve(data);
      },
      error: function (error) {
        reject(error);
      },
    });
  });
}

现在看不懂也没关系,我们把下面的初级版源码学完就懂啦。

Promise初级版源码

Promise基本功能

我们先来说说,如何实现一个只能实现基本功能的Promise源码:

首先使用Promise的第一步,就是新建一个Promise对象,并且要传入一个方法作为参数。类似这样:

javascript 复制代码
new Promise(() => {
  ...
});

对于Promise来说,接收的方法要立即执行。所以我们要先新建一个Promise类,并将接收的一个参数方法立即执行。

我们用ES6的class关键字来定义一个类,把自定义方法以executor进行命名,并在类的构造函数constructor中进行接收调用:

javascript 复制代码
class Promise {
  constructor(executor) {
    executor();	// 立即执行参数方法
  }
}

随后我们要知道,一个promise对象需要有哪些属性?我们来看看promise对象长啥样:

可以看到,图上的三个不同的Promise实例对象,它们都有两个共同属性值:PromiseStatePromiseResult,这就是我们下一步要定义到到Promise中的值。

我们要先介绍一下第一个属性值:PromiseState,也就是Promise的"状态":每个Promise对象都会有一个"状态","状态"无非上图三种情况:

  • pending(等待状态)
  • fulfilled(成功状态)
  • rejected(失败状态)

每一个Promise对象在初始阶段都是pending状态,也就是"等待执行完成"的状态,而随着我们的自定义方法executor的执行,根据方法执行结果来对这个Promise对象的状态PromiseState进行相应修改。

另一个属性值PromiseResult是Promise对象的值,每个promise都有自己的值,这个值可以是任何类型,默认是undefined。当然这个值和"状态"一样,都是在参数方法executor执行时被修改的。

我们来在代码上定义这两个属性值的初始状态:

javascript 复制代码
class Promise {
  constructor(executor) {
	const self = this;
    self.state = "pending";
    self.result = undefined;

    executor();
  }
}

完成了对promise对象基本属性的定义,我们要来做关键一步了:刚刚一直在说promise对象的状态和值都是被参数方法executor所改变的,我们就来看看自定义方法executor是怎么改变promise对象的状态的。

我们先来想想,Promise到底干了个啥?举个例子:Promise是一个很靠谱的人,他可以完成你交给他的所有事情,你只用事先把要办事的具体流程单填好(executor):

除了告诉它你要办的事是啥,还有一个很重要的特点:Promise办事,并不是只把你交代的事情"做完"就算完事,而是"你们事先说好怎样算成功,怎样算失败"(比如上图例子,偷东西并不算成功,而是东西要符合条件才算成功)。只要你在流程单上,把什么是"办成了"的状态用绿色的笔 打个勾,把什么是"办垮了"用红笔打个叉。

Promise并不会以事情做没做完来决定这一单是否成功,而是以你定义的方式来决定。最后不管事情结果咋样,Promise都会告诉你结果。

所以回到代码上,你传入的executor参数就是流程单,你在里面写上要交给Promise执行的代码,并且在其中调用resolve方法表示这事成功了,而调用reject方法表示这事失败了。比如这样:

javascript 复制代码
new Promise((resolve, reject) => {
  const 采购清单 = 偷东西()
  if (采购清单.include('海鱼')) {
    resolve()	// 绿笔打的勾,说明采购清单中的采购物品是海鱼,任务成功
  } else if (采购清单.include('河鱼')) {
    reject()	// 红笔打的叉,说明采购清单中的采购物品是河鱼,任务失败
  }
})

这下你应该明白Promise是怎么用的了吧!至于这个resolvereject是从哪来的?当然是源码中定义的,你就当它们是Promise这个人给你准备的绿笔和红笔,方便你写流程单用的就好。

这两个方法做的事情其实并不神秘,相信有同学已经猜到了,这两个方法只不过是用来改变promise状态和值而已。我们来简单的实现一下:

javascript 复制代码
class Promise {
  constructor(executor) {
    const self = this;
    self.state = "pending";
    self.result = undefined;

    executor(
	  // 在调用自定义方法executor时,传入两个方法供executor调用
      function resolve(value) {
        self.state = 'fulfilled';
        self.result = value;
      },
      function reject(reason) {
        self.state = "rejected";
        self.result = reason;
      }
    );
  }
}

其实不过就是这样,Promise在调用你传入的函数executor时,传入对应的resolvereject方法作为参数,方法功能就是改变当前promise对象的stateresult

  • 如果你调用了resolve,则表示任务成功,promise的状态会变为fulfilled;
  • 如果你调用了reject,则表示任务失败,promise的状态会变为rejected;
  • 不管你调用哪个方法,你调用方法时传递的参数会成为promise的result的值;

我们来调用试试看:

javascript 复制代码
const shoppingList = ['海鱼', '海鱼', '海鱼'];  // 采购清单
const steal = () => {  // 偷采购清单的方法
  return shoppingList;
}

var p = new Promise((resolve, reject) => {
  const list = steal()
  if (list.includes('海鱼')) {
    resolve(`任务成功,购买清单中有${list.filter(item => item === '海鱼').length}份海鱼`)
  } else if (list.includes('河鱼')) {
    reject(`任务成功,购买清单中有${list.filter(item => item === '河鱼').length}份河鱼`)
  }
});

console.log(p);

结果看起来已经很像那么回事了,Promise已经帮我们把交给他的任务顺利完成,并且把任务状态和结果都修改成了正确的样子。

让我们再来完善一下代码,Promise有个特点:Promise的状态一旦被确定就不能再被改变了。所以我们把resolvereject方法的功能抽到一个change方法中并进行判断。这个change方法后面还有大用处,我们多看它几眼:

javascript 复制代码
class Promise {
  constructor(executor) {
    self = this;
    self.state = "pending";
    self.result = undefined;
	// 新建change方法来管理promise的状态和值
    const change = function change(state, data) {
      if (self.state !== "pending") return;  // 判断是否已为确定状态(fulfilled, rejected)
      self.state = state;
      self.result = data;
    };

    executor(
      function resolve(value) {
        change("fulfilled", value);
      },
      function reject(reason) {
        change("rejected", reason);
      }
    );
  }
}

我们来试试"防止二次篡改状态"的逻辑是否生效:先调用resolve方法再来调用reject方法会怎么样:

javascript 复制代码
var p = new Promise((resolve, reject) => {
  resolve('成功了');
  reject('失败了');
});

console.log(p);

没问题,后面调用的reject并没有再次改变promise的状态!

好了,Promise本体部分已经接近完成了,我们接下来又有新问题了:这偷采购清单可是件大事,任务成功后,怎么都得开个庆功宴;但如果失败了,我们就要开复盘大会。

而Promise不仅可以帮你完成任务本身,你还可以把要在任务结束后要做的事情也交代给他,他一样会帮你实现。

promise.then

用过Promise的同学一定知道我说的是.then方法了。

.then方法就像你交代Promise的第二张办事流程单,你要在上面写好任务成功后做什么、任务失败后做什么:

所以.then方法的调用其实非常简单,就是传入两个参数,分别对应成功后的方法和失败后的方法:

javascript 复制代码
p.then(
  (data) => {
    // 成功方法
  },
  (reason) => {
    // 失败方法
  }
);

.then方法的实现也很简单,我们在其中判断当前promise的状态,如果是fulfilled就执行第一个参数方法,如果是rejected就执行第二个参数方法。并且把当前promise的值(result)作为参数传递给调用的参数方法。

.then方法是每个promise对象都能调用的方法,所以我们把它定义在Promise的原型对象上:

javascript 复制代码
Promise.prototype.then= function then(onfulfilled, onrejected) {
  if (this.state === "fulfilled") {
    onfulfilled(this.result);
  } else if (this.state === "rejected") {
    onrejected(this.result);
  }
}

很好理解吧!我们来试试效果:

javascript 复制代码
var p = new Promise((resolve, reject) => {
    const list = steal();
    if (list.includes("海鱼")) {
      resolve(list.filter((item) => item === "海鱼").length);
    } else if (list.includes("河鱼")) {
      reject(list.filter((item) => item === "河鱼").length);
    }
});

p.then(
  (data) => {
    console.log(`任务成功,开庆功宴,开了${data}瓶香槟`);
  },
  (reason) => {
    console.log(`任务失败,开庆功宴,抽了${reason}个嘴巴`);
  }
);

结果如我们所愿,promise把我们在.then中传递的成功方法顺利调用了,你也可以试试如果我们把任务清单内容改成"河鱼",这里的结果会不会有变化呢?

用Promise处理异步问题

但我们现在的代码有点问题:一般我们需要交给Promise的任务都不是可以马上完成的(同步任务),而是需要等待一段时间才可以完成的(异步任务)。

我们假设"偷购物清单"这件事需要一秒钟,我们再来看看会怎么样:

javascript 复制代码
var p = new Promise((resolve, reject) => {
  console.log('开始执行任务,一秒后执行完毕...');
  setTimeout(() => {	// 设定定时器,一秒后偷到购物清单
    const list = steal();
    if (list.includes("海鱼")) {
      resolve(list.filter((item) => item === "海鱼").length);
    } else if (list.includes("河鱼")) {
      reject(list.filter((item) => item === "河鱼").length);
    }
  }, 1000)
});

p.then(
  (data) => {
    // 成功方法
    console.log(`任务成功,开庆功宴,开了${data}瓶香槟`);
  },
  (reason) => {
    // 失败方法
    console.log(`任务失败,开庆功宴,抽了${reason}个嘴巴`);
  }
);

这时候我们的代码就出问题了,我们没有获得任何输出结果。

原因也不复杂:.then中的方法是立即执行的,此时p这个promise还没有确定任务结果(还没有执行完任务),在then方法执行时p还是pending状态,既不属于成功(fulfilled)的,也不属于失败(rejected)的,自然就不会有任何输出结果了。那这个多简单!我们再写一个判断条件不就行了:

javascript 复制代码
Promise.prototype.then= function then(onfulfilled, onrejected) {
  if (this.state === "fulfilled") {
    onfulfilled(this.result);
  } else if (this.state === "rejected") {
    onrejected(this.result);
  } else if (this.state === "pending") {
    // 还未完成任务的情况
    console.log(this);
  }
}

这下有点不好办了,then方法在被执行的时候任务还没有执行完,此时我们也不知道任务结果,这也没法去调用相应的回调函数呀。

我们想想怎么解决这个问题。还记得当promise任务执行完成,不论成功失败,唯一可以改变promise状态的方法是什么吗?没错,就是我们前面写的change方法:

javascript 复制代码
class Promise {
  constructor(executor) {
    self = this;
    self.state = "pending";
    self.result = undefined;

    // 新建change方法来管理promise的状态和值
    const change = function change(state, data) {
      if (self.state !== "pending") return;
      self.state = state;
      self.result = data;
    };

    executor(
      (data) => {
        //resolve
        change("fulfilled", data);
      },
      (reason) => {
        // reject
        change("rejected", reason);
      }
    );
  }
}

既然只有change方法可以改变promise的状态,也就是说不管任务什么时候执行完毕,change方法都是第一个知道的。那我们不如这样:如果then方法执行时promise还是pending状态(还没有执行完任务),那就麻烦change方法帮帮忙,等任务执行完后,由change方法来调用对应的成功/失败回调。

逻辑很简单,但还是有个小问题:then方法和change方法直接没法直接沟通,毕竟then方法是原型方法,而change方法是个私有方法。我们不如直接给每个任务都建立一个备忘录,then方法如果遇到当前promise还没完成的情况,就把它所接收到的两个函数记录在备忘录上;而change方法执行的时候,就去看看备忘录上有没有记录,有的话就把对应的方法执行了。

说干就干:

javascript 复制代码
class Promise {
  constructor(executor) {
    self = this;
    self.state = "pending";
    self.result = undefined;
    self.onfulfilledCallbacks = [];  // 成功回调备忘录
    self.onrejectedCallbacks = [];  // 失败回调备忘录

    const change = function change(state, data) {
      if (self.state !== "pending") return;
      self.state = state;
      self.result = data;
	  // 执行"备忘录"中的回调方法
      const callbacks = self.state === "fulfilled" ? self.onfulfilledCallbacks : self.onrejectedCallbacks;  // 根据成功/失败状态,判断要执行的备忘录
      callbacks.forEach((callback) => {  // 遍历执行备忘录中所有方法
        callback(self.result);
      });
    };

    executor(
      (data) => {
        change("fulfilled", data);  //resolve
      },
      (reason) => {
        change("rejected", reason);  // reject
      }
    );
  }
}

Promise.prototype.then= function then(onfulfilled, onrejected) {
  if (this.state === "fulfilled") {
    onfulfilled(this.result);
  } else if (this.state === "rejected") {
    onrejected(this.result);
  } else if (this.state === "pending") {
	// 还未完成任务的情况
    this.onfulfilledCallbacks.push(onfulfilled);  // 将当前未执行的成功回调写到"成功备忘录"中
    this.onrejectedCallbacks.push(onrejected);  // 将当前未执行的失败回调写到"失败备忘录"中
  }
}

新增的代码不多,我们将"备忘录"设置为两个数组:"成功备忘录(onfulfilledCallbacks)"和"失败备忘录(onrejectedCallbacks)"。因为同一个Promise对象的then方法可以被多次调用(可以多开几次庆功宴),所以我们用两个数组来进行存储。

then方法中,对于当前未完成的promise,我们将对应的回调方法存入到对应的备忘录中。

change方法中,我们根据promise执行完成后的情况,对应地遍历执行对应的备忘录中的回调方法。

就是这么简单,我们来看看效果:

完美实现了!

完整版初级Promise

当然,Promise还有一个特点就是:executor会同步执行,而.then中的回调方法都是异步执行的。我们来略微改造一下代码,把每个.then中的回调方法都放入到异步微任务队列中去等待执行(如果你不知道什么是异步宏任务和异步微任务,记得留言告诉我,下次教你浏览器的"事件循环机制"),我们来看看改造后的代码全貌:

javascript 复制代码
class Promise {
  constructor(executor) {
    self = this;
    self.state = "pending";
    self.result = undefined;
    self.onfulfilledCallbacks = [];   // 成功回调备忘录
    self.onrejectedCallbacks = [];    // 失败回调备忘录

    const change = function change(state, data) {
      if (self.state !== "pending") return;
      self.state = state;
      self.result = data;
      // 执行"备忘录"中的回调方法
      const callbacks = self.state === "fulfilled" ? self.onfulfilledCallbacks : self.onrejectedCallbacks;  // 根据成功/失败状态,判断要执行的备忘录
      callbacks.forEach((callback) => { // 遍历执行备忘录中所有方法
        queueMicrotask(() => callback(self.result));
      });
    };

    executor(
      (data) => {
        change("fulfilled", data);  //resolve
      },
      (reason) => {
        change("rejected", reason); // reject
      }
    );
  }
}

Promise.prototype.then= function then(onfulfilled, onrejected) {
  if (this.state === "fulfilled") {
    queueMicrotask(() => onfulfilled(this.result));
  } else if (this.state === "rejected") {
    queueMicrotask(() => onrejected(this.result));
  } else if (this.state === "pending") {
    // 还未完成任务的情况
    this.onfulfilledCallbacks.push(onfulfilled);    // 将当前未执行的成功回调写到"成功备忘录"中
    this.onrejectedCallbacks.push(onrejected);    // 将当前未执行的失败回调写到"失败备忘录"中
  }
}

const shoppingList = ["海鱼", "海鱼", "海鱼"];
const steal = () => {
  return shoppingList;
};

var p = new Promise((resolve, reject) => {
  console.log('开始执行任务,一秒后执行完毕...');
  setTimeout(() => {    // 设定定时器,一秒后偷到购物清单
    const list = steal();
    if (list.includes("海鱼")) {
      resolve(list.filter((item) => item === "海鱼").length);
    } else if (list.includes("河鱼")) {
      reject(list.filter((item) => item === "河鱼").length);
    }
  }, 1000)
});

p.then(
  (data) => {
    // 成功方法
    console.log(`任务成功,开庆功宴,开了${data}瓶香槟`);
  },
  (reason) => {
    // 失败方法
    console.log(`任务失败,开庆功宴,抽了${reason}个嘴巴`);
  }
);

后记

其实Promise的基本逻辑就是这样了,虽然我们写的这确实是个非常基础的版本,但你学完这篇文章的内容后,对于Promise的基本使用和基本实现逻辑一定是了然于胸啦。

你也可以用现在的这个Promise代码来尝试实现一下实际开发中会频繁用到的网络请求,尝试像第二章一样把ajax封装成一个ajaxPromise方法等等。记得一定要把代码敲出来!看过和敲过的效果可是完全不同的!

下一篇文章 ,带你写一个符合A+规范的Promise,我们还要实现链式调用(让then方法返回promise)、错误处理、Promise.all等等功能!

写文不易,想看的同学多多留言点赞!

相关推荐
一坨阿亮2 小时前
使用e-tree开发树形穿梭框
javascript·vue.js·elementui
excel2 小时前
为什么需要构建工具(Webpack / Vite 的本质)
前端
lang201509282 小时前
Java SAX 流式解析全解:从原理到 EasyExcel 实战
java·前端·javascript
VidDown2 小时前
视频协议传输全解析:从 HTTP/HTTPS 到 HLS/DASH 的完整旅程
javascript·网络·http·https·编辑器·音视频·视频编解码
Rain5092 小时前
2.4. PostgreSQL 数据库连接与实战指南
前端·数据库·人工智能·后端·postgresql·数据分析
console.log('npc')2 小时前
Codex 桌面端接入 Headroom 压缩代理完整教程
前端·vscode
独泪了无痕2 小时前
Vue集成uuid生成唯一标识实践指南
前端·vue.js
yuanyxh10 小时前
Mac 软件推荐
前端·javascript·程序员
万少10 小时前
AtomCode开发微信小程序《谁去呀》 全流程
前端·javascript·后端
某人辛木11 小时前
Web自动化测试
前端·python·pycharm·pytest