异步编程

1,659 阅读20分钟

异步处理的概念

JavaScript 中的异步处理指的是在代码执行过程中,能够不阻塞当前线程并处理一些时间较长的操作。异步处理通常涉及到回调函数、Promise、async/await 等机制。

在 JavaScript 中,传统的同步处理方式采用的是阻塞式的单线程模型。这种模型的缺点是当一个任务被执行时,它会一直执行到结束,期间如果有耗时的操作也会一直阻塞下去,直到任务执行完毕,才会执行后续的任务。这种方式会导致页面卡死,体验非常不好。

因此,JavaScript 异步处理机制应运而生,它允许在代码执行过程中,执行一些耗时的操作,而不会阻塞当前线程。异步处理机制可以通过以下方式实现:

回调函数

概念: 函数被当参数传递进去. 在 JS 函数作为参数的专门名称就是callback.

回调函数有两种类型:
同步回调函数
常见的有forEach(遍历数组)、filter(过滤数组,筛选数组)、reduce(数组求和)

arr.forEach((value, index) => {
    console.log(value, index);
})

let f = arr.filter(value) => {
    return value % 2 != 0
})

let r =arr.reduce((total, value) => {
    return total += value
}, 0)//total初始值为0

异步回调函数
常见的有addEventListener事件监听、setInterval定时器、setTimeout延时器

回调函数是一种很常见的异步编程模型,通过在异步操作完成后调用回调函数来通知异步操作已结束,从而执行后续的任务。

<div id="wrapper"></div>
<button id="btn">获取数据</button>

<script>
    const btnDom=document.querySelector("#btn");
    const erapperDom=document.quereSelector("#wrapper");
    
    const fetchData= (callback) =>{
        //模拟请求,服务会在1s后返回数据
        setTimeout(() => {
            //这块逻辑其实不应该耦合在这里
            //因为这块是操作视图部分(view)的内容,不应该与服务层(service)的内容耦合在一起
            //wrapperDom.innerHTML="data";
            
            //一个函数中间的数据怎么给外面: 1.return  2.回调
            //不能直接这样: return data;
            //我们需要给它加上callback
            callback("hello world");//传入实参 这里只负责把请求的数据给外面,并不关心外面怎么处理该数据
        },1000);
    };
    
    btnDom.addEventListener(
        "click",
        () => {
            wrapperDom.innerHTML = "loadding...";
            //fetchData();
            
            fetchData(function (data){
                wrapperDom.innerHTML =data;
                //do something
            })//形参接收
        },
        false
    );//点击btn按钮后会先出现loadding...然后fetchData,在1s后显示得到数据
</script>

- > 耦合: 各种不相关的功能都放在了一起,下次再想用某个单个功能,就很麻烦.

简化整理一下就是这样:

function fetchData(callback) {
  setTimeout(function() {
    const data = { name: '张三', age: 20 };
    callback(data);//传入实参
  }, 1000);
}

fetchData(function(data) {
  console.log(data);
});//接收形参

在这个示例中,fetchData()函数在完成数据加载后,调用 回调函数 callback() 并传递数据作为参数。当数据加载完成后,控制器会跳转到回调函数中执行后续任务。

Promise

Promise 是一种比较流行的异步编程模型,它可以在异步操作完成后执行一些回调操作,并将结果返回给请求方。Promise 代表了一个异步操作的最终完成(或失败)及其结果值。 例如:

function fetchData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const data = { name: '张三', age: 20 };
      resolve(data);
    }, 1000);
  });
}

fetchData().then(function(data) {
  console.log(data);
});

在这个示例中,fetchData() 函数返回了一个 Promise 对象,当数据加载完成后,Promise 对象会调用then()方法中的回调函数,并将数据作为参数传递进去。

async/await

async/await 基于 Promise 编写,并提供了更加精简的语法。async/await 语法可以使异步代码看起来像同步代码,从而写出更加直观和易于维护的代码。

async function fetchData() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      const data = { name: '张三', age: 20 };
      resolve(data);
    }, 1000);
  });
}

async function main() {
  const data = await fetchData();
  console.log(data);
}

main();

在这个示例中,fetchData() 函数依然返回一个 Promise 对象,但是在主函数 main() 中,我们使用了 async 关键字来定义一个异步函数,并使用 await 关键字等待fetchData()函数执行结果。当 fetchData() 函数执行完成后,数据会作为 await 表达式的结果返回,在控制台输出数据信息。

