js异步经典之Promise

异步

要了解Promise首先需要了解异步,这是js一个老生常谈的问题,为什么js会有异步,这和js最开始的的作用是离不开的。

js是单线程的语言,他主要是实现用户与浏览器的交互,以及操作dom,这决定了它只能是单线程,否则会带来很复杂的同步问题,

如果js可以多线程进行,那对于同一个dom同时进行自相矛盾的操作,比如删除或者添加功能,那就有些问题了。

但是在代码中不可避免的会有需要时间才能给出反应的代码,比如常见的获取一个资源,发送一个请求。

所以为了让运行不要停止在这里,js会先执行下面的代码最后再来执行这些需要一定时间的代码。

这里涉及到js的事件循环。

Event-Loop

这里简单介绍一下event-loop。

js的代码是自上而下,从左到右执行,一个一个放入调用栈。

- 首先把同步代码全部放入到执行栈中,异步代码按顺序放入异步进程。

- 然后同步代码按照顺序执行

- 异步进程中的代码到达他所设置的时间或者请求有了响应之后放入会放入调用栈

- 进程会一直查询调用栈是否有任务执行,有就放入执行栈执行

这里写一段示例代码

 console.log("start") setTimeout(() => {
      console.log("setTimeout"); }, 0)  
}
// 这里要注意虽然写的是0但是由于他是异步函数所以还是会放到异步进程中执行
// 当然这里还有一个setTimeout的最小延迟时间 是最小延时 >=4ms 
console.log("end");

输出是

 start 
 end 
 setTimeout

promise第一版

了解了事件循环我们就可以开始写promise了

先了解promise的优缺点

优点

1)可以解决异步嵌套问题

2)可以解决异步并发问题

缺点

1)promise基于回调

2)promise无法终止异步

他有三种状态等待成功和失败,而且一旦启动就立即开始执行,这也是他的一个缺点之一。

关于能不能停止执行promise

一个知乎作者的回答我觉得很好理解 https://www.zhihu.com/people/Tech_Query/answers

Promise 异步函数与普通同步函数基本语义的对应关系: resolve(data) 对应 return data,表示最终结果 reject(error) 对应 throw error,表示中间异常 因此才能再包装成 async、await 语法糖来复用同步函数的所有语法。反观同步函数,你可以取消、中断执行吗?不能。同理,异步函数也不能。

看一个简单的promise的使用

 let Promise = require('./one'); 
 let promise = new Promise((resolve, reject) => {
    // executor 执行函数   
    console.log(1);
    resolve('2') 
}).then(
    (value) => { console.log(value); },
    (reason) => { console.log(reason);}
   )
//输出为 
1 2

了解之后就可以开始写promise了,这里注意promise有一个规范Promise/A+ 规范,业界所有 Promise 的类库都遵循这个规范。

promise传入一个函数,我们叫做executor函数,这个函数两个参数,resolve和reject,promise执行的过程

就是执行 executor函数的内容。然后有then这个对象,可以传入两个函数作为对象,可以理解为一个是resolve一个是reject。

在executor中调用resolve或者reject就会把调用时候的值传入then的两个函数中。然后promise的状态发生改变。通过这些我们写出第一版的promise。

class Promise {
  constructor(executor) {
    // 默认状态为 PENDING
    this.status = PENDING;
    // 存放成功状态的值,默认为 undefined
    this.value = undefined;
    // 存放失败状态的值,默认为 undefined
    this.reason = undefined;

    // resolve 和 reject 是私有的

    // 调用此方法就是成功
    let resolve = (value) => {
      // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resolve/reject 方法
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
      }
    }

    // 调用此方法就是失败
    let reject = (reason) => {
      // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resolve/reject 方法
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
      }
    }
// 为了捕获executor过程中的错误,所以这里用try catch
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error);
    }
  }
// 执行then方法,传入两个函数,如果成功就执行
  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }

    if (this.status === REJECTED) {
      onRejected(this.reason)
    }
  }
}

可以看到执行过程,定义三个状态,两个函数resolve,reject,然后就执行executor也就是传入进来的函数,然后then中执行

onFulfilled或者onRejected。

这里一样执行

 let Promise = require('./one'); 
 let promise = new Promise((resolve, reject) => {
    // executor 执行函数   
    console.log(1);
    resolve('2') 
}).then(
    (value) => { console.log(value); },
    (reason) => { console.log(reason);}
   )
//输出为 
1 
2

换用reject也可以达到效果,这里就完成了初步的promise。

但是promise是为了完成异步任务而出现的,这个可以完成异步任务吗?显然不可以,用代码测试

let promise = new Promise((resolve, reject) => {   // executor 执行函数
  setTimeout(() => {
    resolve('good');
  }, 1000)
}).then(
  (value) => { console.log(value); },
  (reason) => { console.log(reason); }
)

结果是没有任何输出。接下来我们要解决这个问题

这里我们先介绍一个发布订阅模式

发布订阅模式

假设有一个all-on-emit文件同级目录下有一个

name.txt内容为{ “jack” }

age.txt内容为{ “12” }

