「3.4w字」超保姆级教程带你实现Promise的核心功能

Posted 星期一研究室

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「3.4w字」超保姆级教程带你实现Promise的核心功能相关的知识,希望对你有一定的参考价值。

保姆级详解promise的核心功能

📚序言

众所周知, promise 是前端面试中雷打不动的面试题了,面试官都很爱考。周一之前也是知识比较浮于表面,在一些面经上看到了 promise 的实现方式,就只停留在那个层面上。但实际上我发现,如果没有深入其原理去理解,面试官稍微变个法子来考,这道题很容易就把我给问倒了。所以呀,还是老老实实从头到尾研究一遍,这样等遇到了,不管怎么考,万宗不变其一,把原理理解了,就没有那么容易被问倒了。

下面开始进入本文的讲解~🏷️

📋文章内容抢先看

📰一、js的同步模式和异步模式

1. 单线程💡

大家都知道, js 的设计是基于单线程进行开发的,它原先的目的在于只参与浏览器中DOM节点的操作。

而对于单线程来说,其意味着只能执行一个任务,且所有的任务都会按照队列的模式进行排队。

所以,单线程的缺点就在于,当 js 运行的时候, html 是不会进行渲染的。因此,如果一个任务特别耗时,那么将会很容易造成页面阻塞的局面。

为了解决这个问题, js 提出了同步模式异步模式的解决方案。

2. 同步模式💡

(1)定义

所谓同步模式,指的就是函数中的调用堆栈,按照代码实现的顺序,一步步进行。

(2)图例

接下来我们来用一段代码,演示 js函数调用堆栈的执行情况。具体代码如下:

const func1 = () => {
    func2();
    console.log(3);
}

const func2 = () => {
    func3();
    console.log(4);
}

const func3 = () => {
	console.log(5);
}

func1(); //5 4 3

看到这里,相信很多小伙伴已经在构思其具体的执行顺序。下面用一张图来展示执行效果:

对于这个数据结构来说,它遵循后进先出的原则。因此,当 func1func2func3 依次放进调用栈后, 遵循后进先出原则 ,那么 func3 函数的内容会先被执行,之后是 func2 ,最后是 func1

因此,对于 js同步模式来说,就是类似于上述的函数调用堆栈。

3. 异步模式💡

(1)举例

当程序遇到网络请求或定时任务等问题时,这个时候会有一个等待时间。

假设一个定时器设置 10s ,如果放在同步任务里,同步任务会阻塞代码执行,我们会等待 10s 后才能看到我们想要的结果。1个定时器的等待时间可能还好,如果这个时候是100个定时器呢?我们总不能等待着 1000s 的时间就为了看到我们想要的结果吧,这几乎不太现实。

那么这个时候就需要异步,通过异步来让程序不阻塞代码执行灵活执行程序

(2)定义

对于同步模式来说,它只能自上而下地一行一行执行,一行一行进行解析。那与同步模式不同的是,异步模式是按照我们想要的结果进行输出,不会像同步模式一样产生阻塞,以达到让程序可控的效果。

(3)js如何实现异步

相对于同步模式来说,异步模式的结构更为复杂。除了调用栈之外, 它还有消息队列事件循环这两个额外的机制。所谓事件循环,也称为 event loop事件轮询。因为 js 是单线程的,且异步需要基于回调来实现,所以, event loop 就是异步回调的实现原理。

JS在程序中的执行遵循以下规则:

  • 从前到后,一行一行执行;
  • 如果某一行执行报错,则停止下面代码的执行;
  • 先把同步代码执行完,再执行异步。

一起来看一个实例:

console.log('Hi');

setTimeout(function cb1(){
    console.log('cb1'); //cb1 即callback回调函数
}, 5000);

console.log('Bye');

//打印顺序:
//Hi
//Bye
//cb1

从上例代码中可以看到, JS 是先执行同步代码,所以先打印 HiBye ,之后执行异步代码,打印出 cb1

以此代码为例,下面开始讲解 event loop 的过程。

(4)event loop过程

对于上面这段代码,执行过程如下图所示:

