面试突破:利用Promise知识的精准应用获得优势

前言

Promise 是面试中的热门话题,无论你面试的岗位是什么,几乎都会涉及到这个重要的概念。在面试之前,我们都会对 Promise 的基本概念、功能、状态及其转变含义、相关方法等进行深入的复习和准备。然而,这些知识点应该如何在面试中恰到好处地呈现出来呢?我们又该如何才能在面试中娓娓道来,展现我们对 Promise 的深刻理解和熟练运用呢?

人生百态,思绪千川。每个面试官的提问方式和侧重点可能会有所不同。这里有一些经典的问法:

面试官:你平时如何运用promise?/ 对promise的运用掌握得如何?

面试官:请详细介绍一下promise?/ 能否简述一下promise的应用?

面试官:你对异步编程了解多少?/ 你们项目是如何处理异步问题的?

面试官:你如何理解异步编程?/ 你对promise的理解是什么?

面试官:请手写一个promise示例

以及一些基础问题的问法:promise的工作原理 / promise的状态 / 请解释Promise的链式调用 等。

promise是干什么的

要想在Promise上获取优势,首先就要深刻的了解promise是干什么的:

Promise是JavaScript中用于处理异步操作的一种机制。它可以让我们更优雅地处理异步代码,避免了回调地狱(callback hell)的问题。

在传统的回调方式中,我们需要通过嵌套多个回调函数来处理异步操作的结果,导致代码难以理解和维护。而Promise则提供了一种更结构化的方式来处理异步操作。

Promise对象代表了一个异步操作的最终完成或失败的状态,并且可以返回一个值。它有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已失败)。一旦Promise的状态发生改变,就不会再改变。

通过使用Promise,我们可以通过链式调用的方式编写异步代码,使其更加可读和易于理解。Promise提供了两个重要的方法:thencatchthen方法用于处理Promise的成功状态,catch方法用于处理Promise的失败状态。

下面是一个使用Promise的简单示例:

javascript 复制代码
const fetchData = () => {
	return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = 'Hello, World!';
      resolve(data);
    }, 2000);
  });
};

fetchData()
  .then(data => {
    console.log(data); // 输出:Hello, World!
  })
  .catch(error => {
    console.error(error);
  });

在上面的示例中,fetchData函数返回一个Promise对象,在2秒后通过resolve方法将结果传递给then方法进行处理。

通过使用Promise,我们可以更好地处理异步操作,避免了回调地狱的问题,使代码更加清晰和可维护。同时,Promise还提供了更多的方法和功能,如Promise.allPromise.race等,用于处理多个异步操作的情况。

promise有哪些方法

Promise对象提供了一些方法来处理和操作异步操作的状态和结果。以下是Promise对象常用的方法:

  1. then(onFulfilled, onRejected):用于处理Promise对象的成功状态和失败状态。onFulfilled是在Promise成功时调用的回调函数,接收Promise的返回值作为参数;onRejected是在Promise失败时调用的回调函数,接收Promise的错误原因作为参数。then方法返回一个新的Promise对象,可以进行链式调用。

  2. catch(onRejected):用于处理Promise对象的失败状态。catch方法相当于then(null, onRejected),用于捕获Promise的错误。catch方法也返回一个新的Promise对象,可以进行链式调用。

  3. finally(onFinally):用于在Promise对象无论成功或失败时都执行的回调函数。finally方法接收一个回调函数作为参数,无论Promise的状态如何,都会执行该回调函数。finally方法也返回一个新的Promise对象,可以进行链式调用。

  4. Promise.resolve(value):返回一个以给定值解析后的Promise对象。如果给定的值是一个Promise对象,那么Promise.resolve将直接返回该对象;如果给定的值是一个thenable对象(具有then方法),那么Promise.resolve将将该对象转换为Promise对象并解析;否则,Promise.resolve将返回一个以给定值为成功状态的Promise对象。

  5. Promise.reject(reason):返回一个以给定原因拒绝的Promise对象。

  6. Promise.all(iterable):接收一个可迭代对象(如数组或类数组对象)作为参数,返回一个Promise对象。当所有的Promise对象都成功解析时,返回的Promise对象将以一个包含所有Promise结果的数组进行解析;如果其中任何一个Promise对象失败,则返回的Promise对象将以失败状态解析,并传递失败的原因。

  7. Promise.race(iterable):接收一个可迭代对象作为参数,返回一个Promise对象。当可迭代对象中的任何一个Promise对象解析或拒绝时,返回的Promise对象将以相应的状态进行解析或拒绝。