let fs = require('fs')
let event = { // 发布与订阅没有关系  
  _arr: [],
  on(fn) {
    this._arr.push(fn)
  },
  emit() {
    this._arr.forEach(fn => fn());
  }
}
let school = {}
event.on(function () {
  console.log("读取一个");
})
event.on(function () {
  if (Object.keys(school).length === 2) {
    console.log(school);
  }
})
fs.readFile('./name.txt', 'utf8', function (err, data) {  // 注意这里的路径是根路径开始
  school.name = data;
  event.emit();
})

fs.readFile('./age.txt', 'utf8', function (err, data) {
  school.age = data;
  event.emit();
})
---------------------------------
输出为:
读取一个
读取一个
{ name: 'jack', age: '12' }

这个就是发布订阅模式。

先将两个函数订阅,然后再读取两个txt文件之后都发布一次,发布的时候执行每一个函数,就像订阅了一个公众号,公众号发布文章的时候,每一个用户都会接收文章。

发布订阅简单来说就是,只要订阅了就会通知每一个人,但是接受不接受就是个人的事情,

在这段代码中就是,订阅了两个函数(也可以是多个函数),只要触发了emit函数就会把这些函数都指向,

但是这些函数执行的效果,执行的条件,就可以自己添加判断条件。

常用的还有一种观察者模式,这里我们先简单了解一下发布订阅模式,之后在写文章详细了解这个两种模式。

Promise第二版

这里直接先给出代码

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
  constructor(executor) {
    // 默认状态为 PENDING
    this.status = PENDING;
    // 存放成功状态的值,默认为 undefined
    this.value = undefined;
    // 存放失败状态的值,默认为 undefined
    this.reason = undefined;

    this.onRejectedCallbacks = [];
    this.onResolvedCallbacks = [];

    // resolve 和 reject 是私有的
    // 调用此方法就是成功
    let resolve = (value) => {
      // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resolve/reject 方法
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        // 这里就是发布,将所有函数全部执行
        this.onResolvedCallbacks.forEach(fn => fn())
      }
    }

    // 调用此方法就是失败
    let reject = (reason) => {
      // 状态为 PENDING 时才可以更新状态,防止 executor 中调用了两次 resolve/reject 方法
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }
// 为了捕获executor过程中的错误,所以这里用try catch
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error);
    }
  }
// 执行then方法,传入两个函数,如果成功就执行,然后如果是executor异步的那么此时状态应该还是pending
// 那么就将要执行的代码放入发布数组中(订阅)然后再时间到了就全部执行(发布)
 then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }

    if (this.status === REJECTED) {
      onRejected(this.reason)
    }

    if (this.status === PENDING) {
      this.onResolvedCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.onRejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  }
}

这里就用到发布订阅模式,因为异步函数不会立刻执行,所以先把需要执行的代码保留下来,

在再执行resolve或者reject的时候把所有订阅的函数执行一遍。

到这里,还没有完全结束,promise一大特点就是可以链式调用,也就是这一点可以解决异步嵌套问题。

第三版Promise

promise能够链式调用,比如

let promise = new Promise((resolve, reject) => {   // executor 执行函数
  resolve('good');
}).then(
  (value) => {
    console.log(value);
    return value;
  }
).then((value) => {
  console.log(value);
})
--------------------
输出
good
good

也可以值穿透,就是当前的then不放入参数,后面的then也可以获得之前的返回值。

let promise = new Promise((resolve, reject) => {   // executor 执行函数
  setTimeout(() => {
    resolve('good');
  })
}).then()
  .then((value) => {
    console.log(value);
  })
----------
输出
good

所以可以得到以下几个要点

- 两个函数参数可以省略,也就是有默认值

- 为了能够多次调用then函数,要每一次then都返回一个promise

- then返回的是一个普通值的话,如果是成功返回就将结果传递给下一个then的resolve如果出错或者失败的返回就将值传递给下一个then的reject

- 如果then返回的是一个promise,就等待执行这个promise,promise 如果成功,就走下一个 then 的成功;如果失败,就走下一个 then 的失败;如果抛出异常,就走下一个 then 的失败

- 这里返回的promise一样要注意resolve和reject只能有一种状态,二选一

