Promise 窥探

238 阅读10分钟

简介

Promise是ES6新增的内置类:承诺模式,主要用来规划异步编程的。

回调地狱

回调函数嵌套回调函数

什么是回调函数?

把一个函数作为值,传递给另外一个函数,在另外一个函数执行的时候,再把传递进来的函数进行处理。(包括调用、绑定this等)

实现一个forEach

这块为什么写forEach呢?是以为我们调用他的时候也传递了一个函数当做参数

Function.prototype.forEach = function(callback, ctx) {
    let arr = this
    for(let i = 0 ; i < arr.length ; i++) {
        callback && callback.call(ctx, arr[i], i, arr)
    }
}

[1,2,3,4].forEach((item, index, arr)=>{
    console.log(item, index, arr, this)
}, {})

回调函数的方式

// 假设我们现在有三个接口,我们还在用JQ中的ajax
// 并且这三个请求是相互依赖的,也就是串行的请求。
// 那么我们要依次执行他们
  $ajax({
    url: "1.txt",
    success: function (res) {
      $ajax({
        url: "2.txt",
        success: function (res) {
          $ajax({
            url: "3.txt",
            success: function (res) {
                // 最终的结果
                console.log(res)
            },
          });
        },
      });
    },
  });

promise的方式解决回调地狱

  function fn1() {
    return new Promise((resolve, reject) => {
      $ajax({
        url: "1.txt",
        success: function (res) {
          // 成功的结果 调用 promise 提供的 resolve方法
          resolve(res);
        },
      });
    });
  }
  function fn2() {
    return new Promise((resolve, reject) => {
      $ajax({
        url: "2.txt",
        success: function (res) {
          // 成功的结果 调用 promise 提供的 resolve方法
          resolve(res);
        },
      });
    });
  }
  function fn3() {
    return new Promise((resolve, reject) => {
      $ajax({
        url: "3.txt",
        success: function (res) {
          // 成功的结果 调用 promise 提供的 resolve方法
          resolve(res);
        },
      });
    });
  }

  // 使用了 promise 之后就可以用 .then 的形式去拿到resolve的参数
  // 既成功的结果了
  fn1()
    .then((res) => {
      return fn2(res);
    })
    .then((res) => {
      return fn3(res);
    })
    .then((res) => {
      // 最终的结果
      console.log(res);
    });

promise + async/await

(async function(){
    let result = await fn()
    result = await fn(result)
    result = await fn(result)
    console.log(result)
})()

Promise API 探究

executor 函数

let p = new Promise([executor])

// Promise 是一个内置类
// p 是类的实例
// executor 是回调函数(必须要传递)

看以下代码的执行顺序

let p = new Promise(()=>{
    console.log(1)
})
console.log(2)

1
2
  1. 这样我们可以看出来executor函数是立即执行的,Promise本身是同步的(它只是用来管理异步编程的)。
  2. 会给executor传递两个实参resolve函数执行会把promise实例改为成功。reject函数执行会把promise状态改成失败状态。传递的值就是promiseResult
  3. 创建了一个promise实例。

Promise 实例

接下来我们再打印一下p这个实例,看看实例上面都有什么东西。

[[Prototype]]: Promise
[[PromiseState]]: "pending"
[[PromiseResult]]: undefined

我们先忽略掉[[Prototype]]其实他也是__proto__ 先来看其他两个

PromiseState   promise状态
    pending 初始状态
    fulfilled 成功状态
    rejected  失败状态
PromiseResult  promise结果/值
    初始值是 undefined
        不论成功还是失败,成功的结果或者失败的原因都会赋值给这个值
    

PromiseState

  1. 状态改变成功就不能再改变了。
let p = new Promise((resolve, reject)=>{
    resolve(1)  // 状态改变之后就不能再改变了。
    reject(100)
})
console.log(p)

p的状态还是fulfilled

then 方法 (重点)

  1. 可以在then方法调用的时候传入两个函数,并且在内部可以拿到resolve或者reject的值。
  2. 执行.then方法会把成功失败的回调函数先存起来。
  let p = new Promise((resolve, reject) => {
    setTimeout(() => {
      let result = Math.random();
      result > 0.5 ? resolve(result) : reject(result);
    }, 1000);
  });

  p.then(
    (res) => {
      console.log(`成功了${res}`);
    },
    (reason) => {
      console.log(`失败了${reason}`);
    }
  );

