深入理解 Promise

更新日期: 2022-01-02阅读: 872标签: Promise

对于前端开发者们来说,ES6 的学习已经成为必然,其中的 Promise 更是频繁出没于各大面试题,因此了解 Promise 已经不能简简单单的会用,更需要深入原理。 《ES6 入门教程》 其中对 Promise 的讲解已经能满足业务场景的需要,来源和基础使用此篇就不做赘述,只是记录一下 Promise 的一些边角知识和我的一些心得体会。

事件循环

深入了解 Promise 之前,我们需要对 JavaScript 的“事件循环(Event Loop)”机制有一定的了解。那么什么是“事件循环”呢?这个如果细说的话又能开一个系列,这里只简单的描述一下。

在 JavaScript 中任务分为两种: 宏任务(Task) 、 微任务(MicroTask) 。在主线程运行的时候, 同步任务 会直接运行, 异步任务 则会根据其类型分别进入“ 宏任务队列 ”和“ 微任务队列 ”,在 同步任务 执行完毕后,会先清空“ 微任务队列 ”,然后再清空“ 宏任务队列 ”,其大致的运行图如下:


:warning: 注意:一般情况下,异步任务都是按顺序依次执行,但同样也存在一些“插队”执行的现象,特别是两异步任务同时执行的情况下,因处理时间的不同可能会出现意料之外的结果。

『图片引用自博客: https://www.cnblogs.com/weiyongchao/p/13766429.html

案例简析

了解了“事件循环”后,我们基本明白了 JavaScript 代码的执行机制, Promise 在其中扮演的是一个“微任务”,基于此,我们分析几个案例来一步步了解 Promise 。

Promise 状态

众所周知, Promise 有三种状态: pending 、 fulfilled 和 rejected ,代码尝试如下:

const p = new Promise((res, rej) => {
  setTimeout(() => {
    res();
  }, 3000);
});

console.log(p);

console.log(Promise.resolve());
console.log(Promise.reject());

// 浏览器中运行结果:
// Promise { <pending> }
// Promise {<fulfilled>: undefined}
// Promise {{<rejected>: undefined}}

Promise 内的执行函数是同步任务

很多新手容易弄混的部分是“Promise 是异步函数,因此它初始化传入的执行函数属于异步任务”,同样用代码来解释:

console.log('start');

const p = new Promise((res, rej) => {
  console.log('working...');
  res();
});

console.log('end');

// 浏览器中的运行结果:
// start
// working...
// end

Promise 状态不可逆

除了最开始的 pending 态外,一旦转变为 fulfilled 或 rejected 状态后,其状态就不会再改变了:

const p = new Promise((res, rej) => {
  res(1);
  rej(2);
});

console.log(p);

// 浏览器中的运行结果:
// Promise {<fulfilled>: 1}

then 链返回的是新 Promise 对象

then 链中,无论你返回什么内容,它都会给你给你包装一个 Promise 的外壳:

const p = new Promise((res, rej) => {
  res();
});

console.log(p.then());
console.log(p.then(() => 1));
console.log(
  p.then(
    () =>
      new Promise((res, rej) => {
        res();
      }),
  ),
);

// 浏览器中的运行结果:
// Promise {<pending>}
// Promise {<pending>}
// Promise {<pending>}

then 链有两个函数,第二个 onReject 函数如果有设置,那么其后续会返回至 fulfilled 状态链:

new Promise((res, rej) => {
  rej(1);
})
  .then(
    res => {},
    rej => {
      console.log('err', rej);
      // return 2
    },
  )
  .then(res => {
    console.log('success', res);
  });

// 浏览器中的运行结果:
// err 1
// success undefined