先给出代码,再逐步分析

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
const resolvePromise = (promise2, x, resolve, reject) => {
  // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise 
  if (promise2 === x) {                                                                                     //要点四
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  //  只能调用一次
  let called;                                                                                                // 要点七
  // 后续的条件要严格判断 保证代码能和别的库一起使用
  if ((typeof x === 'object' && x != null) || typeof x === 'function') {
    try {
      // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候)  
      let then = x.then;
      if (typeof then === 'function') {                                                                       //要点五
        // 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty  
        then.call(x, y => { // 根据 promise 的状态决定是成功还是失败
          if (called) return;
          called = true;
          // 递归解析的过程(因为可能 promise 中还有 promise) 
          resolvePromise(promise2, y, resolve, reject);                                                        //要点六
        }, r => {
          // 只要失败就失败
          if (called) return;
          called = true;
          reject(r);
        });
      } else {
        // 如果 x.then 是个普通值就直接返回 resolve 作为结果  
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e)
    }
  } else {
    // 如果 x 是个普通值就直接返回 resolve 作为结果  
    resolve(x)
  }
}

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    let resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    }

    let reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    }

    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  then(onFulfilled, onRejected) {
    //解决 onFufilled,onRejected 没有传值的问题
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;                                // 要点一
    //因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后 then 的 resolve 中捕获
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    // 每次调用 then 都返回一个新的 promise  
    let promise2 = new Promise((resolve, reject) => {                                                      // 要点二
      if (this.status === FULFILLED) {
         //--- setTimeout
        setTimeout(() => {  // 宏任务,保证promise2已经new完了
          try {// 为了避免再executor中就出现错误,所以这里要用try
            let x = onFulfilled(this.value);
            // x可能是一个promise
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e)
          }
        }, 0);
      }

      if (this.status === REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e)
          }
        }, 0);
      }

      if (this.status === PENDING) {                                                                        // 要点三
        // 可能是异步任务,所以和第二版一样先放入数组中存着,等到了时间在执行
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e)
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0);
        });
      }
    });

    return promise2;
  }
}

这里有五个要点,我在代码中加了注释标注,

- 要点一

then中的两个函数参数是可以省略的,也就是他有默认值,默认值成功时就返回这个值不管是是什么值,

失败的时候就是抛出一个错误。

- 要点二

为了让then可以链式调用,所以必须返回一个promise,这里就new一个promise,然后分为三类,也就是promise的三种状态

其中FULFILLED和REJECTED可以一起讨论,这两种情况,先执行then的两个函数参数之一,并将执行之后的返回值,去调用resolvePromise函数。

- 要点三

如果是上一步的then或者promise的executor函数是异步的就将这些函数放入订阅的数组,等时间到了再执行。

- 要点四

再要点二和要点三中会执行这个resolvePromise函数,这里先判断传入的值也就是那个x,不能和自己相等,这里给出一个例子

let p = new Promise((resolve, reject) => {
    resolve();
})
let promise2 = p.then(() => {
    return promise2;
})
promise2.then(null, (err) => {
    console.log(err)
})
------------------------
输出
[TypeError: Chaining cycle detected for promise #<Promise>]

如果返回值和自己相等,那么就会等待自己的状态返回再返回状态,这就是死循环了。所以这里要加一个判断。

- 要点五

如果返回的值是一个普通值,那么很简单直接传递给下一个then就好了,这里进行两次判断,先用是不是object和是不是function判断,

第二次取它的then判断,如果then是一个function那么就是一个promise,如果不是就直接当普通值传给下一个then,当然中途

可能出错,所以放到try中。

- 要点六

判断为promise之后,先执行promise,然后根据他的状态再往下走,如果是FULFILLED,那么需要继续递归调用resolvePromise,

因为这个的返回值还可能是一个promise,如果是REJECTED,那么直接传递给下一个then的reject即可

- 要点七

这里和promise类里面一样要加一个判断,以防调用完resolve之后又调用reject,也就是promise的状态不可逆转,

所以加一个判断变量,如果已经是成功或者失败的状态就直接返回

总结一下:

promise类有三种状态,等待成功失败,然后有两个值,一个是value,一个是reason,一个成功的值,一个是失败的原因,还有两个数组,一个是成功的回调数组一个是失败的回调数组,还有两个方法,一个是成功的方法,一个失败的方法,回调的时候如果状态变是成功,然后把这个返回值 值赋给类中的value,并且变为成功态,如果回调是时候是失败就把状态变为失败,并且把失败的返回值赋给reason,有一个对多次调用resolve和reject的阻断机制。executor函数会立即执行,去改变状态成功或者失败, 如果出错的话,就直接走失败的状态。

之后就是then函数,传入成功和失败两个函数,如果不传参数,就有两个默认值,然后new一个promise,判断状态,如果是等待就先放入回调数组,等到时间到了再执行,如果是成功或者失败就将这个函数的返回值得到,判断返回值是普通值或者promise,就调用resolvePromise

resolvePromise函数先判断传入的值是不是自己,不能循环调用,然后判断传入是值是普通值还是promise如果是promise就递归调用,如果是普通值就直接传给下一个then。 这些过程中都要用try,出错了就直接返回错误状态。

测试

Promisees/A+中有一个测试,检验写的promise是否符合规范,tests下载地址

先全局下载

npm install promises-aplus-tests -g

然后直接

promises-aplus-tests name(文件名字)

通过测试,至此就完成了一个完整的Promise

这里内容是从一个公开课中看到的,找到一个转载的网址 https://www.bilibili.com/video/BV1GU4y1q7DM?p=1&vd_source=6802f8b41b3e39392ba777a8a8b1ff32

还有一些参考引用的文章,后续会对promise的一些方法进行实现

https://juejin.cn/post/6844904096525189128

https://juejin.cn/post/6844903629187448845

https://juejin.cn/post/6844903796129136654

posted @ 2023-03-21 15:41  天然气之子  阅读(331)  评论(0编辑  收藏  举报