约定一下 then的第一个参数 称为A 第二个参数称为 B

  1. then()方法返回一个新的 Promise
  2. p2的状态由p1 then 存放的两个函数决定。
  3. p2的结果由p1 then 存放的两个函数执行的返回值决定
  • 不论是A执行还是B执行,只要执行不报错,则p2的状态都是成功,反之报错就是失败(A、B函数返回的都是普通值 .then 执行结果)。

  • p1的成功和失败也会收到executor函数执行是否报错的影响,如果报错了,则p1的状态就是失败的,promiseResult的值就是失败原因,如果执行不报错,再看执行的是 resolve/reject这两个中的哪一个(new Promise 执行结果)。

  1. new Promise 执行结果和 .then()执行出来返回的promise实例的结果是不一样的 (第6、7条)。
  2. 特殊情况:如果A、B返回的都是一个新的Promsie实例,则返回的promise实例的成功失败以及结果,直接决定p2的状态和结果。
var p1 = new Promise((resolve, reject)=>{
    resolve('hi')
})

var p2 = p1.then(res => {
    console.log(`成功了 -> ${res}`)
    return res+ '$$'
}, reason => {
    console.log(`失败了 -> ${reason}`)
    return reason + '$$'
})
console.log(p2)

会返回一个新的 promsie 实例

[[Prototype]]: Promise
[[PromiseState]]"fulfilled"
[[PromiseResult]]"hi$$"
  1. then([A], [B]):如果第一个函数没有传递,则会“顺延”(成功顺延)
  • [A]没有传递:则找下一个 then 中的 A 函数
  • [B]没有传递:就找下一个 then 中的 B 函数
Promise.resolve(10).then(null, reason => {
    console.log(`失败了  === ${reason}`)   
})
.then(res => {
    console.log(`成功了 ==== ${res}`)
    return res * 10 
}, reason => {
    console.log(`失败了 === ${reason}`)
    return reason / 10
})
  1. (失败顺延)
Promise.reject(10).then(null, null)
.then(res => {
    console.log(`成功了 ==== ${res}`)
    return res * 10 
}, reason => {
    console.log(`失败了 === ${reason}`)
    return reason / 10
})

Promise异步流程

从下面这段代码结合Event loop进行分析一下。

let p = new Promise((resolve, reject)=>{
  resolve('ok')
})
.then((res)=>{console.log(res)}, (reason)=>{
  console.log(reason)
})

promise.drawio.png

catch

  1. catch相当于then(null, reason => {...})

这两个是等价的。

Promise.reject(10).then(res => {
    console.log(`成功了 ${res}`)
    return res * 10
}).catch(reason => {
    console.log(`失败了 ${reason}`)
    return reason * 10
})


Promise.reject(10).then(res => {
    console.log(`成功了 ${res}`)
    return res * 10
}).then(null,reason => {
    console.log(`失败了 ${reason}`)
    return reason * 10
})

all

管理多个promise实例,当所有实例都成功,Promise.all返回的总实例才是成功(结果是一个数组,按照顺序依次存储了每个实例执行成功的结果)

race

管理多个promise实例,看所有实例谁先有结果,无论失败还是成功,总实例的结果以最快的结果为准。

做题

then 参数全部返回正常值

new Promise((resolve, reject)=>{
    resolve(10)
    reject(20)
})
.then(res=>{
    console.log(`成功了 --- ${res}`)
    return res * 10
},reason => {
    console.log(`失败了 --- ${reason}`)
    return reason / 10
})
.then(res=>{
    console.log(`成功了 --- ${res}`)
    return res * 10
},reason => {
    console.log(`失败了 --- ${reason}`)
    return reason / 10
})
.then(res=>{
    console.log(`成功了 --- ${res}`)
    return res * 10
},reason => {
    console.log(`失败了 --- ${reason}`)
    return reason / 10
})

这里A B 返回的是一个普通值。如果不报错就走到下一个then的成功的回调函数里面去了。

成功了 --- 10
成功了 --- 100
成功了 --- 1000

then参数返回一个新的promise

当前promise的结果直接由这个新的promise决定。

new Promise((resolve, reject)=>{
    // resolve(10)
    reject(20)
})
.then(res=>{
    console.log(`成功了 --- ${res}`)
    return res * 10
},reason => {
    console.log(`失败了 --- ${reason}`)
    return reason / 10
})
.then(res=>{
    console.log(`成功了 --- ${res}`)
    return Promise.reject(res * 10)
},reason => {
    console.log(`失败了 --- ${reason}`)
    return reason / 10
})
.then(res=>{
    console.log(`成功了 --- ${res}`)
    return res * 10
},reason => {
    console.log(`失败了 --- ${reason}`)
    return reason / 10
})
失败了 --- 20
成功了 --- 2
失败了 --- 20

catch 用法

  1. then 只处理成功,catch处理失败(一般写在最后面)
  2. 返回失败状态的实例,没有做 catch 处理的会在控制台报错(但是不会影响其他的代码执行)
Promise.reject(10).then(res => {
    console.log(`成功了 ${res}`)
    return res * 10
}).then(res => {
    console.log(`成功了 ${res}`)
    return res * 10
}).catch(reason => {
    console.log(`失败了 ${reason}`)
    return reason * 10
})