从上图中可以分析出这段代码的运行轨迹。首先 console.log('Hi') 是同步代码,直接执行并打印出 Hi 。接下来继续执行定时器 setTimeout定时器是异步代码,所以这个时候浏览器会将它交给 Web APIs 来处理这件事情,因此先把它放到 Web APIs 中,之后继续执行 console.log('Bye')console.log('Bye') 是同步代码,在调用堆栈 Call Stack 中执行,打印出 Bye

到这里,调用堆栈 Call Stack 里面的内容全部执行完毕,当调用堆栈的内容为空时,浏览器就会开始去 消息队列 Callback Queue 寻找下一个任务,此时消息队列就会去 Web API 里面寻找任务,遵循先进先出原则,找到了定时器,且定时器里面是回调函数 cb1 ,于是把回调函数 cb1 传入任务队列中,此时 Web API 也空了,任务队列里面的任务就会传入到调用堆栈里Call Stack 里执行,最终打印出 cb1

4. 回调函数💡

早期我们在解决异步问题的时候,基本上都是使用callback回调函数的形式 来调用的。形式如下:

//获取第一份数据
$.get(url1, (data1) => {
    console.log(data1);
    
    //获取第二份数据
    $.get(url2, (data2) => {
        console.log(data2);
        
        //获取第三份数据
        $.get(url3, (data3) => {
            console.log(data3);
            
            //还可以获取更多数据
        });
    });
});

从上述代码中可以看到,早期在调用数据的时候,都是一层套一层, callback 调用 callback ,仿佛深陷调用地狱一样,数据也被调用的非常乱七八糟的。所以,因为 callback 对开发如此不友好,也就有了后来的 promise 产生。

promiseCommonJS 社区最早提出,之后在2015年的时候, ES6 将其写进语言标准中,统一了它的用法,原生提供了 Promise 对象。 promise 的出现,告别了回调地狱时代,解决了回调地狱 callback hell 的问题。

那下面我们就来看看 Promise 的各种神奇用法~

📃二、Promise异步方案

1. Promise的三种状态📂

(1)Promise的三种状态

状态含义
pending等待状态,即在过程中,还没有结果。比如正在网络请求,或定时器没有到时间。
fulfilled满足状态,即事件已经解决了,并且成功了;当我们主动回调了 fulfilled 时,就处于该状态,并且会回调 then 函数。
rejected拒绝状态,即事件已经被拒绝了,也就是失败了;当我们主动回调了 reject 时,就处于该状态,并且会回调 catch 函数。

(2)状态解释

对于 Promise 来说,它是一个对象,用来表示一个异步任务在执行结束之后返回的结果,它有 3 种状态pendingfulfilledrejected其执行流程如下:

如果一个异步任务处于 pending 状态时,那么表示这个 promise 中的异步函数还未执行完毕,此时处于等待状态。相反,如果 promise 中的异步函数执行完毕之后,那么它只会走向两个结果:

  • fulfilled ,表示成功
  • rejected ,表示失败

一旦最终状态从 pending 变化为 fulfilled 或者 rejected 后,状态就再也不可逆

所以,总结来讲,Promise 对象有以下两个特点:

  • promise 对象的状态不受外界影响,一旦状态被唤起之后,函数就交由 web API 去处理,这个时候在函数主体中再执行任何操作都是没有用的
  • 只会出现 pendingfulfilled,或者 pendingrejected 状态,即要么成功要么失败。即使再对 promise 对象添加回调函数,也只会得到同样的结果,即它的状态都不会再发生被改变

2. 三种状态的变化和表现📂

(1)状态的变化

promise 主要有以上三种状态, pendingfulfilledrejected 。当返回一个 pending 状态的 promise 时,不会触发 thencatch 。当返回一个 fulfilled 状态时,会触发 then 回调函数。当返回一个 rejected 状态时,会触发 catch 回调函数。那在这几个状态之间,他们是怎么变化的呢?

1)演示1

先来看一段代码:

const p1 = new Promise((resolved, rejected) => {

});

console.log('p1', p1); //pending

在以上的这段代码中,控制台打印结果如下:

在这段代码中, p1 函数里面没有内容可以执行,所以一直在等待状态,因此是 pending

2)演示2

const p2 = new Promise((resolved, rejected) => {
    setTimeout(() => {
        resolved();
    });
});

console.log('p2', p2); //pending 一开始打印时
setTimeout(() => console.log('p2-setTimeout', p2)); //fulfilled

在以上的这段代码中,控制台打印结果如下:

在这段代码中, p2 一开始打印的是 pending 状态,因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时,才会触发到 resolved 函数,触发后返回一个 fulfilled 状态 promise

3)演示3

const p3 = new Promise((resolved, rejected) => {
    setTimeout(() => {
        rejected();
    });
});

console.log('p3', p3);
setTimeout(() => console.log('p3-setTimeout', p3)); //rejected

在以上的这段代码中,控制台打印结果如下。

在这段代码中, p3 一开始打印的是 pending 状态,因为它没有执行到 setTimeout 里面。等到后续执行 setTimeout 时,同样地,会触发到 rejected 函数,触发后返回一个 rejected 状态的 promise

看完 promise 状态的变化后,相信大家对 promise 的三种状态分别在什么时候触发会有一定的了解。那么我们接下来继续看 promise 状态的表现。

(2)状态的表现

  • pending 状态,不会触发 thencatch
  • fulfilled 状态,会触发后续的 then 回调函数。
  • rejected 状态,会触发后续的 catch 回调函数。

我们来演示一下。

1)演示1

const p1 = Promise.resolve(100); //fulfilled
console.log('p1', p1);
p1.then(data => {
    console.log('data', data);
}).catch(err => {
    console.error('err', err);
});

在以上的这段代码中,控制台打印结果如下:

在这段代码中, p1 调用 promise 中的 resolved 回调函数,此时执行时, p1 属于 fulfilled 状态, fulfilled 状态下,只会触发 .then 回调函数,不会触发 .catch ,所以最终打印出 data 100

2)演示2

const p2 = Promise.reject('404'); //rejected
console.log('p2', p2);
p2.then(data => {
    console.log('data2', data);
}).catch(err => {
    console.log('err2', err);
})

在以上的这段代码中,控制台打印结果如下:

在这段代码中, p2 调用 promise 中的 reject 回调函数,此时执行时, p1 属于 reject 状态, reject 状态下,只会触发 .catch 回调函数,不会触发 .then ,所以最终打印出 err2 404

3. Promise的使用案例📂

对三种状态有了基础了解之后,我们用一个案例来精进对 Promise 的使用。现在,我们想要实现的功能是,通过 fs 模块,异步地调用本地的文件。如果文件存在,那么在控制台上输出文件的内容;如果文件不存在,则将抛出异常。实现代码如下:

const fs = require('fs');

const readFile = (filename) => {
    // 返回一个 promise 实例,以供 then 调用
    const promise = new Promise(function(resolve, reject){
        // 使用 readFile 去异步地读取文件,异步调用也是 promise 函数的意义
        // 注意:下面这个函数的逻辑是错误优先,也就是先err,再data
        fs.readFile(filename, (err, data) => {
            // 如果文件读取失败,就调取 reject ,并抛出异常
            if(err){
                reject(err);
            }else{
                // 如果成功,就调取 resolve ,并返回调用成功的数据
                resolve(data);
            }
        });
    });
    return promise;
}

// 测试代码
// 文件存在逻辑
const existedFile = readFile('./test.txt');
existedFile.then(
    (data) => {
        // Buffer.from()方法用于创建包含指定字符串,数组或缓冲区的新缓冲区。
        // Buffer.from(data).toString()读出文件里面的内容。文件里面记得写内容!!
        console.log('content: ', Buffer.from(data).toString());
    },
    (error) => {
        console.log(error);
    }
)

// 文件不存在逻辑
const failFile = readFile('./fail.txt');
failFile.then(
    (data) => {
        console.log(Buffer.from(data).toString());
    },
    (err) => {
        console.log(err);
    }
);

最终控制台的打印结果如下:

[Error: ENOENT: no such file or directory, open 'C:\\\\promise\\\\fail.txt'] {
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path: 'C:\\\\promise\\\\fail.txt'
}
content:  这是一个测试文件!

大家可以看到,当 ./test.txt 文件存在时,那么 existedFile 会去调用后续的 .then 回调函数,因此最终返回调用成功的结果。注意,这是一个测试文件! 这行字就是 test 文件里面的内容。

同时, ./fail.txt 文件不存在,因此 failFile 会调用后续的 .catch 文件,同时将异常抛出。

现在,大家应该对 promise 的使用有了一定的了解,下面我们继续看 promisethencatch 对状态的影响。

