1.0 Promise-止水(Promise+new操作符+Event Loop浅析)

发布于 2022年 02月 16日 18:47

1.稍微了解一下promise

Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。

2.为什么需要promise

JS作为一门单线程语言在特殊的情况下我们只能通过回调函数进行异步操作,所以我们就引入了,Promise

3.最常见的异步

ajax请求、stetimeOut

4.什么是异步?什么是同步?

同步:一定要等任务执行完了,得到结果,才执行下一个任务。 异步:不等任务执行完,直接执行下一个任务。

Promise创建

想要创建一个promise的对象,那我们就是需要使用 new 来调用 Promise的构造器来进行实例化 栗子

const promise = new Promise((resolve, reject) =>{
  // 异步处理
  // 处理结束后、调用resolve 或 reject
  if (req.status === 200) { 
        resolve('成功');
     } else {
        reject('失败');
     } 
   };
})

在我们使用promise的时候需要会有三个状态

  • 等待(Pending)
  • 成功(Fulfilled)
  • 失败(Rejected)

这三个状态让我们在调用Promise的异步方法的时候,逻辑变的更加简单

ps: 当我们在Promise的首层函数出现throw语句的时候,Promise会直接进入失败状态哦!

new Promise((resolve,reject)=>{
  throw new Error('test')
})

顺便一提: new 操作符到底做了什么?

根据平常的使用经验,我们使用new操作符的时候应该是

  1. 创建一个空的js对象
  2. 修改空对象的原型prototype并让它指向构造函数的原型
  3. 将空对象作为构造函数的上下文(改变this指向)

将新创建的对象作为this的上下文 对构造函数的返回值进行判断,如果该函数没有返回对象,则返回this 写个简单的栗子

function create(Con, ...args){
  // 创建一个空的对象
  let  obj = Object.create(null);
  // 将空对象指向构造函数的原型链
  Object.setPrototypeOf(obj, Con.prototype);
  // obj绑定到构造函数上,便可以访问构造函数中的属性,即obj.Con(args)
  let result = Con.apply(obj, args);
  // 如果返回的result是一个对象则返回
  // new方法失效,否则返回obj
  return result instanceof Object ? result : obj;
}

测试

function company(name, address) {
    this.name = name;
    this.address = address;
  }

使用

var company1 = create(company, 'yideng', 'beijing');
console.log('company1: ', company1);

Promise.prototype.then方法:链式操作

栗子1号

var p1 = new Promise(function(resolve, reject){
  // ... some code
resolve(1)
});

p1.then(() =>{ console.log(1)}).then(() =>{console.log(2)}) // 1 2


Promise.resolve 方法,Promise.reject 方法
// resolve
var p = Promise.resolve('Hello');

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

// reject 
var p1 = Promise.reject('出错了');
p1.then(null, function (s){
  console.log(s)
});

栗子2号

var a = new Promise((resolve,reject) =>{ resolve(1)})
var a1 = new Promise((resolve,reject) =>{ resolve(2)})
var a3 = new Promise((resolve,reject) =>{ resolve(3)})
a.then((val) =>{
   console.log(val)
   return a1
}).then((va) =>{
  console.log(va)
  return a3
}).then((v) =>{
  console.log(v)
})
// 输出 1,2,3

Promise.resolve 方法,Promise.reject 方法

// resolve
var p = Promise.resolve('Hello');

p.then((s) =>{
  console.log(s)
})
// 输出 Hello

// reject 
var p1 = Promise.reject('出错了');
p1.then(null, function (s){
  console.log(s)
});
// 输出 出错了

Promise.all的用法

该方法传入一个可迭代的数组,并返回一个promise对象,该对象在数组内所有的Promise对象执行完毕后激活

ps:如果数组内全部执行成功则返回的对象也进入成功的状态,如果有一个执行失败就进入失败状态

栗子:全部成功
var a = new Promise((resolve,reject) =>{ resolve(1)})
var a1 = new Promise((resolve,reject) =>{ resolve(2)})
var a3 = new Promise((resolve,reject) =>{ resolve(3)})
Promise.all([a,a1,a3]).then(value =>{
    console.log(value)
})
// 输出 [1, 2, 3]
栗子: 部分成功
var a = new Promise((resolve,reject) =>{ resolve(1)})
var a1 = new Promise((resolve,reject) =>{ resolve(2)})
var a3 = new Promise((resolve,reject) =>{ reject(3)})
Promise.all([a,a1,a3]).then(value =>{
    console.log(value)
}).catch(err =>{
    console.log(err)
})
// 输出: 3