这些方法是Promise对象的基本方法,可以根据具体的需求选择使用。它们使得异步操作的处理更加方便和灵活,提供了更好的代码组织和可读性。

promise优点

解决异步编程中的回调地狱问题

回调地狱是指在多层嵌套的回调函数中,代码难以阅读和维护。使用Promise可以将异步代码封装在Promise对象中,从而避免回调地狱问题。例如:

ini 复制代码
getUser().then(user => {
  // 处理获取用户信息的结果
}).catch(error => {
  // 处理获取用户信息的错误
});
避免全局变量的污染

使用回调函数时,经常会在回调函数中使用全局变量,从而导致代码可读性差且易于出错。使用Promise可以避免这个问题。例如:

ini 复制代码
const data = await getUser();
console.log(data);
实现异步操作的依赖关系

在多个异步操作之间,可能需要保持一定的依赖关系。使用Promise的then()和catch()方法可以方便地实现这个功能。例如:

ini 复制代码
getUser().then(user => {
  return getPostByUserId(user.id);
}).then(post => {
  console.log(post);
}).catch(error => {
  console.error(error);
});
实现异步操作的异常处理

在异步操作中,可能会出现各种错误,如网络错误、用户权限错误等。使用Promise的catch()方法可以方便地捕获这些异常,并进行相应的处理。例如:

ini 复制代码
getUser().then(user => {
  return getPostByUserId(user.id);
}).then(post => {
  console.log(post);
}).catch(error => {
  console.error(error);
});
promise双层嵌套,外面的catch可以拿到里面then的错误吗

当内层的then方法中发生错误时,如果没有进行错误处理,错误会被传递到外层的catch方法中。这是因为Promise链式调用中的每个then方法都会返回一个新的Promise对象,如果其中任何一个then方法中抛出异常或返回一个rejected状态的Promise对象,会直接跳过后续的then方法,进入最近的catch方法。

以下是一个示例,展示了外层的catch捕获内层then方法中的错误:

javascript 复制代码
function asyncOperation() {

	return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟异步操作
      const randomNum = Math.random();
      if (randomNum < 0.5) {
        resolve('Success');
      } else {
        reject('Error');
      }
    }, 1000);
  });
}

asyncOperation()
  .then(result => {
    console.log(result); // Success
    throw new Error('Something went wrong');
  })
  .then(() => {
    console.log('This will not be executed');
  })
  .catch(error => {
    console.error(error); // Error: Something went wrong
  });

Promise方法的使用技巧:

利用Promise的all()方法处理多个异步操作

Promise的all()方法可以接受一个包含多个Promise对象的数组,然后依次等待这些Promise对象都完成后再执行回调函数。这个方法可以帮助我们更方便地处理多个异步操作。例如:

ini 复制代码
const promises = [
  getUser().then(user => {
	return getPostByUserId(user.id);
  }),
  getCommentByPostId(postId).then(comment => {
	console.log(comment);
  })
];

Promise.all(promises).then(results => {
  console.log(results);
}).catch(error => {
  console.error(error);
});
利用Promise的race()方法处理最先完成的异步操作

Promise的race()方法可以接受一个包含多个Promise对象的数组,然后依次等待这些Promise对象中最先完成的一个对象完成后执行回调函数。这个方法可以帮助我们更方便地处理最先完成的异步操作。例如:

ini 复制代码
const promise1 = getUser().then(user => {
  return getPostByUserId(user.id);
}).then(post => {
  console.log(post);
});

const promise2 = getCommentByPostId(postId).then(comment => {
  console.log(comment);
});

Promise.race([promise1, promise2]).then(result => {
  console.log(result);
}).catch(error => {
  console.error(error);
});
利用Promise的async/await语法更加简洁地处理异步操作

ES2017引入了async/await语法,可以更加简洁地处理异步操作。例如:

javascript 复制代码
async function getUser() {
  try {
	const response = await fetch('/api/user');
	const user = await response.json();
	return user;
  } catch (error) {
	console.error(error);
  }
}

async function getPostByUserId(userId) {
  try {
	const response = await fetch('/api/post', {
	  method: 'GET',
	  headers: {
		'Content-Type': 'application/json'
	  },
	  params: {
		userId: userId
	  }
	});
	const post = await response.json();
	return post;
  } catch (error) {
	console.error(error);
  }
}