异步处理常见场景与处理策略

异步处理常见场景包括但不限于:

  1. 网络请求:当请求数据需要一定的时间才能返回时,为了避免用户体验受到影响,需要进行异步处理。
  2. 定时任务:定时执行任务,需要进行异步处理。
  3. 事件处理:通过异步处理来避免事件处理函数执行时间过长,导致页面卡顿等问题。
  4. 大量数据处理(web worker):对于大量数据的处理,需要进行异步处理,以免阻塞主线程。

异步处理的常见处理策略有:

  1. 回调函数:将后续处理逻辑以函数的形式传入异步操作的回调函数中,异步操作完成后自动执行回调函数。
  2. Promise:通过返回 Promise 对象,在异步操作完成后进行下一步处理。
  3. Async/await:在异步函数中使用 await 等待异步操作完成后再进行下一步处理。
  4. 事件监听:通过监听异步操作所触发的事件,在事件处理函数中进行后续处理。例如通过 XMLHttpRequest 对象的 onreadystatechange 事件进行处理。
  5. 发布订阅模式:在异步操作完成后,发布事件,订阅事件的相关回调函数处理后续逻辑。
  6. Web Worker:将耗时的计算任务交给 Web Worker 在后台处理,避免主线程阻塞
  7. Generator 函数:通过 Generator 函数中的 yield 关键词(中断操作),将多步骤的异步操作转化为同步代码的执行模式,提高代码可读性。
  8. Promise.all:如果存在多个异步操作,需要等待所有操作完成后再进行处理,可以使用 Promise.all 将多个 Promise 对象合并处理。
  9. 回调地狱解决方案:当存在多层嵌套的回调函数时,回调函数的代码难以阅读和维护。通过使用 Promise 和 async/await 进行代码重构,提高可读性和可维护性。
  10. 预加载资源:对于一些大型的资源,通过异步进行资源预加载可以提高页面速度和用户体验。
  11. 异步流程控制库:各种异步流程控制库(如 async)可以帮助简化异步过程中的问题,提高代码可读性和维护性。

回调函数与“回调地狱”

CPS 与 DS

