Browse Source

two lessons

master
jingxun 2 years ago
parent
commit
2434bc672d
  1. 1
      .gitignore
  2. 43
      code/ajax_promise.html
  3. 20
      code/fs_test.js
  4. 1
      code/resource/content.txt
  5. 40
      code/start_promise.html
  6. 217
      note/lesson01_introduce_and_start.md
  7. 153
      note/lesson02_use_promise_test.md

1
.gitignore

@ -0,0 +1 @@
.DS_Store

43
code/ajax_promise.html

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Promise Ajax</title>
<link rel="stylesheet" href="https://file.lynchow.com/bootstrap.min.css">
</head>
<body>
<div class="container">
<h2 class="page-header">Promise AJAX</h2>
<button id="btn" class="btn btn-primary">Click to send ajax</button>
</div>
<script>
const btn = document.getElementById("btn");
btn.addEventListener(
"click",
() => {
let p = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = "json";
xhr.open("GET", "https://api.apiopen.top/getJoke");
xhr.send();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.status);
}
}
}
});
p.then(
value => console.log(value),
reason => console.log(reason)
);
});
</script>
</body>
</html>

20
code/fs_test.js

@ -0,0 +1,20 @@
const fs = require("fs");
// fs.readFile("./resource/content.txt", (err, data) => {
// if (err) throw err;
// console.log(data.toString());
// });
let p = new Promise((resolve, reject) => {
fs.readFile("./resource/contentt.txt", (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
});
});
p.then(
value => console.log(value.toString()),
reason => console.log(reason)
)

1
code/resource/content.txt

@ -0,0 +1 @@
test file

40
code/start_promise.html

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Promise</title>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<h2 class="page-header">Start Promise</h2>
<button id="btn" class="btn btn-primary">Click</button>
</div>
<script>
function rand(m, n) {
return Math.ceil(Math.random() * (n - m + 1)) + m - 1;
}
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
let n = rand(1, 100);
if (n <= 50) {
resolve(n);
} else {
reject(n);
}
}, 2000);
});
p.then(
(value) => { alert("You clicked the button " + value); },
(reason) => { alert("Click the button failed " + reason); }
);
});
</script>
</body>
</html>

217
note/lesson01_introduce_and_start.md