await 后面的代码放到微任务中

async function async1() {
    console.log('async1 start')
    await async2()
    // 执行async 函数,并且把后面的代码放到微任务中
    // console.log('async1 end') 这段代码被放到了微任务中
    console.log('async1 end')  
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(() => {
    console.log('setTimeout')
}, (0));

async1()
new Promise((resolve, reject)=>{
    console.log('promise1')
    resolve()
}).then(()=>{
    console.log('promise2')
})
console.log('script end')
script start       
async1 start
async2
promise1  
script end
async1 end
promise2
setTimeout

async/await (重点)

  1. async修饰一个函数:保证函数返回的是一个promise实例
  2. .then很相似、函数执行不报错,promise实例的状态就是成功的,执行报错的话,promise状态就是失败的。
  3. return 的值或者报错的原因就是 promise实例的结果。
  4. 如果return的是一个新的promise实例,则新实例的结果影响promise的结果。
  5. async最常见的应用,就是修饰函数。让函数中可以使用await(想要使用await,所在的函数必须使用async进行修饰)。
  6. await 后面一般跟promise实例,等待promise状态改变之后在执行下面的代码。
function fn() {
    return new Promise((resolve, reject)=>{
        setTimeout(() => {
            console.log('异步处理完成了')
            resolve('ok')
        }, 2000);
    })
}

async function func() {
    await fn()

    console.log('start')
}

func()
'异步处理完成了'
'start'
  1. await后面的promsie如果是失败的,则当前函数中的await后面的代码都不会执行了。
(async function(){
    let x = await Promise.reject(10)
    console.log('hhhhh')   // 这里不会再执行了
})()

console.log('kkkkkkk')
  1. 因为以上情况,所以要使用try catchawait进行包裹。
(async function(){
    try{
        let x = await Promise.reject(10)
    }catch(err){
        console.log(err)
    }
    console.log('hhhhh')   // 这块还会执行
})()

console.log('kkkkkkk')
kkkkkkk
10
hhhhh
  1. 遇到await 先执行后面的函数,返回结果当做promise实例放在await后面。
  • 不论promise实例成功还是失败。先把await下面的代码放在的微任务队列中,继续执行同步操作。
  • 当js渲染线程把同步任务都执行完成之后,并且也知道await后面的promise是成功的状态,才会把之前存放的微任务拿来执行
  1. 宏任务执行顺序是:谁先到时间谁先执行。微任务执行顺序是:谁先放到微任务队列里面谁先执行。

实现Promise深入理解

根据上面学到的东西,我们很快的就写下了以下代码。

初略版本

function MyPromise(executor) {
    if (typeof executor !== "function")
      throw new TypeError("参数必须是一个函数!");
    const PENDING = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED = "rejected";
    // 1. 检验 executor 参数类型
    // 2. 先调用 executor 函数
    // 3. 定义 executor 函数的两个参数   resolve reject
    // 4. 初始化状态和结果
    // 5. then 的时候可以在成功和失败的函数中拿到 promise 实例的结果
    this.promiseStatus = PENDING;
    this.promiseValue = undefined;

    // 成功状态改变的时候 调用的函数 相当于 then((res)=>{}, (reason)=>{}) 函数的第一个实参
    this.resolveFunc = () => {};
    // 失败状态改变的时候 调用的函数  相当于 then() 的第二个参数。
    this.rejectFunc = () => {};

    // 调用 resolve 或者 reject 函数都会改变自身实例的 结果和状态
    // 所以我们写一个 专门用来改变状态的函数
    var change = (status, value) => {
      // 状态只能是 pending 的时候才能进来
      // 意味着状态只能改变一次
      if (this.promiseStatus !== PENDING) return;
      this.promiseStatus = status;
      this.promiseValue = value;
      // 根据不同的状态调用不同的处理函数
      status === FULFILLED
        ? this.resolveFunc(this.promiseValue)
        : this.rejectFunc(this.promiseValue);
    };
    var resolve = (value) => {
      change(FULFILLED, value);
    };
    var reject = (value) => {
      change(REJECTED, value);
    };
    // executor 函数执行报错的话直接调用 reject
    try {
      executor(resolve, reject);
    } catch (err) {
      change(REJECTED, err);
    }
  }

  MyPromise.prototype.then = function (resolveFunc, rejectFunc) {
    this.resolveFunc = resolveFunc;
    this.rejectFunc = rejectFunc;
  };

好的我们来试一试,自己写的代码。如下,可以看见在控制台并没有输出ok, 现在我们来想一想为什么没有输出呢?我们很快的发现在调用executor函数的时候,我们.then()函数还没有调用,当然里面传递的成功的回调失败的回调都还没有注册到this.resolveFuncthis.rejectFunc上面去。所以在 调用resolve()的时候不会触发.then()里面注册的成功的回调函数。下面我们想办法解决他。

  let p = new MyPromise((resolve, reject)=>{
      resolve('ok')
  });

  p.then(
    (res) => {
      console.log(res);
    },
    (reason) => {
      console.log(reason);
    }
  );

我们的解决方案是让在change函数里面把调用注册then成功和失败的方法异步执行

支持.then中进行回调

var change = (status, value) => {
  // 状态只能是 pending 的时候才能进来
  // 意味着状态只能改变一次
  if (this.promiseStatus !== PENDING) return;
  this.promiseStatus = status;
  this.promiseValue = value;
  // 根据不同的状态调用不同的处理函数
  // 由于在执行 executor 函数的时候 内部调用了 resolve更改status和value是同步的
  // 但是执行 通知方法要的是异步的(因为此时.then()后面的方法还没执行回调函数还没注册好)
  // 所以我们用srtTimeout() 把执行 then里面的方法异步执行
  setTimeout(() => {
    status === FULFILLED
    ? this.resolveFunc(this.promiseValue)
    : this.rejectFunc(this.promiseValue);
  }, 0);
};

两个静态方法

下面再扩展两个静态方法es5中就是属性是一个函数

  MyPromise.resolve = function (result) {
    return new MyPromise((resolve) => {
      resolve(result);
    });
  };

  MyPromise.reject = function (reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  };

then 成功失败顺延

  MyPromise.prototype.then = function (resolveFunc, rejectFunc) {
    // 不传递参数的话可以顺延
    if (typeof resolveFunc !== "function") {
      // 默认返回一个成功的 promise
      resolveFunc = (result) => {
        return new MyPromise.resolve(result);
      };
    }

    if(typeof rejectFunc !== 'function') {
        rejectFunc = (reason) => {
            // 默认返回一个失败的 promise
            return new MyPromise.reject(reason)
        }
    }
    this.resolveFunc = resolveFunc;
    this.rejectFunc = rejectFunc;
  };

then的链式调用 (难点)

分析一下。

  1. 每次执行then方法都会返回一个新的promise实例
  2. 构造新promise实例executor函数的resolvereject方法控制返回实例的状态和结果。
  3. 需要在新的promise实例的resolve或者reject调用的时候拿到上一个promise返回的结果。 下面我们就来写一下,主要更改then后面的逻辑
// this.resolveFunc = resolveFunc;
// this.rejectFunc = rejectFunc;
return new MyPromise((resolve, reject)=>{
    // 把之前的函数包一层,在内部进行调用,
    // 1. 拿到 上一个 then的返回结果
    // 2. 检测上一个 then的回调函数是否报错
    this.resolveFunc = (result) =>{
        try{
            // 如果执行不报错,则返回的实例是成功的(特殊情况:then A 方法也返回的是一个promise)
            // 则由返回的promise实例决定这个实例的状态
            let x =  resolveFunc(result)
            x instanceof MyPromise ? 
            x.then(resolve, reject) : resolve(x)
        }catch(err){
            // 如果执行报错则这个实例的状态就失败
            reject(err)
        }
    }

    this.rejectFunc = (reason) =>{
        try{
            let x =  rejectFunc(reason)
            x instanceof MyPromise ? 
            x.then(resolve, reject) : resolve(x)
        }catch(err){
            reject(err)
        }
    }

promise.catch

相当于then不传第一个参数

MyPromise.prototype.catch = function (rejectFunc) {
  return this.then(null, rejectFunc)
}

promise.all

要点:

  1. 对数组中的 item 进行类型检测,如果是promsie的实例,就执行then取结果,如果不是就直接把 item放在结果对应的位置上面。
  2. 进行计数,并且每次放完结果的时候index++,表示已经处理完数组中的一个item了,当 index >= 传入数组的长度的时候就代表整个promise数组执行完成了,就可以调用resolve把整个结果fire(抛出去了)。当发现有一个失败的就直接reject掉。
MyPromise.all = function (promiseArr) {
return new MyPromise((resolve, reject) => {
  let index = 0,
    results = [];
  for (let i = 0; i < promiseArr.length; i++) {
    // 如果index >= 数组的长度 就抛出最终的结果
    let fire = () => {
      if (index >= promiseArr.length) {
        resolve(results);
        return;
      }
    };
    // 先看每个item是不是一个 promise ,如果不是的话就把他放在对应的结果上面
    let item = promiseArr[i];
    // 不是promise 就直接设置返回结果
    if (!(item instanceof MyPromise)) {
      results[i] = item;
      index++;
      fire()
      continue;
    }
    // 如果是promise 就执行
    item
      .then((res) => {
        results[i] = res;
        index++;
        fire()
      })
      .catch((reason) => {
        reject(reason);
      });
  }
});
}