4. then和catch对状态的影响📂

  • then 正常返回 fulfilled ,里面有报错则返回 rejected
  • catch 正常返回 fulfilled ,里面有报错则返回 rejected

我们先来看第一条规则: then 正常返回 fulfilled ,里面有报错则返回 rejected

1)演示1

const p1 = Promise.resolve().then(() => {
    return 100;
})
console.log('p1', p1); //fulfilled状态,会触发后续的.then回调
p1.then(() => {
    console.log('123');
});

在以上的这段代码中,控制台打印结果如下。

在这段代码中, p1 调用 promise 中的 resolve 回调函数,此时执行时, p1 正常返回 fulfilled , 不报错,所以最终打印出 123

2)演示2

const p2 = Promise.resolve().then(() => {
    throw new Error('then error');
});
console.log('p2', p2); //rejected状态,触发后续.catch回调
p2.then(() => {
    console.log('456');
}).catch(err => {
    console.error('err404', err);
});

在以上的这段代码中,控制台打印结果如下。

在这段代码中, p2 调用 promise 中的 resolve 回调函数,此时执行时, p2 在执行过程中,抛出了一个 Error ,所以,里面有报错则返回 rejected 状态 , 所以最终打印出 err404 Error: then error 的结果。

我们再来看第二条规则catch 正常返回 fulfilled ,里面有报错则返回 rejected

1)演示1(需特别谨慎! !)

const p3 = Promise.reject('my error').catch(err => {
    console.error(err);
});
console.log('p3', p3); //fulfilled状态,注意!触发后续.then回调
p3.then(() => {
    console.log(100);
});

在以上的这段代码中,控制台打印结果如下。

在这段代码中, p3 调用 promise 中的 rejected 回调函数,此时执行时, p3 在执行过程中,正常返回了一个 Error这个点需要特别谨慎!!这看起来似乎有点违背常理,但对于 promise 来说,不管时调用 resolved 还是 rejected ,只要是正常返回而没有抛出异常,都是返回 fulfilled 状态。所以,最终 p3 的状态是 fulfilled 状态,且因为是 fulfilled 状态,之后还可以继续调用 .then 函数。

2)演示2

const p4 = Promise.reject('my error').catch(err => {
    throw new Error('catch err');
});
console.log('p4', p4); //rejected状态,触发.catch回调函数
p4.then(() => {
    console.log(200);
}).catch(() => {
    console.log('some err');
});

在以上的这段代码中,控制台打印结果如下。

在这段代码中, p4 依然调用 promise 中的 reject 回调函数,此时执行时, p4 在执行过程中,抛出了一个 Error ,所以,里面有报错则返回 rejected 状态 , 此时 p4 的状态为 rejected ,之后触发后续的 .catch 回调函数。所以最终打印出 some err 的结果。

5. Promise的并行执行📂

(1)Promise.all

Promise.all 方法用于将多个 Promise 实例包装成一个新的 Promise 实例。比如:

var p = Promise.all([p1, p2, p3]);

p的状态由 p1p2p3 决定,分成两种情况:

  • 只有 p1p2p3 的状态都变为 fulfilled ,最终 p 的状态才会变为 fulfilled 。此时 p1p2p3 的返回值组成一个数组,并返回给 p 回调函数。
  • 只要 p1p2p3 这三个参数中有任何一个被 rejected , 那么 p 的状态就会变成 rejected 。此时第一个被 rejected 的实例的返回值将会返回给 p 的回调函数。

下面用一个实例来展示 Promise.all 的使用方式。具体代码如下:

//生成一个Promise对象的数组
var promises = [4, 8, 16, 74, 25].map(function (id) {
    return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(fucntion (posts) {
	// ...
}).catch(function (reason[1w6k 字详细讲解] 保姆级一步一步带你实现 Promise 的核心功能

10W+字C语言从入门到精通保姆级教程(2021版下)

10W+ 字C语言从入门到精通保姆级教程(2021版上)

[保姆级万字教程]打造最迷人的S曲线----带你从零手撕基于Huffman编码的文件压缩项目

[保姆级万字教程]打造最迷人的S曲线----带你从零手撕基于Huffman编码的文件压缩项目

[保姆级万字教程]打造最迷人的S曲线----带你从零手撕基于Huffman编码的文件压缩项目