js三座大山之异步三promise本质

js三座大山

一:函数式编程
js三座大山之函数1
js三座大山之函数2-作用域与动态this

二:面向对象编程
js三座大山之对象,继承,类,原型链

三:异步编程:
js三座大山之异步一单线程,event loope,宏任务&微任务
js三座大山之异步二异步方案
js三座大山之异步三promise本质

当前主流的js异步编程方案中 都是基于promise的。promise最大的优点就是统一了异步调用的api,所有异步操作都可以使用相同的模式来处理问题。使用链式调用,避免了回调地狱。本质上和回调的方案并没有区别。promise本质是一个存储回调的容器js三座大山之异步二异步方案 中的基于事件的发布订阅有点类似。

关于promise的基础使用请看
阮一峰promise
mdn-Promise

用法范式:

promise的用法可以分为两大步骤。

  1. 创建promise对象并传入执行器&同步运行执行器
ini 复制代码
const execute = (onResolve, onReject) => {
  setTimeout(() => {
    onResolve(123);
  }, 2000);
};
const p = new Promise(execute);
  1. 注册异步回调函数,等待执行器返回结果,调用注册的函数。
javascript 复制代码
// 成功的回调
p.then((data)=>{
  console.log('第一个成功的异步回调 异步结果:', data)
})
// 失败的回调
p.catch((reason)=>{
  console.log('第一个失败的异步回调 异步结果:', reason)
})
// 成功|失败都被执行的回调
p.finally(()=>{
  console.log('第一个finally异步回调')
})

容器结构:promise本质是一个存储回调的容器

那么这容器里面都有什么呐?

1.状态state

promsie内部维护了一个状态 可以从pendding->fulfilled || pendding->rejected。仅有这两种变化。且一旦状态改变,就不会再变,任何时候都可以得到这个结果。

这一点promise与基于事件的发布订阅不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

2.执行结果value

当执行器执行成功后需要设置结果 执行失败后需要设置失败原因 这都是运行的结果被保存在promise内部。

3.回调函数列表handles

有成功的回调函数 通过then,finally注册的 可以是多个

有失败的回调函数 通过then,catch,finally注册的 可以是多个

4.钩子函数onResolve/onReject

外部在promise构造函数中需要注入一个执行器 promise需要对外提供一个钩子 当执行器获取到异步结果后 需要通过钩子改变promise的状态 设置结果 触发回调函数执行。

5.api

例如接受注册回调函数的各种api then catch finally 以及一些静态race all resolve reject等。

promise特点

链式调用

promise的这些api 无论是静态的还是原型上的 都有一个特点即支持链式调用。实现链式调用的原理也很简单,每次都返回一个新的promise实例即可。

值穿透

promise的值通过钩子函数onResolve/onReject设置 或者通过注册的回调函数获取结果,如果处理函数返回一个非Promise值,那么这个值会被直接传递到下一个

举个例子

例子1:

javascript 复制代码
const execute = (onResolve, onReject) => {
  setTimeout(() => {
    onResolve(123);
  }, 2000);
};
const p = new Promise(execute);
p.then(result => {
    console.log('result:', result)
    return 456;
}).catch(error => {
    console.log('error:', error)
}).finally(() => {
    console.log('finally')
}).then(res=>{
    console.log('res:', res)
});
console.log('同步代码')

下面逐行分析下:

  1. js解析执行脚本代码,初始化执行器execute。
  2. 初始化第一个promise实例 将execute传入构造函数 execute被同步执行 遇到异步api计时器 加入到事件循环中
  3. p执行then 容器内部注册一个成功状态的回调函数 then执行完毕返回第二个promise。
  4. 第二个promise执行catch方法 内部被注册一个失败的回调函数 catch执行完毕 返回第三个promise实例。
  5. 第三个promise执行finally方法 内部注册成功和失败都要执行的回调函数 finally执行完毕 返回第四个promise实例。
  6. 第四个promise执行then方法 内部被注册一个成功的回调 返回第五个promise实例。
  7. 打印log 同步代码。 到这里同步代码执行完毕 实例化了5个promise。

下面是异步部分:

  1. 计时器运行结束 回调函数推入到事件队列 执行栈执行回调函数。
  2. resolve(123)钩子执行 触发第一个promise状态改变 pendding->fulfilled。value被设置为123。将value=123作为参数,依次执行容器内的成功回调函数。
  3. 成功回调函数执行结束 获取返回值456。触发第二个promise状态改变pendding->fulfilled,将第二个Promise的value设置为456。将value=456作为参数,依次执行第二个Promise内的成功回调函数。
  4. 第二个Promise内的成功回调函数为空。将value=456透传给第三个promise。同时第三个promise状态改变pendding->fulfilled。因为第三个promise内部的函数是通过finally注册的,所以在执行时并不会将value作为参数,仅仅是依次执行第三个Promise内的成功回调函数。
  5. 第三个Promise内的成功回调函数执行完毕,触发第四个promise状态改变pendding->fulfilled,同时将value=456透传给第四个promise. 将value=456作为参数,依次执行第四个Promise内的成功回调函数。
  6. 执行第四个Promise内的成功回调函数,获取返回值undefined,设置第五个promsie的value=undefined,触发第五个promise状态改变pendding->fulfilled。

promise值透传规律

  1. 通过钩子设置onResolve/onReject显示设置 例如上面的onResolve(123)
  2. 如果回调函数有返回值 则用当前的返回值设置下一个promsie的value 例如上面的返回值456
  3. 当没有对应的回调函数时(finally注册的相当于没有)透传当前的value。 例如第二个promise到第三个。

例子二:

javascript 复制代码
const execute = (onResolve, onReject) => {
  setTimeout(() => {
    onReject('失败')
  }, 2000);
};
const p = new Promise(execute);
p.then((data)=>{
  console.log('成功的异步回调 异步结果:', data)
});
p.catch((reason)=>{
  console.log('失败的异步回调 异步结果:', reason)
  return 'fail'
});
p.finally(()=>{
  console.log('finally异步回调')
});

问题:运行结果是什么

打印失败和成功的回调应该不难理解 因为promsie被reject了。

但是为什么还会有报错呐?我明明已经catch了啊~

分析下:

  1. 首先同步代码执行结束 生成了4个promise实例。第一个promise内部被注册了三组回调函数
  2. 计时器运行结束 onReject钩子触发promise1状态改变pendding->rejected. promise的value='失败'。然后依次执行对应的回调列表。
  3. 执行第一个回调列表 发现没有失败对应的回调函数 所以透传当前的promise的值和状态给下一个promsie。所以promise2的状态改变pendding->rejected. promise的value='失败'。
  4. 执行第二个回调列表 有失败的处理函数 将promsie的value='失败'作为参数 执行此函数。得到返回结果'fail'不是一个promise 因此将结果作为promise3的值 并更新状态pendding->fulfilled.
  5. 执行第三个回调列表 有失败的处理函数 但是这个函数是通过finally注册的 因此仅执行当前函数。并将当前promise的结果通过钩子onReject透传给promsie4。 所以第四个promsie状态pendding->rejected,value='失败'。
  • 第一次循环结束。
  1. 上次循环中3改变了promise2的状态为rejected,这次循环中依次执行对应的回调列表。因为promise2 没有回调列表 所以rejected状态,再次向上抛出。抛出第一个错误异常uncaught.
  2. 上次循环中promise3状态为fulfilled,内部没有回调列表 所以不执行。
  3. 上次循环中promise4状态为rejected,依次执行对应的回调列表,因为promise4没有回调列表 所以rejected状态,再次向上抛出。抛出第二个错误异常uncaught.

例子三

javascript 复制代码
const execute = (resolve, reject) => {
  setTimeout(() => {
    resolve("123");
  }, 2000);
};
const p = new Promise(execute);
p.then((data) => {
  console.log("1111111");
})
  .finally(() => {
    console.log("33333333");
  })
  .finally(() => {
    console.log("55555555");
  });
p.finally(() => {
  console.log("2222222");
}).finally(() => {
  console.log("4444444");
});

输出结果:

复制代码
1111111
2222222
33333333
4444444
55555555

代码实现:

手动实现了一版本promise,手写promise。有兴趣的同学可以看下~

kotlin 复制代码
import microtask from './microtask.js'
const State = {
  pending: "pending",
  fulfilled: "fulfilled",
  rejected: "rejected",
};
export class MyPromise {
  #state = State.pending;
  #res = null;
  #handles = [];

  constructor(executor) {
    const resolve = (data) => {
      this.#changeState(State.fulfilled, data);
    };
    const reject = (reason) => {
      this.#changeState(State.rejected, reason);
    };
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  #changeState = (state, res) => {
    if (this.#state !== State.pending) {
      return;
    }
    this.#state = state;
    this.#res = res;
    this.#runMicrotask();
  };

  #isPromiseLike = (promise) => {
    return (
       promise !== null &&
      (typeof promise === "object" || typeof promise === "function") &&
      typeof promise.then === "function"
    );
  };

  #runOne = (callback, resolve, reject) => {
    try {
      const nextBack = this.#state === State.fulfilled ? resolve : reject;
      if (typeof callback !== "function") {
        nextBack(this.#res);
        return;
      }
      const data = callback(this.#res);
      if (this.#isPromiseLike(data)) {
        data.then(resolve, reject);
        return;
      }
      resolve(data);
    } catch (error) {
      reject(error);
    }
  };
  #run = () => {
    if (this.#state === State.pending || !this.#handles.length) {
      return;
    }
    const item = this.#handles.shift();
    const { onFulfilled, onRejected, resolve, reject } = item;
    this.#runOne(
      this.#state === State.fulfilled ? onFulfilled : onRejected,
      resolve,
      reject
    );
    this.#run();
  };

  /**
   * 运行于微任务中
   * */
  #runMicrotask = () => {
    microtask(this.#run);
  };

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.#handles.push({ resolve, reject, onFulfilled, onRejected });
      this.#runMicrotask();
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }
  finally(onFinally) {
    return this.then(
      (data) => {
        onFinally();
        return data;
      },
      (reason) => {
        onFinally();
        throw reason;
      }
    );
  }
}
相关推荐
让梦想疯狂19 分钟前
开源、免费、美观的 Vue 后台管理系统模板
前端·javascript·vue.js
葡萄糖o_o1 小时前
ResizeObserver的错误
前端·javascript·html
sunbyte1 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | AnimatedNavigation(动态导航)
前端·javascript·vue.js·tailwindcss
JustHappy2 小时前
啥是Hooks?为啥要用Hooks?Hooks该怎么用?像是Vue中的什么?React Hooks的使用姿势(下)
前端·javascript·react.js
江城开朗的豌豆2 小时前
Vue中Token存储那点事儿:从localStorage到内存的避坑指南
前端·javascript·vue.js
江城开朗的豌豆2 小时前
MVVM框架:让前端开发像搭积木一样简单!
前端·javascript·vue.js
氢灵子3 小时前
Canvas 变换和离屏 Canvas 变换
前端·javascript·canvas
dy17173 小时前
tabs页签嵌套表格,切换表格保存数据不变并回勾
javascript·vue.js·elementui
GISer_Jing3 小时前
Axios面试常见问题详解
前端·javascript·面试
Humbunklung3 小时前
JavaScript 将一个带K-V特征的JSON数组转换为JSON对象
开发语言·javascript·json