async function getCommentByPostId(postId) {
  try {
	const response = await fetch('/api/comment', {
	  method: 'GET',
	  headers: {
		'Content-Type': 'application/json'
	  },
	  params: {
		postId: postId
	  }
	});
	const comment = await response.json();
	return comment;
  } catch (error) {
	console.error(error);
  }
}

async function main() {
  const user = await getUser();
  const post = await getPostByUserId(user.id);
  const comment = await getCommentByPostId(post.id);
  console.log(comment);
}

main().catch(error => {
  console.error(error);
});

此外还有一些问题侧重点和回答要点

解释 Promise 的概念:

在回答问题之前,确保你对 Promise 的概念有清晰的理解。解释 Promise 是一种用于处理异步操作的对象,可以用于处理异步代码的流程控制和错误处理。

使用 Promise 进行异步操作:

展示你如何使用 Promise 来处理异步操作。例如,展示如何使用 Promise 包装一个异步函数,并使用 .then() 和 .catch() 方法处理成功和失败的情况。

解决回调地狱问题:

回调地狱是指多个嵌套的回调函数造成的代码难以理解和维护的问题。展示你如何使用 Promise 的链式调用(then() 方法的返回值是一个新的 Promise)来解决回调地狱问题,使代码更清晰、可读性更高。

使用 Promise.all 或 Promise.race:

展示你如何使用 Promise.all() 或 Promise.race() 方法来处理多个 Promise 实例。这些方法可以用于并行执行多个异步操作,或在多个异步操作中选择最快的结果。

错误处理:

展示你如何在 Promise 链中处理错误。使用 .catch() 方法来捕获和处理 Promise 链中的错误,并展示你如何正确地传递错误信息。

异步代码的测试:

展示你如何使用异步测试框架(如 Mocha 或 Jest)来测试包含 Promise 的异步代码。展示你如何使用 async/await 或 .then() 方法等技术来处理异步测试。

对比其他异步处理方式:

展示你对 Promise 和其他异步处理方式(如回调函数、事件监听、async/await 等)的理解和对比,说明为什么 Promise 是更优的选择。

Promise的工作原理

可以简单概括为以下几个步骤:

  1. 创建Promise对象:通过new Promise(executor)来创建一个Promise对象,其中executor是一个带有两个参数(resolvereject)的函数。resolve函数用于将Promise对象从pending状态转换为fulfilled状态,reject函数用于将Promise对象从pending状态转换为rejected状态。

  2. 异步操作:在executor函数中执行异步操作,例如发送网络请求、读取文件等。异步操作可以是任何需要一定时间才能完成的任务。

  3. 状态转换:异步操作执行完毕后,通过调用resolve函数将Promise对象的状态从pending转换为fulfilled,或者通过调用reject函数将Promise对象的状态从pending转换为rejected。同时,可以将异步操作的结果或错误信息作为参数传递给resolvereject函数。

  4. 处理Promise的结果:通过调用Promise对象的then方法来处理Promise的成功状态,或通过调用catch方法来处理Promise的失败状态。thencatch方法返回一个新的Promise对象,可以进行链式调用。

  5. 链式调用:通过链式调用thencatch方法,可以依次处理多个异步操作的结果。每个then方法都接收上一个Promise的成功状态的返回值作为参数,并返回一个新的Promise对象。如果在链式调用中的任何一个then方法中抛出异常或返回一个rejected状态的Promise对象,将会跳过后续的then方法,直接进入最近的catch方法。

Promise的工作原理基于状态转换和链式调用的机制,使得异步操作的处理更加灵活和可读。

手写promise

面试的时候首先要大体对面试官和他对这场面试的态度以及状态有一个了解,他是和颜悦色的还是态度冷淡的,他是随意轻松的渐进式聊天还是对着大纲重点突击的问询。然后我们需要对面试官的态度给一个相似的且正向的态度来对待,先回答对方提出的问题再进行自己知识输出和扩展。比如对方严肃且公式的开门见山说手写一个promise,那么此时如果回答是:"说到Promise,那得先介绍一下JavaScript异步编程的发展史",~~然后一顿输出~~,"这样就理解为啥Promise会出现以及Promise解决了什么问题"~~然后再一顿输出~~。这个交流过程是极有可能会被叫停的,对方没有抓到他想要的信息,回答的内容也有点在顾左右而言他的意味,不够直击要点。 但是换一个形式,我们只简单的引题,然后开门见山的写代码,即输出了对方想要考察的点,也大方自信:

可以这么说(边说边写,不要在乎纸上乱不乱,对方能听明白你表达的意思是最重要的)

版本一:"我之前没有手写过promise,我试着写一下,首先是基本内容,promise,包括 Promise 构造函数、状态管理、then 方法、错误处理、链式调用"。

如果你觉得只说这几个词有点干有点单调,那么可以附带一些解释。

版本二(边说边写下方法名):

当手写一个基本的 Promise 实现时,您需要考虑以下几个关键点:

Promise 构造函数:Promise 构造函数接受一个执行器函数作为参数,该执行器函数包含两个参数 resolve 和 reject。resolve 用于将 Promise 标记为成功并传递结果,reject 用于将 Promise 标记为失败并传递错误信息。

状态管理:Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。在构造函数中,Promise 的初始状态应该为 pending,并且可以通过 resolve 或 reject 方法将其转换为 fulfilled 或 rejected。

then 方法:Promise 实例上应该有一个 then 方法,用于注册成功和失败的回调函数。then 方法接受两个参数,分别是成功回调函数和失败回调函数。当 Promise 的状态变为 fulfilled 时,应该调用成功回调函数并传递结果;当状态变为 rejected 时,应该调用失败回调函数并传递错误信息。

错误处理:Promise 需要具备错误处理的能力。在 then 方法中,如果发生异常或返回一个 rejected 的 Promise,应该将 Promise 的状态标记为 rejected,并将错误信息传递给下一个失败回调函数。

链式调用:Promise 的 then 方法应该返回一个新的 Promise,以支持链式调用。这样可以实现串行执行异步操作,并在每个步骤中处理结果或错误。

(写完方法名就可以往里面添方法了)

我们需要记录需要进行 resolve 的操作,然后在 promise 执行 then 时,可以调起该 resolve 并进行处理。
typescript 复制代码
type FuncType = (...args: any[]) => any;
type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;

export class HePromise {
  private resolves: FuncType[] = [];

  constructor(executor: ExecutorFunc) {
	const { resolve } = this;
	executor(resolve);
  }

  private resolve = (resolvedVal: any) => {
	const { resolves } = this;
	while (resolves.length) {
	  const cb = resolves.shift();
	  if (cb) cb(resolvedVal);
	}
  };

  then(resolveFunc: FuncType) {
	this.resolves.push(resolveFunc);
  }
}

完整执行过程是:当执行 new HePromise() 时,constructor 函数会执行,不过这里需要注意的是,我们暂时只考虑异步操作,忽略了同步的情况。异步情况下 executor 函数会在未来某个时间点执行,而从初始化到这个时间点之间,正是 then 函数执行收集依赖的过程。

添加 reject 的处理
typescript 复制代码
type FuncType = (...args: any[]) => any;

type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;

export class HePromise {
  private resolves: FuncType[] = [];
  private rejects: FuncType[] = [];

  constructor(executor: ExecutorFunc) {
	const { resolve, reject } = this;
	executor(resolve, reject);
  }

  private resolve = (resolvedVal: any) => {
	const { resolves } = this;
	while (resolves.length) {
	  const cb = resolves.shift();
	  if (cb) cb(resolvedVal);
	}
  };

  private reject = (rejectedVal: any) => {
	const { rejects } = this;
	while (rejects.length) {
	  const cb = rejects.shift();
	  if (cb) cb(rejectedVal);
	}
  };

  then(resolveFunc: FuncType, rejectFunc?: FuncType) {
	this.resolves.push(resolveFunc);
	if (rejectFunc) this.rejects.push(rejectFunc);
  }
}

基本方法实现了,但是代码还没有处理异步操作和链式调用。Promise 应该支持异步操作,并且 then 方法应该返回一个新的 Promise,以支持链式调用。这是 Promise 的核心特性之一。

如果你想在这里换点思路,可以这样:写完一个方法之后要对方法进行测试

使用 jest 进行测试,首先配置 jest 环境,

npm install --save-dev jest 在 package.json 中修改执行脚本:

json 复制代码
{
  "scripts": {
    "test": "jest"
  }
}

编写对应测试用例:

ini 复制代码
describe('test HePromise', () => {
  it('basic usage', done => {
    const p = new HePromise(resolve => {
      setTimeout(() => {
        resolve(1);
      }, 1000);
    });
    try {
      p.then(data => {
        expect(data).toBe(1);
        done();
      });
    } catch (error) {
      done(error);
    }
  });
});

最后执行 pnpm test: PASS ./sum.test.js ✓ adds 1 + 2 to equal 3 (5ms) 执行测试,测试通过,完成 Promise 初始版本封装。执行流程总结如下:

  • Promise 构造方法需要传入一个函数,我们将这个函数命名为 executor;
  • 在 executor 内部,将各任务放入宏/微任务队列中(宏/微任务请参看 事件循环 );
  • 在 then 和 catch 中可收集到 resolve、reject 依赖,并将该依赖存放到对应队列中;
  • 异步任务执行完以后,调用 executor 中的 resolve 或 reject,取出对应队列中的依赖依次执行。
如果话题还要继续那么:增加符合 Promise A+ 规范的状态值

我们为 HePromise 添加状态,根据规范约定,在代码中添加状态枚举值,如下:

ini 复制代码
enum STATUS {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected',
}

在执行 resolve 前,需要检测当前状态是否为 pending,如果是则可以继续执行,否则无法执行 resolve,在执行 resolve 时,将状态置为 fulfilled。reject 方法中同理先检测状态是否为 pending,如果是则继续执行并将状态置为 rejected。 改进后,代码示例如下:

ini 复制代码
type FuncType = (...args: any[]) => any;

type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;

enum STATUS {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected',
}

export class HePromise {
  private status = STATUS.PENDING;
  private resolves: FuncType[] = [];
  private rejects: FuncType[] = [];

  constructor(executor: ExecutorFunc) {
	const { resolve, reject } = this;
	executor(resolve, reject);
  }

  private resolve = (resolvedVal: any) => {
	const { resolves, status } = this;
	if (status !== STATUS.PENDING) return;
	this.status = STATUS.FULFILLED;
	while (resolves.length) {
	  const cb = resolves.shift();
	  if (cb) cb(resolvedVal);
	}
  };

  private reject = (rejectedVal: any) => {
	const { rejects, status } = this;
	if (status !== STATUS.PENDING) return;
	this.status = STATUS.REJECTED;
	while (rejects.length) {
	  const cb = rejects.shift();
	  if (cb) cb(rejectedVal);
	}
  };

  then(resolveFunc: FuncType, rejectFunc?: FuncType) {
	this.resolves.push(resolveFunc);
	if (rejectFunc) this.rejects.push(rejectFunc);
  }
}
支持链式调用

根据 Promise A+ 规范,每次 then 返回的值也需要满足 thenable,也就是说我们需要将 resolve 返回值使用 promise 包裹,在本例中就是需要将返回值包装为新的 HePromise 对象。 开发之前我们不妨先来看看 Promise 链式调用的示例:

ini 复制代码
const p = new Promise(resolve => resolve(1));

p.then(r1 => {
  console.log(r1);
  return 2;
})
  .then(r2 => {
	console.log(r2);
	return 3;
  })
  .then(r3 => {
	console.log(r3);
  });

每次 then 函数调用完,都返回了一个新的数字,令人不解的是,这个数据居然也拥有了 then 函数,可以依次调用。这里需要做的处理时,需要将传入的 resolvereject 函数封装然后放入待执行队列中。简言之,当返回值为一个 Promise 时,需要执行 promise.then 方法,否则直接执行 resolve。改进后的 then 方法如下:

scss 复制代码
then(resolveFunc: FuncType, rejectFunc?: FuncType) {
	return new HePromise((resolve, reject) => {
	  const resolvedFn = (val: any) => {
		try {
		  let resolvedVal = resolveFunc(val);
		  resolvedVal instanceof HePromise
			? resolvedVal.then(resolve, reject)
			: resolve(resolvedVal);
		} catch (error) {
		  if (reject) reject(error);
		}
	  };
	  this.resolves.push(resolvedFn);
	  if (rejectFunc) this.rejects.push(rejectFunc);
	})
  }

可以看到,then 方法调用时,会返回新的 HePromise 对象,该对象中主要做了这样几件事情:

  1. 包装初始 then 方法传入的 resolve 函数;
  2. 先将初始 then 方法传入的 resolve 函数执行,得到返回值,如果返回值是一个新的 HePromise 对象,则需要手动调用该实例的 then 方法,否则直接执行 resolve 函数;
  3. 将包装过的 resolve 函数放入 resolves 队列中,等待执行
