【Promise】手写简版Promise以及Promise.all

166 阅读9分钟

highlight: dark theme: smartblue

其实是在学习Node的时候延伸到Promise的,如果已经比较了解的小伙伴可以直接跳到Promise部分哈~

Node.js是什么

Node.js是运行在服务器端的JavaScript

特点:1 单线程(一个服务员)2 异步 3 非阻塞

Node.js和js有什么区别?

区别:

  • JavaScript是包含ECMAScriptDOMBOM 而后两者是浏览器独有的, Node.js没有

  • js是执行在浏览器中的语言,浏览器通过v8引擎来编译js称为机器语言,而Node.js就是v8引擎的一个容器 使得js能够脱离浏览器运行

联系:

Node.js借助js的单线程 实现了服务端对开销的降低(不像java来一个请求 就分配一个线程)

通过它我们可以读写电脑上的文件,连接数据库,充当web服务器(而web server的作用就是处理http请求)

如:写了js代码之后node 文件名.js就能输出在终端,而不需要在浏览器中去运行

同步是什么

各方都实时(或者尽可能实时)地收取(而且必要的话也处理或者回复)信息的即时沟通方式,即为同步

电话即为一个日常的例子:人们都倾向于在使用电话时及时地作出回应。

特点:

  • 通常代码都是从上至下一行一行的执行
  • 前面的代码不执行后面的也无法执行,所以如果遇到长时间等待的,会阻塞

(堵车,前面车祸了,后面的车都走不了,感觉也很像TCP三次握手和四次挥手)

这样的机制就会出现阻塞的问题

js如何解决同步可能带来的阻塞问题呢?(JAVA & Python通过多线程来解决)

而我们的js本来就是单线程的 用到Node.js中也是单线程 那是怎么解决同步带来的问题的呢?

通过异步的方式来解决,也就是说可能会阻塞的 执行时间长的 我等他先执行着,我先去干别的事

(好像小时候的数学题问你怎么规划时间,煮牛奶5分钟,看电视20分钟

同步就像是我一定要先煮好牛奶 才能去看电视,或者看完电视再去煮牛奶,一共耗时25分钟

而异步就像是我可以在看电视的同时 让电磁炉在旁边煮着牛奶,而5分钟到的时候,闹钟会响,这样就只耗时20分钟)

异步

异步的概念

异步指两个或两个以上的对象或事件同时存在或发生(或多个相关事物的发生无需等待其前一事物的完成),(说人话:一段代码的执行不影响其他后面的执行)

异步的特点

  • 不会阻塞其他代码执行
  • 需要回调函数返回结果

如何实现异步

JavaScript解决异步任务的几种方式:

  1. 回调函数
  2. 事件监听
  3. 发布/订阅
  4. Promise对象
  5. Generator函数
  6. async/await(Generator 函数的语法糖)

1 回调函数

函数f1被作为实参传入另一函数f2,并在该外部函数内被调用,用以来完成某些任务的函数,称为回调函数

2 事件监听

采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

3 发布/订阅

假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

4 Promise

但是异步也会带来问题,就是它无法像同步一样立马拿到return的结果,如果立马返回,是返回的undefined的(还是刚才的例子,我在看电视的一开始 是没有办法拿到热牛奶的)

那我在异步的情况下如何拿到之前代码执行的结果呢? 通过回调函数(也相当于煮牛奶定的闹钟,煮好了通知我去拿)

但回调函数会带来地狱回调的问题,并且可调试性差,所以用Promise来代替回调给我们返回结果

所以由上面的推导我们知道了

  • promise可以帮我们解决异步中的地狱回调函数问题
  • promise是一个用来存储数据对象的容器,由于存取方式特殊所以可以将异步调用结果保存到promise中

what:就是一个容器,里面放的就是异步调用结果

how:如何做存取操作的

why:回调函数的形式如果回调太多不方便调试

ad : 帮我们解决地狱回调问题

disad: Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚

4.1 创建Promise

Promise是一个构造函数,可以实例化一个对象出来p,而在实例化对象的时候需要接收一个参数,这个参数是一个函数类型的参数,里面包裹一个异步操作

const p = new Promise(()=>{})

进一步,这个函数有两个函数类型的形参,分别叫resolve,reject,当包裹的异步任务成 功时调resolve,异步任务失败的时候调reject

const p = new Promise((resolve,reject)=>{
    //异步操作代码
        setTimeout(()=>{
            if(成功){
                //console.log("你成功了");
                resolve(); //将p的状态设置为成功,对应执行then第一个回调
            } 
            else{
                //console.log("你失败了");
                reject();//将p的状态设置为失败,对应执行then第二个回调
            }
        },1000);
})

存储数据通过resolve,reject这两个函数,也就是说resolve函数现在里面存的就是注释掉的"你成功了",那么它在哪里进行内容的输出呢?

取用数据通过Promise的实例promise的方法 then((result)=>{},(erro)=>{}) ,它同样接受两个参数,两个参数都是函数,第一个参数是成功时的回调,第二个参数是失败时的回调