:warning: 注意:在日常开发中不要设置 then 链的 onReject 函数,说不定那天就因为常规思维而被它给坑了 ( ´∇ ` )。

then 链是可透传的

当 then 链内的内容为非函数的情况下,其会将上一个 Promise 的结果和状态传递给下一个函数:

Promise.resolve('start')
  .then(console.log('pass'))
  .then(res => console.log(res));

// 浏览器中的运行结果:
// pass
// start

【 小贴士 】其实工作中经常会用到这个特性,比如 then 链默认不写第二个函数,从而使用 catch 在 then 链末尾单独对错误进行处理,当然 catch 的返回值默认也是 fulfilled 状态哟。

【拓展】 finally 效果与“ then 链透传”一致,仅多了内部函数执行,感兴趣的可自行尝试一下~

案例深入

嵌套 Promise , then 链返回非 Promise 结果

刚刚聊的都是较为基础的例子,想要捋清 Promise 的知识可能还需要一些稍有难度的案例,这样才能加深对它的印象。那么看一下这个嵌套 Promise 的例子,弄懂了它,基本上就能对 Promise 有一定的理解了,不会被大多数的面试题给难住:

Promise.resolve()
  .then(() => {
    console.log('1-1');

    new Promise((res, rej) => {
      console.log('1-2');
      res();
    })
      .then(() => {
        console.log('1-3');
      })
      .then(() => {
        console.log('1-4');
      });

    return 'Anything except promise func, also no "return"';
  })
  .then(() => {
    console.log('2-1');
  });

这里注意到,我们在第一个 then 链内调用了一个新的 Promise 方法,但没有返回值,那么运行的结果会是顺序执行吗?在浏览器上运行得到的结果为:

1-1
1-2
1-3
2-1
1-4

可以看到 “2-1” 在 “1-4” 之前打印了,可这又是为什么呢?让我们一步步分析来看:

  • 【Step1】 then 链先打印 1-1 ,然后进入 Promise 函数内;
  • 【Step2】打印 Promise 内部的 1-2 ,执行 resolve 函数,并将紧接着的 then 链存入 微任务事件队列 ;
  • 【Step3】跳出 Promise 并运行至 then 链结尾,无返回值,默认 resolve 处理 并将下一个 then 链存入微任务队列 
  • 【Step4】此时微任务队列有俩微任务,依次执行处理,后续内容就是依次打印 1-3 和 2-1 ,最后再打印 1-4 了;

微任务队列内容变化如下:

/** 每步微任务队列内容 */
【Step 1】
[
  then(() => {
    console.log('1-1');
    new Promise((res, rej) => {
      console.log('1-2');
      res();
    })
      .then(() => {
        console.log('1-3');
      })
      .then(() => {
        console.log('1-4');
      });
  }).then(() => {
    console.log('2-1');
  }),
]

【Step2】
[
  (then(() => {
    console.log('1-3');
  }).then(() => {
    console.log('1-4');
  }),
  then(() => {
    console.log('2-1');
  }))
]

【Step3】
[
  then(() => {
    console.log('1-4');
  })
];

从此结果我们可以了解到,在无返回值的情况下最好不要在内部处理别的 Promise 函数并接上 then 链来达到步骤控制,很容易会出现一些意想不到的问题。在业务中,很容易就会写出这样的代码,如:

// 加入 http 是封装好的 Axios 文件
const initFunc = async () => {
  // 等初始化
  await http.post('getInfo').then(info => {
    const { userId, userRightIds } = info;

    // 获取用户信息
    http.post('getUserInfo', { userId }).then(() => {
      /* 处理用户信息 */
    });

    // 获取用户信息
    http.post('getRightList', { userRightIds }).then(() => {
      /* 处理权限信息 */
    });
  });

  // 初始化后处理别的事
  // ...(电脑前,为什么没拿到权限数据和用户信息???弄个 setTimeout 吧)}
};

模拟请求的代码如下:

async function test() {
  let testVal = 1;

  await Promise.resolve().then(() => {
    // 加长 then 链模拟请求的耗时
    Promise.resolve()
      .then()
      .then()
      .then()
      .then()
      .then()
      .then(() => {
        testVal = 2;
      });
  });

  console.log(testVal);
}

test();
// 结果是:1

所以,业务代码内尽量不要嵌套写 Promise ,用 async/await 拆拆,或者每个内部 Promise 函数加上对应的 async/await 方法令其执行完再做其它操作,这样就不会写出 bug 了。

嵌套 Promise , then 链返回 Promise 结果

当然,嵌套 Promise 也是有坑存在的,说不定那天面试也会面到,当然项目内是不会出现这种写法的(有的话那么需要问问是谁面他进来的,同时还得考虑考虑是否继续和他共事),返回 Promise 结果的例子咱先不看,先分解一下,看如下例子:

Promise.resolve()
  .then(() => {
    console.log('1-1');
  })
  .then(() => {
    console.log('1-2');
  })
  .then(() => {
    console.log('1-3');
  });

Promise.resolve()
  .then(() => {
    console.log('2-1');
  })
  .then(() => {
    console.log('2-2');
  })
  .then(() => {
    console.log('2-3');
  }); // 浏览器结果为(就不换行输出了):// 1-1   2-1   1-2   2-2   1-3   2-3

这结果想必难不倒大家,可以很轻松的解答出来,那么再代入嵌套的例子看看,应该就好分析了:

Promise.resolve()
  .then(() => {
    // 别名: then1
    Promise.resolve()
      .then(() => {
        console.log('1-1');
      })
      .then(() => {
        console.log('1-2');
      })
      .then(() => {
        console.log('1-3');
      });
    // 别名 then2
    return Promise.resolve()
      .then(() => {
        console.log('2-1');
      })
      .then(() => {
        console.log('2-2');
      })
      .then(() => {
        console.log('2-3');
      });
  })
  // 别名 then3
  .then(() => {
    console.log('3-1');
  });

// 浏览器结果为(就不换行输出了):
// 1-1   2-1   1-2   2-2   1-3   2-3    3-1

因为 then3 依赖 then2 的状态改变,而 then2 又需要和 then1 抢 微任务队列 的资源,因此返回的结果就是交替的结果, 3-1 最后打印。

但是,我们把 then2 链精简一下,只留一个 2-1 ,按道理来说 3-1 应该在 1-2 之后打印的,但实际结果如下:

Promise.resolve()
  .then(() => {
    // 别名: then1
    Promise.resolve()
      .then(() => {
        console.log('1-1');
      })
      .then(() => {
        console.log('1-2');
      })
      .then(() => {
        console.log('1-3');
      });
    // 别名 then2
    return Promise.resolve().then(() => {
      console.log('2-1');
    });
  })
  // 别名 then3
  .then(() => {
    console.log('3-1');
  });

// 浏览器结果为(就不换行输出了):
// 1-1   2-1   1-2   1-3   3-1

为什么和预测结果不一样了呢?其实此问题的问题点并不在 Promise 身上,听我一一分析。

首先,在 then2 打印 2-1 后,其会返回一个新的 Promise 对象放入 微任务队列 ,它的返回值为 undefined

然后 then1 打印 1-2 并将 1-3 所在 then 链传入

紧接着这个返回值为 undefiend 的 Promise 对象执行,然后返回一个 undefiend 值,这就回到上面返回值为非 Promise 的情形了,紧接着 then3 就放入了 微任务队列

在看微任务队列,发现 then3 就在 then1 那最后一个链之后,那结果就呼之欲出了。

代码解如下:

【Step1】
[
  // then1
  then(() => { console.log('1-1'); })
    .then(() => { console.log('1-2'); })
    .then(() => { console.log('1-3'); }),
  // then2 的 then 链会包裹 Promise 对象,因为 then 链不论 return 的是什么,它都会返回一个 Promise
  Promise.resolve(then(() => { console.log('2-1'); }))
]
【Step2】
[
  // then1
  then(() => { console.log('1-2'); })
    .then(() => { console.log('1-3'); }),
  // then2
  Promise.resolve(undefined)
]
【Step3】
[
  then(() => { console.log('1-3'); }),
  then(() => { console.log('3-1'); })
]

此需要注意的是, then 链处理返回结果为 Promise 类型时,其会多一个 Promise.resolve(undefined) 的隐藏链。

当然,如果 then2 直接返回一个 Promise.resolve() 其结果仍然是一样的,这就涉及到任务队列的插队问题了,因为我们返回的是一个新的 Promise 对象,这和原来的 Promise 链还是有一点区别的,这里只需要记住,最多差两条链的结果就行,没有深究的必要(实际项目也不会遇到这个问题)

原文 https://kazehaiya.github.io/2021/09/09/深入理解-Promise/


链接: https://www.fly63.com/article/detial/10999

你真的了解 Promise 吗?Promise 必知必会(十道题)

Promise 想必大家十分熟悉,想想就那么几个 api,可是你真的了解 Promise 吗?本文根据 Promise 的一些知识点总结了十道题,看看你能做对几道。

剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,Promise标准中仅指定了Promise对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定.

Async/Await替代Promise的6个理由

Async/Await替代Promise的6个理由:Async/Await是近年来JavaScript添加的最革命性的的特性之一。它会让你发现Promise的语法有多糟糕,而且提供了一个直观的替代方法。

Promise 原理解析与实现(遵循Promise/A+规范)

Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一,Promise 是一个构造函数, new Promise 返回一个 promise对象 接收一个excutor执行函数作为参数

简单模仿实现 Promise 的异步模式

这篇文章是考虑如何自己实现一个简单 Promise,用以理解 Promise。和原生 Promise的调用方法一样,支持链式调用,本文实现的方法只能用于参考Promise的原理,还有很多特性没有实现,比如 race,all 方法的实现。

数组的遍历你都会用了,那Promise版本的呢

在对数组进行一些遍历操作时,发现有些遍历方法对Promise的反馈并不是我们想要的结果。async/await为Promise的语法糖,文中会直接使用async/await替换Promise;map可以说是对Promise最友好的一个函数了,

Promise使用时应注意的问题

最近在使用axios库时遇到了个问题,后端接口报了500错误,但前端并未捕获到。在axios整体配置的代码中,过滤http code时,调用了filter401()、filter500(),但是这里注意并未将两个filter函数的结果返回,也就是并未返回promise,这就是导致问题出现的原因

es6 Promise 的基础用法

想必接触过Node的人都知道,Node是以异步(Async)回调著称的,其异步性提高了程序的执行效率,但同时也减少了程序的可读性。如果我们有几个异步操作,并且后一个操作需要前一个操作返回的数据才能执行

关于 Promise 的 9 个提示

你可以在 .then 里面 return 一个 Promise,每次执行 .then 的时候都会自动创建一个新的 Promise,对调用者来说,Promise 的 resolved/rejected 状态是唯一的,Promise 构造函数不是解决方案,使用 Promise.resolve

手写一款符合Promise/A+规范的Promise

Promise的一些用法在此不多赘述,本篇主要带领你手写一个Promise源码,学完你就会发现:Promise没有你想象中的那么难.本篇大概分为以下步骤:实现简单的同步Promise、增加异步功能、增加链式调用then、增加catch finally方法、增加all race 等方法、实现一个promise的延迟对象defer、最终测试

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!