「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
看到这里,相信很多小伙伴已经在构思其具体的执行顺序。下面用一张图来展示执行效果:
对于栈这个数据结构来说,它遵循后进先出的原则。因此,当 func1
, func2
, func3
依次放进调用栈后, 遵循后进先出原则 ,那么 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
是先执行同步代码,所以先打印 Hi
和 Bye
,之后执行异步代码,打印出 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
产生。
promise
由 CommonJS
社区最早提出,之后在2015年的时候, ES6
将其写进语言标准中,统一了它的用法,原生提供了 Promise
对象。 promise
的出现,告别了回调地狱时代,解决了回调地狱 callback hell
的问题。
那下面我们就来看看 Promise
的各种神奇用法~
📃二、Promise异步方案
1. Promise的三种状态📂
(1)Promise的三种状态
状态 | 含义 |
---|---|
pending | 等待状态,即在过程中,还没有结果。比如正在网络请求,或定时器没有到时间。 |
fulfilled | 满足状态,即事件已经解决了,并且成功了;当我们主动回调了 fulfilled 时,就处于该状态,并且会回调 then 函数。 |
rejected | 拒绝状态,即事件已经被拒绝了,也就是失败了;当我们主动回调了 reject 时,就处于该状态,并且会回调 catch 函数。 |
(2)状态解释
对于 Promise
来说,它是一个对象,用来表示一个异步任务在执行结束之后返回的结果,它有 3 种状态: pending
, fulfilled
, rejected
。其执行流程如下:
如果一个异步任务处于 pending
状态时,那么表示这个 promise
中的异步函数还未执行完毕,此时处于等待状态。相反,如果 promise
中的异步函数执行完毕之后,那么它只会走向两个结果:
fulfilled
,表示成功;rejected
,表示失败。
一旦最终状态从 pending
变化为 fulfilled
或者 rejected
后,状态就再也不可逆。
所以,总结来讲,Promise
对象有以下两个特点:
promise
对象的状态不受外界影响,一旦状态被唤起之后,函数就交由web API
去处理,这个时候在函数主体中再执行任何操作都是没有用的;- 只会出现
pending
→fulfilled
,或者pending
→rejected
状态,即要么成功要么失败。即使再对promise
对象添加回调函数,也只会得到同样的结果,即它的状态都不会再发生被改变。
2. 三种状态的变化和表现📂
(1)状态的变化
promise
主要有以上三种状态, pending
、 fulfilled
和 rejected
。当返回一个 pending
状态的 promise
时,不会触发 then
和 catch
。当返回一个 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
状态,不会触发then
和catch
。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
的使用有了一定的了解,下面我们继续看 promise
中 then
和 catch
对状态的影响。
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的状态由 p1
、 p2
、 p3
决定,分成两种情况:
- 只有
p1
、p2
、p3
的状态都变为fulfilled
,最终p
的状态才会变为fulfilled
。此时p1
、p2
、p3
的返回值组成一个数组,并返回给p
回调函数。 - 只要
p1
、p2
、p3
这三个参数中有任何一个被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 的核心功能
[保姆级万字教程]打造最迷人的S曲线----带你从零手撕基于Huffman编码的文件压缩项目