const p = new Promise((resolve,reject)=>{
    //异步操作代码
        setTimeout(()=>{
            if(成功){
                //console.log("你成功了");
                resolve();
            } 
            else{
                //console.log("你失败了");
                reject();
            }
        },1000);
})

p.then((result)=>{
    console.log("你成功了");
},(erro)=>{
    console.log("你失败了");
})

(what?那还是有可能回调函数嵌套啊....Promise是如何解决的呢?)

取到的原理:Promise中维护了两个隐藏的属性 [[PromiseState]] [[PromiseResult]]

1678698919195.png

同样的代码用p接收后返回了新的promise对象,但是又没有通过resolve存储值,所以[[PromiseResult]]为undefined

[[PromiseResult]]用来存储数据

[[PromiseState]]用来记录promise的以下三种状态 (且只会被修改一次)

  • 待定(pending) :初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled) :意味着操作成功完成。
  • 已拒绝(rejected) :意味着操作失败。

状态在通过resolve存储数据之后就会发生从pending -> fulfilled的改变

状态在通过reject 存储数据之后就会发生从pending -> rejected的改变

4.2 Promise如何解决回调地狱的?

promise在调用then catch finally这三个方法的时候会返回新的Promise(其实新旧好像都可以,只要满足promise/A+规范就行)并且把回调函数的返回值放到新的Promise中的[[PromiseResult]],通过then catch等继续调用就可以避免地狱回调问题

image.png

也就是说我们后面的方法(then和catch)就可以读取上一步的结果,但如果上一步的执行结果不是想要的结果就会被跳过

Promise的静态方法

(静态方法就是类方法 直接通过类去调的,与上面的then,catch,finally实例方法区分开~)

Promise.resolve()

创建一个立即完成的Promise,不要在解析为自身的回调上调用Promise.resolve。这将导致无限递归

//创建立即成功的promise
Promise.resolve(10).then(res => console.log(res));
//等价于
let promise = new Promise((resolve,reject)=>{
  resolve(10);
})
promise.then(res =>{console.log(res)});

//MDN上会造成死循环的情况
let thenable = {
  then: (resolve, reject) => {
    resolve(thenable)
  }
}
Promise.resolve(thenable) //这会造成一个死循环

Promise.reject()

创建一个立即拒绝的Promise,使用和上面差不多

Promise.all()

Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set 都属于 ES6 的 iterable 类型)的输入,并且只返回一个Promise实例 其中有一个报错就会报错(讲求一个同生共死)

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([p1, p2, p3]).then(values => {
  console.log(values); // [3, 1337, "foo"]
});

手写Promise.myAll

  • 静态方法,在Promise上写,而不是原型上写 promise.prototype.all是错的
  • all的参数(Promise数组) all的返回值(新Promise对象)
  • 用数组来记录结果
  • 同生共死,只要有一个reject,就返回reject
Promise.myAll = function(list){ //myAll的参数
  const results = []; //用来存放返回的结果
  let count = 0;
  //myAll的返回结果是一个promise对象
  return new Promise((resolve,reject)=>{
    list.map((promise,index)=>{
      promise.then((r)=>{
        results[index] = r;
        count += 1;
        if(count === list.length){
          resolve(result);
        }
      },(reason)=>{
        reject(reson);
      })
    })
  })
}

手写简单版Promise

        class myPromise{
            #status = 'pending' //默认不能改的私有属性
            constructor(fn){
                this.q = []; //链式调用的成功队列和失败队列
                const resolve = (data)=>{
                    // this.#status = 'fulfilled'
                    const f1f2 = this.q.shift()
                    if(!f1f2 || !f1f2[0]) return
                    const x = f1f2[0].call(undefined,data)
                    if(x instanceof myPromise){
                        x.then((data)=>{
                            //调用下一组队列里的f1
                            resolve(data)
                        }
                        ,(reason)=>{
                            //调用下一组队列里的f2
                            reject(reason)
                        })
                    }else{
                        //else就是成功了。调用下一组队列里的f1
                        resolve(x)
                    }
                }
                const reject = (reason)=>{
                    this.#status = 'rejected'
                    const f1f2 = this.q.shift()
                    if(!f1f2 || !f1f2[1]) return
                    const x = f1f2[1].call(undefined,reason)
                    if(x instanceof myPromise){
                        x.then((data)=>{
                            //调用下一组队列里的f1
                            resolve(data)
                        }
                        ,(reason)=>{
                            //调用下一组队列里的f2
                            reject(reason)
                        })
                    }else{
                        //else就是成功了。调用下一组队列里的f1
                        resolve(x)
                    }
                }
                fn.call(undefined,resolve,reject)
            }
            //根据promise A+规范then必须返回一个promise
            then(f1,f2){
                this.q.push([f1,f2]);
            }
        }
        const promise = new myPromise(function(resolve,reject){
            setTimeout(function(){
                resolve('hi')
            },3000);
        })
        promise.then((data)=>{console.log(data)},(reason)=>{console.error(reason)});

参考文章:

我终于搞懂了async/await、promise和setTimeout的执行顺序

Javascript异步编程的4种方法