补全 reject 的逻辑
scss 复制代码
then(resolveFunc: FuncType, rejectFunc?: FuncType) {
	return new HePromise((resolve, reject) => {
	  const resolvedFn = (val: any) => {
		try {
		  const resolvedVal = resolveFunc(val);
		  resolvedVal instanceof HePromise
			? resolvedVal.then(resolve, reject)
			: resolve(resolvedVal);
		} catch (error) {
		  if (reject) reject(error);
		}
	  };
	  this.resolves.push(resolvedFn);

	  const rejectedFn = (val: any) => {
		if (rejectFunc) {
		  try {
			const rejectedVal = rejectFunc(val);
			rejectedVal instanceof HePromise
			  ? rejectedVal.then(resolve, reject)
			  : resolve(rejectedVal);
		  } catch (error) {
			if (reject) reject(error);
		  }
		}
	  };
	  if (rejectFunc) this.rejects.push(rejectedFn);
	});
  }

编写更多测试用例,进行测试

ini 复制代码
it('chain invoke usage', done => {
  const p = new HePromise(resolve => {
    setTimeout(() => {
      resolve(11);
    }, 1000);
  });

  try {
    p.then(data => {
      expect(data).toBe(11);
      return 'hello';
    })
      .then(data => {
        expect(data).toBe('hello');
        return 'world';
      })
      .then(data => {
        expect(data).toBe('world');
        done();
      });
  } catch (error) {
    done(error);
  }
});

执行测试,可以看到测试用例通过。 不过需要注意的是,根据 Promise A+ 规范,需要对 then 参数进行处理,如果参数不是函数,则需要忽略并继续往下执行,示例如下:

javascript 复制代码
typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;
typeof rejectFunc !== 'function'
  ? (rejectFunc = reason => {
      throw new Error(reason instanceof Error ? reason.message : reason);
    })
  : null;
值过滤与状态变更

与此同时,如果在执行过程中,Promise 状态值已发生变化,则需要根据不同状态直接进行相应,例如,如果是 pending,则将任务放入对应队列中,如果为 fulfilled,直接调用 resolve,如果为 rejected 则直接调用 reject。可以使用 switch 语句进行策略处理,如下:

kotlin 复制代码
switch (this.status) {
  case STATUS.PENDING:
    this.resolves.push(resolvedFn);
    this.rejects.push(rejectedFn);
    break;
  case STATUS.FULFILLED:
    resolvedFn(this.value);
    break;
  case STATUS.REJECTED:
    rejectedFn(this.value);
    break;
}

此处 this.value 是上次执行完后得到的值,起到暂存的目的。补充以上代码后,完整代码示例如下:

ini 复制代码
type FuncType = (...args: any[]) => any;

type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;

enum STATUS {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected',
}

export class HePromise {
  private status = STATUS.PENDING;
  private value = undefined;
  private resolves: FuncType[] = [];
  private rejects: FuncType[] = [];

  constructor(executor: ExecutorFunc) {
    const { resolve, reject } = this;
    executor(resolve, reject);
  }

  private resolve = (resolvedVal: any) => {
    const { resolves, status } = this;
    if (status !== STATUS.PENDING) return;
    this.status = STATUS.FULFILLED;
    this.value = resolvedVal;
    while (resolves.length) {
      const cb = resolves.shift();
      if (cb) cb(resolvedVal);
    }
  };

  private reject = (rejectedVal: any) => {
    const { rejects, status } = this;
    if (status !== STATUS.PENDING) return;
    this.status = STATUS.REJECTED;
    this.value = rejectedVal;
    while (rejects.length) {
      const cb = rejects.shift();
      if (cb) cb(rejectedVal);
    }
  };