传递风格(Continuation-passing style, CPS,与之对比是直接风格(Direct style)

直接风格的范例如下,其实就是一般函数调用的方式:

//直接風格 DS
function func(x) {
  return x
}

CPS 风格就不是这样,它会用 另一个函数作为函数中的传入参数,然后将本来应该要回传的值(不限定只有一个),传给下一个延续函数,继续下个函数的执行:

//CPS風格
function func(x, cb) {
  cb(x)
}

通常,我们使用回调函数是迫不得已,因为我们没办法立即拿到值,我们就只能寄希望于事件循环机制

JavaScript 引擎是单线程执行的,也就是说同一时间内只有一个任务在执行。当需要进行异步操作时,通常会使用回调函数。

假设我们有一个获取用户信息的异步函数 getUserInfo,在信息获取完成后需要调用相关回调函数。一种实现方式是将回调函数作为 getUserInfo 函数的第二个参数传入,信息获取完成后调用该函数。

function getUserInfo(userId, callback) {
  setTimeout(function() {
    const userInfo = {
      id: userId,
      name: "Tom",
      age: 25
    }
    callback(userInfo)
  }, 1000)
}

getUserInfo(1001, function(userInfo) {
  console.log(userInfo)
})

上述代码首先调用 getUserInfo 函数,该函数通过 setTimeout 模拟异步操作,等待 1 秒钟后获取用户信息,并在信息获取完成后调用传入的回调函数。最后在回调函数中输出用户信息。

我们再来看一个更可怕的(回调地狱),这个案例我们贯穿始终

function f1(f2){
    f2(function(f3){
        f3(function(f4) {
            f4(function () {});
        });
    });
}

f1(function(f2) {
  const t1 = 'f1';
  console.log(t1);
  f2(function(f3) {
    const t2 = `${t1},f2`;
    console.log(t2);
    f3(function(f4) {
      const t3 = `${t2},f3`;
      console.log(f3);
      f4();
    });
  });
});

从 callback 到 promise

回调函数最让人诟病的就是嵌套了,为此开发者们还为它取了个名字——“回调地狱”。

同样是以上 getUserInfo 示例,如果我们通过 promise 来处理,是怎样的呢?

function getUserInfo(userId) {
    return new Promise((resolve, reject) => {
      setTimeout(function() {
        const userInfo = {
          id: userId,
          name: "Tom",
          age: 25
        }
        resolve(userInfo)
      }, 1000)
    })
}

getUserInfo(1001).then(res => console.log(res))

一般用 Promise 方案进行处理时,我们需要关注以下几个点:

  1. 函数一定返回 promise 实例(return new Promise((resolve,reject) =>{});)
  2. promise 实例中需要进行 resolve、reject 处理
  3. 使用时,通过 then 进行链式调用并获取结果
  4. 如果有异常,可以在 then 第二个参数回调或者 .catch 进行处理
  5. 还包括 finally

如果 callback 嵌套上述贯穿始终的回调地狱示例,我们用 promise 改写,会怎样?

f1()
  .then(() => {
    const t1 = 'f1';
    console.log(t1);
    return t1;
  })
  .then(t1 => {
    return f2().then(function() {
      const t2 = `${t1},f2`;
      console.log(t2);
      return t2;
    });
  })
  .then(t2 => {
    return f3().then(function() {
      const t3 = `${t2},f3`;
      console.log(f3);
      return t3;
    });
  });

这样看,其实还是繁琐,代码不够简洁,没事儿,我们会在后面继续将它优化。

就算 Promise 相较于 callback 代码简化了很多,但是我们通常还是抱怨 Promise 定义太繁琐,有没有更简洁的方式呢?

详解 Promise A+ 规范

image.png

  • 解决(fulfill):指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但在后世的 promise 实现多以 resolve 来指代
  • 拒绝(reject):指一个 promise 失败时进行的一系列操作

Promise 的状态

一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)执行态(Fulfilled)拒绝态(Rejected)

  • 等待态(Pending) 处于等待态时,promise 需满足以下条件:
    • 可以迁移至执行态或拒绝态
  • 执行态(Fulfilled) 处于执行态时,promise 需满足以下条件:
    • 不能迁移至其他任何状态
    • 必须拥有一个不可变的终值
  • 拒绝态(Rejected) 处于拒绝态时,promise 需满足以下条件:
    • 不能迁移至其他任何状态
    • 必须拥有一个不可变的原因
    • 这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(注:当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。

then 方法

一个 promise 必须提供一个 then 方法以访问其当前值、终值(promise 被解决时传递给解决回调的值) 和 据因(promise 被拒绝时传递给拒绝回调的值)。

promise 的 then 方法接受两个参数:

promise.then(onFulfilled, onRejected);

其中,onFulfilled 和 onRejected 都是可选参数。

  • 如果 onFulfilled 不是函数,其必须被忽略
  • 如果 onRejected 不是函数,其必须被忽略

手写 Promise

手写 Promise 前,首先我们加深一个概念,那就是发布-订阅模式,这个概念非常重要。 image.png 我们可以用来实现Promise,而且Promise有三种状态:pending、fulfilled、rejected。初始状态为pending。还需要对Promise的终值进行初始化。Promise还有两个方法resolve和reject

Promise有四个特点:

  • 执行了resolve,Promise状态就会变成fulfilled
  • 执行了reject,Promise状态就会变成rejected
  • Promise状态不可逆,第一次成功就永久为fulfilled,第一次失败就永久为rejected
  • Promise中有throw的话,就相当于执行了rejected
const STATUS = {
    PENDING: "pending",
    FULFILLED: "fulfilled",
    REJECTED: "rejected",
};

// 首先有一个 myPromise 的类
class myPromise {
    status = STATUS.PENDING; // 默认状态是 pending
    // 用于 then 中的回调函数存储,以便在 resolve 时执行
    resolves = [];
    rejects = [];

    // subscribers = [];
    // webpack 中 tiptap 的源码中的写法

    resolve = (value) => {
      // 1. 改变状态
      // 2. 保存值
      // 3. 触发回调
      const { resolves } = this;
      // 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.FULFILLED;
      // 把我们现在存储在 resolves 中的 resolve 依次执行 出队
      while (resolves.length) {
        const resolveFn = resolves.shift();
        const newVal = resolveFn(value);
      }
    };

    reject = (value) => {
      const { rejects } = this;
      // 把我们现在存储在 rejects 中的 reject 依次执行 出队
      // 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.REJECTED;
      while (rejects.length) {
        const rejectFn = rejects.shift();
        const newVal = rejectFn(value);
      }
    };

    constructor(executor) {
      executor(this.resolve, this.reject);
    }

    then(resolveFn, rejectFn) {
      // .then 就意味着我需要订阅你的状态变更后的值 入队
      this.resolves.push(resolveFn);
      rejectFn && this.rejects.push(rejectFn);
      console.log("then");

      return this;
    }

    catch(rejectFn) {
      this.rejects.push(rejectFn);
      console.log("catch");
    }

    finally() {
      console.log("finally");
    }
}

// 然后有一个 myPromise 的实例
const myPromise = new myPromise((resolve, reject) => {
    setTimeout(() => {
      resolve("hello world");
    }, 1000);
});

myPromise
.then((res) => {
  // 我是非常期望在一秒以后,能够打印 hello world
  console.log(res);
  return "res" + res;
})
.then((res) => {
  console.log(res + "111");
});
// .catch((err) => {
//   console.log(err);
// });

console.log(myPromise);

基础实现

我们需要记录需要进行 resolve 的操作,然后在promise 执行 then 时,可以调起该 resolve 并进行处理
JS:

class myPromise{
    resolves = [];
    
    constructor(executor){
        executor(this.resolve);
    }
    
    resolve = (value) => {
        const{ resolves } = this;
        while(resolves.length){
            const resolveFn = resolves.shift();//出队
            const newVal = resolveFn(value);
        }
    }
    
    then(resolveFn){
        this.resolves.push(resolveFn);//入队
    }
}

const myPromise = new myPromise((resolve, reject) => {
    setTimeout(() => {
      resolve("hello world");
    }, 1000);
});

TS:

type FuncType = (...args: any[]) => any;
type ExecutorFunc = (resolveFunc: FuncType, rejectFunc?: FuncType) => any;

export class myPromise {
  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 myPromise() 时,constructor 函数会执行,不过这里需要注意的是,我们暂时只考虑异步操作,忽略了同步的情况。异步情况下 executor 函数会在未来某个时间点执行,而从初始化到这个时间点之间,正是 then 函数执行收集依赖的过程。

添加 reject 的处理

JS:

class myPromise{
    resolves = [];
    rejects = [];
    constructor(executor){
        executor(this.resolve,this.reject);
    }
    
    resolve = (value) => {
        const{ resolves } = this;
        while(resolves.length){
            const resolveFn = resolves.shift();//出队
            const newVal = resolveFn(value);
        }
    }
    
    reject = (value) => {
        const { rejects } = this;
        while(rejects.length){
            const rejectFn = rejects.shift();
            constnewVal = rejectFn(value);
        }
    }
    then(resolveFn){
        this.resolves.push(resolveFn);//入队
        rejectFn && this.rejects.push(rejectFn);
    }
}

TS:

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);
  }
}