@ -0,0 +1,217 @@
# `Promise`介绍与初体验
在前端的工作中从表面上来看无非就是两个要点,静态的页面结构和动态的页面数据。我们之前出了一套`react`全家桶的教程,而且在教程的开篇我们就已经有介绍过,`react`是一个只关注页面的`js`框架。就是说`react`框架只为我们解决页面层面的问题,但是数据从哪来呢?`react`并不关心。
我们大家都知道,现在的`web`页面大多都是前后端分离的,前端和后端都用数据来进行相应的交互。那么自然而然我们都知道,前端的动态数据都是由页面动态地向后端发送网络请求,然后由后端服务器返回相应请求所需要的相应的数据。也就是说我们的前端框架不关心我们怎么发送请求,那就代表着发送请求我们得自己来实现。
当然了,我们其实对发送请求都并不陌生了,`ajax`嘛,而且我们也知道发送`ajax`请求有很多种方法,比如:
- 原生`ajax`请求
- `jQuery`发送`ajax`请求
- `fetch`发送`ajax`请求
- `axios`发送`ajax`请求
不过现如今的市场上,前端用`axios`发送`ajax`请求这个方法应用的十分广泛。而`axios`是一个基于`Promise`的网络请求库。那么我们今天就来介绍一下`Promise`。
## 什么是`Promise`
按照我们常规的思路来说,当我们拿到一个陌生的东西,第一件事是问一句,这玩意儿是什么?那么我们这里也一样,什么是`Promise`?
抽象点来说呢,就是大家以及网上面很官方的回答:`Promise`是一个新技术,是`js`异步编程的新解决方案。这一句话我们仿佛并没法理解到什么东西,只知道是一个新的解决方案,但是这个解决方案是什么呢?我们知道异步编程的旧方案是单纯地使用回调函数,那么这个新方案是什么?
那么我们具体点来说,`Promise`从语法上来说其实就是一个构造函数可以进行对象的实例化。从功能上来说呢,这个`Promise`对象可以用来封装一个异步操作,并且可以获取这个异步操作的成功或者失败时的结果值。
## 异步编程
我们现在从抽象和具体层面都介绍了`Promise`是什么,但是可能大家还是有些迷糊,什么意思啊这一大串?我们知道了这个玩意儿是一个构造函数,既然是构造函数那就能实例化成一个对象,那么这个对象里面是什么呢?这个对象可以封装一个异步操作。
那么好,我们说到了这个东西是异步编程的解决方案,而且这个东西实例化的对象可以封装一个异步操作,这里出现了相同的词,异步。那么我们`js`中的常见的异步操作都有哪些呢?
- `fs`文件操作
- 数据库操作
- `ajax`网络请求
- 定时器
以上这些比如像`fs`文件操作:
```js
require("fs").readFile("path", (error, data) => {});
```
这是一个简单的文件读取的`demo`,`readFile`的参数第一个是文件路径,第二个是一个回调函数。我们再来看`ajax`请求的`demo`
```js
$.get("/path", (data) => {});
```
我们可以看到,这和文件读取差不多,`get`方法中的参数,第一个是`url`请求路径,第二个也是一个回调函数。
但是以上这两个`demo`中都是用回调函数来做的异步操作,这就是`js`异步编程的旧方案,单纯地在使用回调函数来完成一系列的异步操作。现在有了`Promise`这个新的解决方案,是不是就可以把这些异步操作都封装到`Promise`对象里呢?
## 为什么要使用`Promise`
我们知道了`Promise`是一个构造函数,可以封装异步操作,那么我们为什么要用`Promise`呢?我们就直接用回调函数也可以实现我们的异步编程啊。当然了,我们就得直接用回调函数来实现也是可以的。但是`Promise`自然是有一定的优势的。
1. `Promise`支持链式调用,可以解决回调地狱的问题
什么意思呢?比如说,我们一个异步操作中的回调函数里如果要再次嵌套一个回调函数,而嵌套的回调函数中依然需要嵌套回调函数比如下面这个`demo`:
```js
asyncFunc1(opt,(...args1) => {
asyncFunc2(opt,(...args2) =>{
asyncFunc3(op,(...args3) => {
asyncFunc4(opt,(...args4) =>{
//some operation });
});
});
});
});
```
我们来看这个`demo`,一个异步回调里面嵌套了另一个异步操作,被嵌套的异步操作里面又嵌套了异步操作,这样的话我们是很不方便阅读代码的,而且这样的话我们代码如果有了异常也是很不方便处理的。因为我们每一层进入异步任务的时候我们都要对这个错误来进行一个相应的处理,可能会写很多重复性代码。
2. 指定回调函数的方式更加灵活
怎么说呢?我们在用旧的异步编程方案的时候,在进行异步任务之前就得把回调函数给指定好。但是我们用`Promise`的话,我们可以先启动异步任务,返回一个`Promise`对象,然后再在后面给`Promise`对象来绑定回调函数,甚至我们还可以在异步任务之后来指定多个回调函数。
## `Promise`初体验
上面说了这么多了,那么我们来尝试使用一下`Promise`吧,我们来看这样一个`demo`:
![image-20220307111524158](https://file.lynchow.com/2022-03-07-031526.png)
我们这边有一个按钮,当我们点击按钮之后,延迟两秒之后弹窗提醒。我们先来用最原始的回调函数来实现以下:
```html
<div class="container">
<h2 class="page-header">Start Promise</h2>
<button id="btn" class="btn btn-primary">Click</button>
</div>
<script>
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
setTimeout(() => {
alert("You clicked the button")
}, 2000);
});
</script>
```
我们来看这段代码,我们页面上有按钮,首先我们得先获取到按钮,然后添加点击事件,在点击事件中我们用定时器设置让弹窗延迟两秒之后再弹出。那么我们来看一下效果:
![iShot2022-03-07 11.28.42](https://file.lynchow.com/2022-03-07-032939.gif)
我我这边这个录制软件不太好用,但是也是可以看出来我们是点击之后等了一会儿然后才弹窗的。这便是我们用纯回调函数来完成的异步操作。那么我们接下来用`Promise`来进行一个封装应该怎么处理呢?
```js
function rand(m, n) {
return Math.ceil(Math.random() * (n - m + 1)) + m - 1;
}
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
let n = rand(1, 100);
if (n <= 30) {
resolve();
} else {
reject();
}
}, 2000);
});
p.then(
() => { alert("You clicked the button") },
() => { alert("Click the button failed") }
);
});
```
我们来看上面这一段代码,我们说过`Promise`是一个构造函数,可以实例化成对象,那么我们在绑定点击事件的时候,我们实例化一个`Promise`对象,在我们实例化`Promise`对象要传一个参数,这个参数需要是一个函数类型的参数。而且这个函数要求接收两个函数作为参数,这两个函数分别是`resolve`和`reject。`
这两个函数分别为异步任务成功与失败时所执行的函数。我们用随机数来模拟任务的成功与失败,当我们生成的随机数小于等于30时,我们认为异步任务是成功的,我们调用`resolve`函数,否则我们认为异步任务是失败的,我们调用`reject`函数。
这两个函数是有一个特点的,就是`resolve`和`reject`函数在执行之后分别会将`Promise`对象的状态修改为成功或者失败。然后接下来我们调用`Promise`对象的`then`方法,而`then`方法会接收两个函数,并且会根据`Promise`对象的状态来判断执行哪个函数,如果状态为成功,则会执行第一个函数,否则会执行第二个函数。
那么我们来看一下效果:
![iShot2022-03-07 15.08.06](https://file.lynchow.com/2022-03-07-070851.gif)
我们看到我两次点击分别延迟了2秒而且两次点击一次成功一次失败。这就是`Promise`的基本流程:
1. 实例化`Promise`对象
1. 实例化`Promise`对象要接收一个函数类型的参数
2. 该参数要接收两个函数`resolve`和`reject`,异步任务成功执行`resolve`,失败执行`reject`,并且这两个函数会修改`Promise`对象的状态
3. `Promise`对象包裹一个异步操作
2. `Promise`对象调用`then`方法,并传入我们自定义的两个函数。
3. `then`方法将通过`Promise`对象的状态来判断执行哪一个函数,如果状态为成功则执行第一个,否则执行第二个。
## `Promise`获取异步任务的结果值
我在前文中提到,用`Promise`来处理异步操作问题是可以拿到异步任务成功或者失败的结果值的。那么怎么拿呢?我们用案例来说明一下,比如我现在还是之前的那个点击按钮,延迟两秒之后弹窗提醒,我们前文的案例中是根据我们生成的随机数来判断是成功还是失败,我们也成功实现了这个需求,但是我们现在进一步加一个需求,我们要在弹窗中显示出我们生成的随机数是多少。那么怎么处理呢?
```js
btn.addEventListener("click", () => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
let n = rand(1, 100);
if (n <= 30) {
resolve();
} else {
reject();
}
}, 2000);
});
p.then(
() => { alert("You clicked the button") },
() => { alert("Click the button failed") }
);
});
```
我们先来回顾一下之前的代码,我们的随机数是在什么时候生成的?是不是在实例化`Promise`对象的时候,我们在我们的异步任务中声明定义了一个随机数`n`?那么我们是在什么时候弹窗的呢?是不是异步任务启动了之后,然后根据异步任务的成功与否执行了`resolve`或者`reject`函数?然后我们在通过`Promise`对象调用`then`方法的时候根据`Promise`对象的状态来执行我们传入的函数才弹出的窗口?
那么我们在`then`方法中定义的函数可以直接拿到实例化`Promise`对象时声明的`n`变量吗?是不是没法直接拿到?但是,我们知道`Promise`是可以拿到的,那么我们怎么处理呢?
```js
btn.addEventListener("click", () => {
const p = new Promise((resolve, reject) => {
setTimeout(() => {
let n = rand(1, 100);
if (n <= 50) {
resolve(n);
} else {
reject(n);
}
}, 2000);
});
p.then(
(value) => { alert("You clicked the button " + value); },
(reason) => { alert("Click the button failed " + reason); }
);
});
```
我们来看这段代码,我们一个函数想要在函数体中拿到一个函数之外的变量是不是只有一个办法?那就是作为参数传进来。所以说我们要想在`then`方法接收到的函数中使用我们在其他地方声明的变量,那么这两个函数是不是就得预留一个参数来接收?
但是我们虽然预留了这个参数的位置了,那么谁会把数据传进来呢?这两个函数是传给`Promise`对象的`then`方法的,而且要根据`Promise`对象的状态来决定执行哪个函数,那么会不会这个数据就是要由`Promise`对象的状态传进来呢?我们来想一下,我们的随机数是在异步任务中声明的,就目前而言,这个异步任务不论成功与否,都会有且仅有一个特别的函数被执行,而且这个函数会修改`Promise`对象的状态,那么我们是不是只要把这个随机数传给这个能够修改`Promise`对象的状态的函数就行了呢?那么我们来看一下效果验证一下。
![image-20220307155406779](https://file.lynchow.com/2022-03-07-075409.png)
我们看见我们成功在弹窗中展示了我们生成的随机数,也就代表着我们将结果值传给`resolve`和`reject`函数,这两个函数中任意一个被执行之后,在调用`then`方法的时候,`then`方法中的函数都能接收到这个结果值。
以上便是`Promise`的初体验
## 总结
- `Promise`可以封装一个异步操作
- 实例化`Promise`对象
1. 实例化`Promise`对象要接收一个函数类型的参数
2. 该参数要接收两个函数`resolve`和`reject。`
3. `Promise`对象包裹一个异步操作
4. 异步任务成功执行`resolve`,失败执行`reject`,这两个函数都可以接收异步任务执行的结果值,并修改`Promise`对象的状态
- `Promise`对象调用`then`方法,并传入我们自定义的两个函数。
- 我们定义的函数可以接收我们异步任务的结果值作为参数
- `then`方法将通过`Promise`对象的状态来判断执行哪一个函数,如果状态为成功则执行第一个,否则执行第二个。

153
note/lesson02_use_promise_test.md

@ -0,0 +1,153 @@
# `Promise`实践练习
上节课我们介绍了`Promise`是什么以及为什么我们要使用`Promise`,而且通过案例来介绍了`Promise`的基本使用方法以及工作流程,那么这节课我们来做一下`Promise`的实践练习。
## `fs`文件操作
大家可能有些人知道`fs`的用途是什么,这是一个可以从计算机硬盘读取文件的一个模块,我们现在有一个需求,要求我们来读取当前目录下的`resource`目录中的`content.txt`文件。那么我们怎么处理?
### 纯回调函数实现
```js
const fs = require("fs");
fs.readFile("./resource/content.txt", (err, data) => {
if (err) throw err;
console.log(data);
});
```
首先我们用纯回调函数来实现一下,我们先是导入了`fs`库,然后调用`readFile`方法来读取文件,这个方法接收两个参数,第一个是文件路径,第二个是一个回调函数,这个回调函数中也接收两个参数,一个是异常,一个是读取到的文件内容。我们来看回调函数体,如果有异常出现,就抛出异常,否则就打印文件内容。我们来执行一下这段代码。
![image-20220307162349453](https://file.lynchow.com/2022-03-07-082400.png)
我们来看一下,代码执行成功了,打印出了一串`buffer`,这是看不见文件内部的实际内容的。那么我们在`data`后面加一个`toString()`专成字符串输出再看一下
![image-20220307162556747](https://file.lynchow.com/2022-03-07-082558.png)
这次我们看见我们打印出来的文件内容中就写了`test file`这两个单词。
### `Promise`方式实现
那么我们如果用`Promise`来封装这个操作应该怎么写呢?我们来看代码:
```js
const fs = require("fs");
let p = new Promise((resolve, reject) => {
fs.readFile("./resource/contentt.txt", (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
p.then(
value => console.log(value.toString()),
reason => console.log(reason)
);
```
我们来看,第一步,我们先来实例化一个`Promise`对象,接收一个函数作为参数,函数接收两个参数,`resolve`和`reject`函数,然后函数体中包裹一个异步操作,就是我们的文件读取操作。然后还是像之前一样的判断,如果有异常,这次我们就不直接抛出异常了,我们调用`reject`函数,代表着异步任务失败,并且把`err`对象传给`reject`函数。如果没有异常,那就说明异步任务成功了,调用`resolve`函数,并且将`data`传给`resolve`函数。
接下来我们就可以调用`Promise`对象的`then`方法了,`then`方法接收两个函数,第一个函数为`Promise`对象状态为成功时调用,并且接收到异步任务成功时的结果值,然后我们打印出来。第二个函数为`Promise`对象状态为失败时调用,也会接收到异步任务失败时的结果值,并且打印出来。
那么我们来看一下结果。大家如果看得仔细一点的话就会发现,上面那段代码我故意把文件路径写错了。我们来看一下执行结果:
![image-20220307164530849](https://file.lynchow.com/2022-03-07-084533.png)
我们先是正确的时候执行了一次,我们看见控制台中成功打印出了文件中的内容,然后我们把文件路径写错再执行一次,这次打印出了我们的错误对象。
以上就是`fs`读取文件的两种实现形式的练习。
## `Ajax`请求
前面我们做了一个读取文件的练习,那么接下来我们来做一个发送`ajax`请求的练习,我们现在有这么一个需求:
![image-20220307165829088](https://file.lynchow.com/2022-03-07-085831.png)
我们现在这个页面上面有一个按钮,当我们点击按钮之后就会发送一个`ajax`请求,并且把请求结果打印在控制台中。
### `Ajax`方式
那么我们怎么处理?如果直接用`ajax`不用`Promise`的话就是`ajax`的固定流程:
```html
<body>
<div class="container">
<h2 class="page-header">Promise AJAX</h2>
<button id="btn" class="btn btn-primary">Click to send ajax</button>
</div>
<script>
const btn = document.getElementById("btn");
btn.addEventListener(
"click",
() => {
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.apiopen.top/getJoke");
xhr.send();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log(xhr.response);
} else {
console.log(xhr.status);
}
}
};
});
</script>
</body>
```
我们先定位到按钮元素,然后绑定点击事件,在点击事件中,我们首先实例化一个`xhr`对象,然后设置我们发送的请求方式和请求`URL`,然后发送请求,接下来我们要处理响应结果。判断`readyState`是不是为4,如果为4,再判断请求状态码是不是为`2xx`,因为状态码只要是2开头的,都代表请求成功了。所以说我们并不是只判断200,如果为`2xx`那么答应响应体,否则打印状态码。那么我们来看一下实际效果:
![image-20220307170656703](https://file.lynchow.com/2022-03-07-090658.png)
我们看见了控制台中打印出了很多接口返回的数据。这就是我们完全使用`ajax`来发送请求。
### `Promise`方式
那么上面的这一部分如何使用`Promise`的方式来实现呢?
```js
btn.addEventListener(
"click",
() => {
let p = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.apiopen.top/getJoke");
xhr.send();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(xhr.status);
}
}
}
});
p.then(
value => console.log(value),
reason => console.log(reason)
);
});
```
我们来看代码,首先实例化一个`Promise`对象,并传入一个函数,该函数接收两个函数作为参数,这个函数的函数体包裹一个异步操作,那么就是说`ajax`请求的部分都封装在这个函数体中,然后依然还是判断状态码,如果状态码是`2xx`那么执行`resolve`函数,并把响应体传给`resolve`函数,否则说明请求失败,调用`reject`函数并把状态码传给`reject`函数,接下来我们调用`then`方法,在`then`方法中定义两个函数,分别接收成功与失败时的结果值并打印到控制台中。
那么我们这么写的效果是什么样子呢?我们来看一下:
![image-20220307172019312](https://file.lynchow.com/2022-03-07-092021.png)
首先我们故意将`url`的路径写错,然后接口报了404,控制台中也成功打印了404,那么接下来我们把`url`路径修改成正确的路径再来请求看一下:
![image-20220307172158871](https://file.lynchow.com/2022-03-07-092200.png)
这一次成功打印了很多接口返回的信息。
以上便是`Promise`对`ajax`请求的一个封装,大家看是不是就那么一个固定的路数?只要把我们的异步操作放到`Promise`对象里,操作失败就调`reject`函数,成功就调`resolve`函数,然后再调用`Promise`对象的`then`方法,在`then`方法中定义两个函数,成功了执行第一个函数,否则执行第二个函数
Loading…
Cancel
Save