  then(resolveFunc: FuncType, rejectFunc?: FuncType): HePromise {
    typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;
    typeof rejectFunc !== 'function'
      ? (rejectFunc = reason => {
          throw new Error(reason instanceof Error ? reason.message : reason);
        })
      : null;

    return new HePromise((resolve, reject) => {
      const resolvedFn = (val: any) => {
        try {
          const resolvedVal = resolveFunc(val);
          resolvedVal instanceof HePromise
            ? resolvedVal.then(resolve, reject)
            : resolve(resolvedVal);
        } catch (error) {
          if (reject) reject(error);
        }
      };
      this.resolves.push(resolvedFn);

      const rejectedFn = (val: any) => {
        if (rejectFunc) {
          try {
            const rejectedVal = rejectFunc(val);
            rejectedVal instanceof HePromise
              ? rejectedVal.then(resolve, reject)
              : resolve(rejectedVal);
          } catch (error) {
            if (reject) reject(error);
          }
        }
      };

      switch (this.status) {
        case STATUS.PENDING:
          this.resolves.push(resolvedFn);
          this.rejects.push(rejectedFn);
          break;
        case STATUS.FULFILLED:
          resolvedFn(this.value);
          break;
        case STATUS.REJECTED:
          rejectedFn(this.value);
          break;
      }
    });
  }
}
同步任务处理

以上情况我们遗漏了一个点,就是同步任务,我们可以看到以上示例中,初始化 HePromise 中的 resolve 都是在未来进行的,如果同步执行 resolve,则以上代码会出现问题。我们的方案是,将初始处理默认放入宏任务队列中,也就是使用 setTimeout 包裹 resolve,这样一来,就能保证即使是同步任务,也可以保证在同步收集完任务以后在执行 executor 中的 resolve 和 reject。示例如下:

ini 复制代码
export class HePromise {
  private resolve = (resolvedVal: any) => {
    setTimeout(() => {
      const { resolves, status } = this;
      if (status !== STATUS.PENDING) return;
      this.status = STATUS.FULFILLED;
      this.value = resolvedVal;
      while (resolves.length) {
        const cb = resolves.shift();
        if (cb) cb(resolvedVal);
      }
    });
  };
}
同理可实现 reject 逻辑。编写测试代码,如下:
ini 复制代码
it('sync task', done => {
  const p = new HePromise(resolve => {
    resolve(123);
  });
  p.then(res => {
    expect(res).toBe(123);
    done();
  });
});
其他方法实现

Promise 中还包括 catch、finally、Promise.resolve、Promise.reject、Promise.all、Promise.race,接下来我们分别来实现。

catch

其实我们可以理解是 then 方法的一个变体,就是 then 方法省略了 resolve 参数,实现如下:

kotlin 复制代码
catch(rejectFnnc) {
  return this.then(undefined, rejectFnnc)
}
finally

该方法保证 Promise 不管是 fulfilled 还是 reject 都会执行,都会执行指定的回调函数。在 finally 之后,还可以继续 then。并且会将值原封不动的传递给后面的 then 函数。针对这个机制也有很多理解,糙版的处理如下:

javascript 复制代码
finally(cb) {
  return this.then(
    value  => {
      cb();
      return value;
    },
    reason  => {
      cb();
      throw reason
    }
  )
}

不过,如果 Promise 在 finally 前返回了一个 reject 状态的 promise,像上面这样编写是无法满足要求的。

finally 对自身返回的 promise 的决议影响有限,它可以将上一个 resolve 改为 reject,也可以将上一个 reject 改为另一个 reject,但不能把上一个 reject 改为 resolve。 这样一来,我们可以将 callback 使用 Promise.resolve 包裹一下,保证后续的 resolve 状态。如下:

javascript 复制代码
finally(cb) {
  return this.then(
	value => HePromise.resolve(cb()).then(() => value),
	reason => HePromise.resolve(cb()).then(() => { throw reason })
  )
}
resolve

调用该静态方法其实就是将值 promise 化,如果传入值本身就是 promise 示例,则直接返回,否则创建新的 promise 示例并返回,示例如下:

javascript 复制代码
static resolve(val) {
  if(val instanceof HePromise) return val
  return new HePromise(resolve => resolve(val))
}

编写测试代码如下:

ini 复制代码
it('HePromise.resolve', done => {
  HePromise.resolve(1).then(res => {
    expect(res).toBe(1);
    done();
  });
});

reject 该方法的原理同 resolve

javascript 复制代码
static reject(val) {
  return new HePromise((resolve, reject) => reject(val))
}

编写测试代码如下:

ini 复制代码
it('HePromise.reject & catch', done => {
  HePromise.reject(1).then(
	res => {
	  expect(res).toBe(1);
	  done();
	},
	error => {
	  expect(error).toBe(1);
	  done();
	},
  );
});