使用 jest 进行测试

首先配置 jest 环境

npm install --save-dev jest

package.json 中修改执行脚本:

{
  "scripts": {
    "test": "jest"
  }
}

编写对应测试用例

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 内部,将各任务放入宏/微任务队列中(宏/微任务请参看 事件循环 );
  • thencatch 中可收集到 resolvereject 依赖,并将该依赖存放到对应队列中;
  • 异步任务执行完以后,调用 executor 中的 resolvereject,取出对应队列中的依赖依次执行。

增加符合 Promise A+ 规范的状态值

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

//TS
enum STATUS {
  PENDING = 'pending',
  FULFILLED = 'fulfilled',
  REJECTED = 'rejected',
}
//JS
const STATUS = {
    PENDING: "pending",
    FULFILLED: "fulfilled",
    REJECTED: "rejected",
};

在执行 resolve 前,需要检测当前状态是否为 pending,如果是则可以继续执行,否则无法执行 resolve,在执行 resolve 时,将状态置为 fulfilledreject 方法中同理先检测状态是否为 pending,如果是则继续执行并将状态置为 rejected

改进后,代码示例如下:
JS:

const STATUS = {
    PENDING: "pending",
    FULFILLED: "fulfilled",
    REJECTED: "rejected",
};

class myPromise {
    status = STATUS.PENDING; // 默认状态是 pending
    resolves = [];// 用于 then 中的回调函数存储,以便在 resolve 时执行
    rejects = [];

    constructor(executor) {
      executor(this.resolve, this.reject);
    }