Promise.race的用法

该方法传入一个可迭代的数组,并返回一个promise对象,该对象在数组内有第一个Promise对象执行完毕后,该方法会根据这第一个卧槽的对象的状态而改变

var a = new Promise((resolve,reject) =>{ resolve(1)})
var a1 = new Promise((resolve,reject) =>{ resolve(2)})
var a3 = new Promise((resolve,reject) =>{ reject(3)})
Promise.race([a,a1,a3]).then(value =>{
    console.log(value)
}).catch(err =>{
    console.log(err)
})
// 输出: 1

新增 es2021的 Promise.any

Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和AggregateError类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起。本质上,这个方法和Promise.all()是相反的。

例子:
const pErr = new Promise((resolve, reject) => {
  reject("总是失败");
});

const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最终完成");
});

const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
});

Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);
  // pFast fulfils first
})
// 期望输出: "很快完成"

Promise和 steTimeOut 的执行顺序 让我们看一下下面的代码

setTimeout(function(){
 console.log('定时器开始啦') 
}); 
new Promise(function(resolve){
 console.log('马上执行for循环啦'); 
 for(var i = 0; i < 10000; i++){
   i == 99 && resolve(); 
  } 
 }).then(function(){ 
  console.log('执行then函数啦') 
});
console.log('console执行结束');

一看我就很自信的输出如下

console执行结束
马上执行for循环啦
执行then函数啦
定时器开始啦

但是事实是:

马上执行for循环啦
console执行结束
执行then函数啦
定时器开始啦

为什么这样输出呢?

首先,代码执行的遇到第一个 代码块,setTimeout,setTimeout被推入 Event Table 中 然后,我们会执行之后的代码 new Promise模块,在我们执行promise的回调也就是 .then函数之前,我们会先执行

console.log('马上执行for循环啦'); 
for(var i = 0; i < 10000; i++){
   i == 99 && resolve();
} 

这些代码。 其次,我们会执行for循环,当 i == 99 的时候,我们就会执行 resolve() 将回调函数 .then 推入 Event Queue 中, 这时候我们就会执行 console.log('console执行结束');

这行代码。 执行完毕后,我们会发现主线程没有任务需要我们进行执行了,会去 Event Queue 读取对应的函数,进入主线程执行 上述过程会不断重复,也就是常说的Event Loop(事件循环)。 那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

理解一下 setTimeOut 对于 setTimeOut 大家对他的第一印象就是异步可以延时执行, 但是,在我们看了之前的代码之后就不能这样理解了

such assetTimeout(() => {
   console.log(1)
},3000)

sleep(8000)
// 执行 结果
// ... 2000 years later( 8s later)
// 1

给大家解释一下 在我们执行了 setTimeout之后,我们就会将 setTimeout 推入 Event Table 中, 然后我们会开始 执行 sleep 函数, 可是这个时候sleep还在执行,只能继续等待, 再过3s之后,setTimeout的计时结束, console.log(1) 被推入 Event Queue,但是sleep还在执行,只能等着 sleep执行完毕之后, console.log(1) 从 Event Queue 进入主线程

ps: 我们还经常遇到 setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢? 答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。 ps: HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

进入正题 除了广义的同步任务和异步任务之外,我们还有更精细的定义: 宏任务和微任务:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick
  • ES6 规范中,microtask 称为 jobs,macrotask 称为 task,宏任务是由宿主发起的,而微任务由JavaScript自身发起

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

来个栗子
setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
  • 首先代码由上至下进行执行
  1. 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
  2. 接下来遇到了Promise,
  3. new Promise属于主线程任务直接执行打印,然后发现 then函数,这是属于微任务的,分发到微任务Event Queue。
  4. 遇到console.log(),属于主线程任务直接执行打印。
  5. 主线程中任务执行完后,就要执行分发到微任务Event Queue中代码,实行先进先出,我们发现了then在微任务Event Queue里面,执行。
  6. ok,微任务Event Queue中代码执行完,就执行宏任务Event Queue中代码,也是先进先出。
  7. 我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  8. 结束。
  • 所以输出结果应该是
- // promise
- // console 
- // then 
- // setTimeout

事件循环,宏任务,微任务的关系如图所示:

推荐文章