或者通过 catch 的方式,如下:

ini 复制代码
it('HePromise.reject & catch', done => {
  HePromise.reject(1)
    .then(res => {
      expect(res).toBe(1);
      done();
    })
    .catch(error => {
      expect(error.message).toEqual('1');
      done();
    });
});

执行测试,测试通过。

all

就是将传入数组中的值 promise 化,然后保证每个任务都处理后,最终 resolve。示例如下:

ini 复制代码
class HePromise {
  static all(promises: any[]) {
    let index = 0;
    const result: any[] = [];
    const pLen = promises.length;
    return new HePromise((resolve, reject) => {
      promises.forEach(p => {
        HePromise.resolve(p).then(
          val => {
            index++;
            result.push(val);
            if (index === pLen) {
              resolve(result);
            }
          },
          err => {
            if (reject) reject(err);
          },
        );
      });
    });
  }
}

编写测试用例如下:

ini 复制代码
it('HePromise.all', done => {
  HePromise.all([1, 2, 3]).then(res => {
    expect(res).toEqual([1, 2, 3]);
    done();
  });
});

执行测试,测试通过。

race

就是将传入数组中的值 promise 化,只要其中一个任务完成,即可 resolve。示例如下:

ini 复制代码
class HePromise {
  static race(promises: any[]): HePromise {
    return new HePromise((resolve, reject) => {
      promises.forEach(p => {
        HePromise.resolve(p).then(
          val => {
            resolve(val);
          },
          err => {
            if (reject) reject(err);
          },
        );
      });
    });
  }
}

编写测试用例:

ini 复制代码
it('HePromise.race', done => {
  HePromise.race([11, 22, 33]).then(res => {
    expect(res).toBe(11);
    done();
  });
});

执行测试,测试通过。

从 Promise 到 tj/co

其实我们发现,Promise 的处理还是存在嵌套和多级 then 的情况,我们能不能幻想一下,同步写 promise 呢? 这时我们就需要想到另一个技术点------生成器 *** generator *** 我们这里之所以简单将 *** tj/co ***方案应用到项目中,是因为其方案在以前被大多开发者接受,同样还有一些其他技术实现------q、bluebird。 异步处理同步写法需要借助的正式可中断函数,能够在执行完部分操作后,继续完成后续操作。。。

。。。

除了以上的特定问题点之外还有有一个知识的链式输出问题,知识点之间都是相关联的,相信很多人都在面试回答问题的时候遇到过面试官问:既然你说到XXX,那你说一下XXX的什么什么吧。

正如阿基米德(Archimedes)的名言:"给我一个支点,我能撬动地球。"从promise出发可以发展出很多考点,在追求优势的过程中,不能只停留在一个知识点上,而是要以此为基础,构建起一个庞大的知识网络。

例子

扩展

就算 Promise 相较于 callback 代码简化了很多,但是我们通常还是抱怨 Promise 定义太繁琐,有没有更简介的方式呢?如果你关注 JavaScript 的动态那么你就会发现它的新方法:

const { promise, resolve, reject } = Promise.withResolvers();

如何关心 ES 最新拓展相关内容,可以看看这个:

github.com/tc39

分享到这里就要结束了,突然想到一个问题,如果面试官一直不问promise相关的问题,他在说到什么的时候我们可以把话题引到promise上呢?

相关推荐
TttHhhYy1 分钟前
vue写后台管理系统,有个需求将所有的$message消息提示换成确认框来增强消息提示效果,遇到嵌套过多的情况,出现某些问题
前端·javascript·vue.js·anti-design-vue
FIRE32 分钟前
uniapp小程序分享使用canvas自定义绘制 vue3
前端·小程序·uni-app
四喜花露水32 分钟前
vue elementui el-dropdown-item设置@click无效的解决方案
前端·vue.js·elementui
jokerest1231 小时前
web——sqliabs靶场——第五关——报错注入和布尔盲注
前端
谢尔登1 小时前
前端开发调试之 PC 端调试
开发语言·前端
每天吃饭的羊1 小时前
在循环中只set一次
开发语言·前端·javascript
斗-匕1 小时前
面试击穿mysql
mysql·面试
_默_4 小时前
adminPage-vue3依赖DetailsModule版本说明:V1.2.1——1) - 新增span与labelSpan属性
前端·javascript·vue.js·npm·开源
也无晴也无风雨6 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang7 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js