    resolve = (value) => {
      const { resolves } = this;
      // 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.FULFILLED;
      while (resolves.length) {//出队
        const resolveFn = resolves.shift();
        const newVal = resolveFn(value);
      }
    };

    reject = (value) => {
      const { rejects } = this;
      // 因为根据 Promise A+ 规范,只有在 pending 状态下才能改变状态
      if (this.status !== STATUS.PENDING) return;
      this.status = STATUS.REJECTED;
      while (rejects.length) {
        const rejectFn = rejects.shift();
        const newVal = rejectFn(value);
      }
    };

    then(resolveFn, rejectFn) {
      this.resolves.push(resolveFn);//入队
      rejectFn && this.rejects.push(rejectFn);
    }
}

TS:

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 包裹,在本例中就是需要将返回值包装为新的 myPromise 对象。 开发之前我们不妨先来看看Promise 链式调用的示例

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 函数,可以依次调用。这里需要做的处理是,需要将传入的 resolve 与 reject 函数封装然后放入待执行队列中。简言之,当返回值为一个 Promise 时,需要执行 promise.then 方法,否则直接执行 resolve。 改进后的 then 方法如下:
JS:

then(resolveFn, rejectFn) {
  return new myPromise((resolve, reject) => {
    const resolvedFn = (val) => {
      try {
        let resolvedVal = resolveFn(val);
        //如果返回的是一个 Promise,那么就需要等待这个 Promise 执行完毕
        resolvedVal instanceof myPromise
          ? resolvedVal.then(resolve, reject)
          : resolve(resolvedVal);
      } catch (error) {
        if (reject) reject(error);
      }
    };
    
    this.resolves.push(resolvedFn);
    if (rejectFn) this.rejects.push(rejectFn);
  });
}

TS:

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

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

  1. 包装初始 then 方法传入的 resolve 函数;
  2. 先将初始 then 方法传入的 resolve 函数执行,得到返回值,如果返回值是一个新的 myPromise 对象,则需要手动调用该实例的 then 方法,否则直接执行 resolve 函数;
  3. 将包装过的 resolve 函数放入 resolves 队列中,等待执行

补全 reject 的逻辑
JS:

then(resolveFn, rejectFn) {
  return new myPromise((resolve, reject) => {
    const resolvedFn = (val) => {
      try {
        let resolvedVal = resolveFn(val);
        //如果返回的是一个 Promise,那么就需要等待这个 Promise 执行完毕
        resolvedVal instanceof myPromise
          ? resolvedVal.then(resolve, reject)
          : resolve(resolvedVal);
      } catch (error) {
        if (reject) reject(error);
      }
    };

    const rejectFn = (val) => {
      try {
        let rejectedVal = rejectFn(val);
        rejectedVal instanceof myPromise
          ? rejectedVal.then(resolve, reject)
          : resolve(rejectedVal);
      } catch (error) {
        if (reject) reject(error);
      }
    };
    
    this.resolves.push(resolvedFn);
    if (rejectFn) this.rejects.push(rejectFn);
  });
}

TS:

then(resolveFunc: FuncType, rejectFunc?: FuncType) {
    return new myPromise((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);
    });
  }

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

