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等等功能!

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

相关推荐
有梦想的刺儿16 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具37 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
清灵xmf1 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据1 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
334554322 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json