it('chain invoke usage', done => {
  const p = new myPromise(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 参数进行处理,如果参数不是函数,则需要忽略并继续往下执行,示例如下:

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 语句进行策略处理,如下:

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 是上次执行完后得到的值,起到暂存的目的。补充以上代码后,完整代码示例如下:

type FuncType = (...args: any[]) => any;

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

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

export class myPromise {
  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): myPromise {
    typeof resolveFunc !== 'function' ? (resolveFunc = value => value) : null;
    typeof rejectFunc !== 'function'
      ? (rejectFunc = reason => {
          throw new Error(reason instanceof Error ? reason.message : reason);
        })
      : null;

    return new myPromise((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 myPromise
              ? 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;
      }
    });
  }
}

同步任务处理

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

export class myPromise {
  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 逻辑。
编写测试代码,如下:

it('sync task', done => {
  const p = new myPromise(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 参数,实现如下:

catch(rejectFnnc) {
  return this.then(undefined, rejectFnnc)
}

finally

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

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 状态。如下:

finally(cb) {
  return this.then(
    value => myPromise.resolve(cb()).then(() => value),
    reason => myPromise.resolve(cb()).then(() => { throw reason })
  )
}

resolve

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

static resolve(val) {
  if(val instanceof HePromise) return val
  return new myPromise(resolve => resolve(val))
}

编写测试代码如下:

it('myPromise.resolve', done => {
  myPromise.resolve(1).then(res => {
    expect(res).toBe(1);
    done();
  });
});

reject

该方法的原理同 resolve,直接贴出代码

static reject(val) {
  return new myPromise((resolve, reject) => reject(val))
}

编写测试代码如下:

it('myPromise.reject & catch', done => {
  myPromise.reject(1).then(
    res => {
      expect(res).toBe(1);
      done();
    },
    error => {
      expect(error).toBe(1);
      done();
    },
  );
});

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

it('myPromise.reject & catch', done => {
  myPromise.reject(1)
    .then(res => {
      expect(res).toBe(1);
      done();
    })
    .catch(error => {
      expect(error.message).toEqual('1');
      done();
    });
});

执行测试,测试通过。

all

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

myPromise.all = function (promises) {
    let index = 0;
    const result = [];
    const pLen = promises.length;
    return new myPromise((resolve, reject) => {
      promises.forEach((p) => {
        myPromise.resolve(p).then(
          (val) => {
            index++;
            result.push(val);
            if (index === pLen) {
              resolve(result);
            }
          },
          (err) => {
            if (reject) reject(err);
          }
        );
      });
    });
  };

编写测试用例如下:

it('myPromise.all', done => {
  myPromise.all([1, 2, 3]).then(res => {
    expect(res).toEqual([1, 2, 3]);
    done();
  });
});

执行测试,测试通过。

race

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

myPromise.race = function (promises) {
    return new myPromise((resolve, reject) => {
      promises.forEach((p) => {
        myPromise.resolve(p).then(
          (val) => {
            resolve(val);
          },
          (err) => {
            if (reject) reject(err);
          }
        );
      });
    });
  };

编写测试用例:

it('myPromise.race', done => {
  myPromise.race([11, 22, 33]).then(res => {
    expect(res).toBe(11);
    done();
  });
});

执行测试,测试通过。

async 与 await 用法及原理详解

基本使用

async function test() {
    const res = await Promise.resolve(1)
    return res
}

需要注意的是,使用 async、await 处理异步操作时,需要注意异常的处理。

异常处理

通常我们使用 try、catch 捕获 async、await 执行过程中抛出的异常,就像这样:

async function test() {
    let res = null
    try {
        const res = await Promise.resolve(1)
        return res
    } catch(e) {
        console.log(e)
    }
}

从零实现一个类似 async、await 的函数

function fn(nums) {
    return new Promise(resolve = >{
        setTimeout(() = >{
            resolve(nums * 2)
        },
        1000)
    })
}
function * gen() {
    const num1 = yield fn(1)
    const num2 = yield fn(num1)
    const num3 = yield fn(num2)
    return num3
}
function generatorToAsync(generatorFn) {
    return function() {
        return new Promise((resolve, reject) = >{
            const g = generatorFn() const next1 = g.next() next1.value.then(res1 = >{

                const next2 = g.next(res1) // 传入上次的res1
                next2.value.then(res2 = >{

                    const next3 = g.next(res2) // 传入上次的res2
                    next3.value.then(res3 = >{

                        // 传入上次的res3
                        resolve(g.next(res3).value)
                    })
                })
            })
        })
    }
}

const asyncFn = generatorToAsync(gen)

asyncFn().then(res = >console.log(res)) // 3秒后输出 8

自动执行

自动执行其实就是运用递归,将生成器函数产生的数据不断调用 next,直至执行完成。

function getData(endpoint) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Data received from ${endpoint}`)
    }, 2000)
  })
}

// 生成器函数
function* getDataAsync() {
  const result1 = yield getData('Endpoint 1')
  console.log(result1)
  const result2 = yield getData('Endpoint 2')
  console.log(result2)
  return 'All data received'
}

// 将生成器函数包装成 Promise
function asyncToPromise(generatorFn) {
  const generator = generatorFn()

  function handleResult(result) {
    if (result.done) {
      return Promise.resolve(result.value)
    }

    return Promise.resolve(result.value)
      .then(res => handleResult(generator.next(res)))
      .catch(err => handleResult(generator.throw(err)))
  }

  try {
    return handleResult(generator.next())
  } catch (error) {
    return Promise.reject(error)
  }
}


asyncToPromise(getDataAsync).then(result => console.log(result))