概念

首先需要强调一下:同步不等于阻塞,异步也不等同于非阻塞

其实同步/异步是一种消息通知机制

  • 同步没有消息通知
  • 异步有消息通知

阻塞和非阻塞的主体是:程序
而同步和异步的主体是:消息

Q:为啥需要这么麻烦的澄清这个关系呢?
A:因为也有同步不阻塞、还有异步阻塞,但是在 JavaScript 中,我们只讨论 同步阻塞 异步非阻塞
比较绕,没关系,先知道有这么个东西就可以。


栗子 1:方块的运动

我们来试着做一些东西吧,效果如下图

在这里插入图片描述

html + css

样式和 dom 元素如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<style>
.box {
width: 100px;
height: 100px;
background: red;
position: absolute;
left: 0px;
top: 0px;
}
</style>
<body>
<div class="box"></div>
</body>

分析

其实就是让方块儿右下左上,顺时针转了一圈

但是!这里是有顺序的,先右移完成后,然后下移,接着左移,最后上移,这应该是同步的对吧!

所以写程序也要是同步的写法。

回调函数 + 回调地狱

比较原始的方法就是用回调函数来写同步的程序了。
我们先写一个移动功能的函数,然后在调用它四遍即可~

回调函数:就是被当作参数传递的函数。百度百科:回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 先实现移动功能函数
function move(ele, arg, target, cb) {
let start = parseInt(window.getComputedStyle(ele, null)[arg]); // 获取起点位置
let direction = (target - start) / Math.abs(target - start); // 正负1,正值代表正方向
let speed = direction * 5;
function fn() {
let now = parseInt(window.getComputedStyle(ele, null)[arg]);
if (now === target) {
cb && cb("结束");
} else {
ele.style[arg] = now + speed + "px"; // 移动
window.requestAnimationFrame(fn);
// 递归
// 60fps,这里也可以使用定时器
}
}
fn();
}

let ele = document.querySelector(".box");
// 回调地狱
move(ele, "left", 200, function (res) {
move(ele, "top", 200, function (res) {
move(ele, "left", 0, function (res) {
move(ele, "top", 0, function (res) {
console.log(res);
});
});
});
});

通过上面的代码可以看到,回调解决同步的问题会产生层层嵌套的代码,这个问题叫做回调地狱。

回调地狱的代码因为是层层嵌套的,如果这个功能很复杂的话,嵌套的层数可能会非常的多,导致后期维护起来十分困难。所以,Promise 应运而生。

额外知识:

  • window.requestAnimationFrame(fn):调用 gpu,节约内存;而且动画比定时器的要流畅。
  • setTimeout也可以,不过会感觉到卡顿

Promise 期约

首先呢,我不先介绍它是什么,而是看他如何解决回调地狱,试着将上面的栗子写成 Promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function move(ele, arg, target) {
// move 函数需要返回一个 Promise 对象
return new Promise((resolve, reject) => {
let start = parseInt(window.getComputedStyle(ele, null)[arg]);
let dis = (target - start) / Math.abs(target - start);
let speed = dis * 5;
function fn() {
let now = parseInt(window.getComputedStyle(ele, null)[arg]);
if (now === target) {
resolve("运动完成");
// cb && cb("运动完成");
} else {
ele.style[arg] = now + speed + "px";
// setTimeout(fn, 100);
window.requestAnimationFrame(fn);
}
}
fn();
});
}

let ele = document.querySelector(".box");
// 以下就是 Promise 的链式操作,解决了回调地狱的问题
move(ele, "left", 200)
.then((res) => {
return move(ele, "top", 200);
})
.then((res) => {
return move(ele, "left", 0);
})
.then((res) => {
return move(ele, "top", 0);
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});

好的,对 promise 有了使用的直观感觉了!我们接下来就看一看相关基础知识吧。

基础

我们先 new 一个 promise 对象,然后打印它

1
2
let promise = new Promise((resolve, reject) => {});
console.log("promise:>>", promise);

可以在控制台看到以下信息:

  • __proto__promise 对象的原型链
  • [[PromiseState]]: "pending":期约的状态
  • [[PromiseResult]]: undefined:成功或失败时,所携带的值(默认为 undefined

而且有一点要知道,new Promise 之后就会立即执行里面的语句

1
2
3
4
new Promise((resolve, reject) => {
console.log(1);
});
// 1

PromiseState

PromiseState 有三种状态:

  • pending:执行中
  • fulfilled :成功
  • rejected :失败

当我们在回调中调用了 resolve() 的时候 ,这个对象就会从 pending 转换成 fulfilled

1
2
3
4
let p = new Promise((resolve, reject) => {
resolve();
});
setTimeout(console.log, 0, p); // Promise {<fulfilled>: undefined}

反之,如果调用了 reject() ,程序就会抛出错误,并且状态变成 rejected

1
2
3
4
let p = new Promise((resolve, reject) => {
reject();
});
setTimeout(console.log, 0, p); // Promise {<rejected>: undefined}

在这里插入图片描述

then 的返回值

p.then() 之后,不管你写没写回调函数,默认都会返回一个 promise 对象,所以它才能链式调用。

then 的成功回调返回有三种情况

第一种:返回空,此时默认会返回一个 fulfilledpromise 对象

1
2
3
4
5
6
7
let p = new Promise((resolve, reject) => {
resolve();
});
let p2 = p.then(() => {
return; // 返回空
});
console.log(p2); // fulfilled

第二种:返回 非 promise 对象,此时,会将数据放到 PromiseResult

1
2
3
4
5
6
7
8
let p = new Promise((resolve, reject) => {
resolve(1);
});
let p2 = p.then((res) => {
// 返回非 promise 对象:11111
return 1111;
});
console.log(p2); // fulfilled

在这里插入图片描述
第三种:返回 promise 对象,此时 p2 的状态就是 return promise() 中的对象状态了

1
2
3
4
5
6
7
8
9
let p = new Promise((resolve, reject) => {
resolve(1);
});
let p2 = p.then((res) => {
return new Promise((resolve, reject) => {
reject("我是p2");
});
});
console.log(p2); // rejected "我是p2"

在这里插入图片描述

我自己实验出来的一个理解

首先我不写 p2onRejected 回调函数。结果是报错的,如下图所示

1
2
3
4
5
6
7
let p = new Promise((resolve, reject) => {
reject("我是p1");
});
let p2 = p.then((res) => {
return res;
});
console.log(p2);

在这里插入图片描述

之后我加上 onrejected 回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
let p = new Promise((resolve, reject) => {
reject("我是p1");
});
let p2 = p.then(
(res) => {
return res;
},
(err) => {
// 加上onrejected
return err;
}
);
console.log(p2);

在这里插入图片描述
我原先以为 p1 的状态决定了 p2 的状态。

总结以下:也就是说啊,p2 的状态跟 p1 并不是那么亲密。

只要我 p2 处理完了,那我的状态就可以变成 fulfilled 了,跟你 p1 成功失败没啥关系了,但是,前提得是 p1 不为pending

错误笔记

给大家看看我原先写的 promise(),现在回看一下,感觉好傻啊,因为原先知道,写 promise() 就是为了避免回调地狱,然而下面的方法跟回调地狱没啥区别(⊙﹏⊙)

1
2
3
4
// 我的错误示范,我原先就这么写的...根本没解决回调
move(ele, "left", 200).then((res) => {
move(ele, "top", 200).then((res) => {});
});

async 和 await

在 es2017 中添加的新的语法糖

首先需要声明一个 async 函数,然后再里面声明 await 的函数

注:await 后面一定要跟一个 promise 对象

可以看一下移动的方块儿的改法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function move(ele, arg, target) {
return new Promise((resolve, reject) => {
let start = parseInt(window.getComputedStyle(ele, null)[arg]);
let dis = (target - start) / Math.abs(target - start);
let speed = dis * 5;
function fn() {
let now = parseInt(window.getComputedStyle(ele, null)[arg]);
if (now === target) {
resolve("运动完成");
} else {
ele.style[arg] = now + speed + "px";
window.requestAnimationFrame(fn);
}
}
fn();
});
}
let ele = document.querySelector(".box");
async function fn() {
try {
await move(ele, "left", 200);
await move(ele, "top", 200);
await move(ele, "left", 0);
let res = await move(ele, "top", 0);
// console.log(res);
} catch (err) {
console.log(err);
}
}
fn();

这是另一个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// es7 语法;
async function myfn() {
try {
// await 后面一定要跟一个 promise对象 ;
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1111);
resolve();
// reject("错误");
}, 1000);
});
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2222);
resolve();
}, 2000);
});
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(3333);
resolve();
}, 3000);
});
} catch (err) {
console.log(err);
}
}
myfn();

async/await 的原理:Generator

如果要介绍 generator 的话,需要先了解一下什么是迭代器iteratioin

迭代器

所谓的迭代器,就是反复的执行某一段代码,通常会有明确的终止条件

我们以 for of举例:for of 能遍历数组,但是不能遍历对象,是因为对象它不是可迭代的(obj is not iterable

obj 的原型上没有obj[Symbol.iterator] 这个函数,而for of 帮我们做的就是执行这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = [1, 2, 3];
for (let item of arr) {
console.log(item); // 能够遍历数组
}

let obj = {
key1: "value1",
key2: "value2",
key3: "value3",
};
for (let item of obj) {
console.log(item); // 报错 Uncaught TypeError: obj is not iterable
}

那么想要用 for of 遍历对象,我们怎么办呢?我们可以给 obj 写一个 迭代器

  • 首先,for of 会调用对象中的 obj[Symbol.iterator] 这个函数

  • 这个函数返回一个对象

  • 对象中又有一个next方法

  • next 方法执行后返回{ done: false, value: values[index++] },其中 done 代表是否迭代结束,value 表示当前迭代的值

哎嘿,闭包的用法吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let obj = {
key1: "value1",
key2: "value2",
key3: "value3",
};
obj[Symbol.iterator] = function () {
// 迭代协议,这里的规则是我们自己定的
let values = Object.values(obj);
let index = 0;
//console.log(values);
return {
next() {
if (index >= values.length) {
return {
done: true,
};
} else {
return {
done: false,
value: values[index++],
};
}
},
};
};
for (let val of obj) {
console.log(val);
}

我们可以试着这么执行一下

1
2
3
4
5
let values = obj[Symbol.iterator]();
console.log(values.next());
console.log(values.next());
console.log(values.next());
console.log(values.next());

在这里插入图片描述
这样一步一步的执行,是不是有点像async/await了?

Generator

Generator 函数的声明方式是 在函数名和 function 中间加个 *

1
function* generatorFn() {}

函数内可以声明 yield 语句

1
2
3
4
5
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}

当我们执行这个函数后,返回一个可迭代的 generator 对象

1
2
3
4
5
6
7
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
let f = generatorFn();
console.log(f);

在这里插入图片描述
之后执行它的 next 方法

1
2
3
4
5
6
7
8
9
10
11
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
let f = generatorFn();
console.log(f);
console.log(f.next());
console.log(f.next());
console.log(f.next());
console.log(f.next());

在这里插入图片描述
看,是不是和上面的迭代器联系起来了!

那我们如何通过它来实现 async/await

实现 async/await

具体实现如下代码,解释的东西也放到代码里了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function* generatorFn() {
yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve("完成异步请求1");
}, 1000);
});
yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve("完成异步请求2");
}, 1000);
});
yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve("完成异步请求3");
}, 1000);
});
}
assist(generatorFn);
// 辅助函数,用来实现同步
// 实现同步的思路:上一个 Promise 执行完了,也就是resolve了
// 然后再执行next()方法

function assist(fn) {
let f = fn(); // 拿到generator对象
next();
function next(data) {
let result = f.next(); // result 是 {done:"false",value:Promise} 这个形式。
console.log(result, `上一步请求的结果:${data}`);
if (!result.done) {
result.value.then((res) => {
console.log(res); // 异步请求返回的值
next(res); // 并把这个值传递到下一步中
});
}
}
}

在这里插入图片描述


Promise 的方法

Promise.resolve()

返还一个 状态为 fulfilledpromise 对象

1
2
3
console.log(Promise.resolve("解决"));
// 同理 reject 返还一个状态为 rejected 的对象
console.log(Promise.reject("未解决"));

Promise.all([])

all 方法第一个参数是数组,数组中放 promise 对象,当所有的 promise 对象都 resolve 了,才会执行 then(),如果有一个是 reject 都拿不到结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1111);
resolve("我是1");
}, 2000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(22222);
resolve("我是2");
// reject(2);
}, 3000);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(33333);
resolve("我是3");
}, 1000);
});

// p1、p2、p3 都是 reslove,才会then;
Promise.all([p1, p2, p3]).then((res) => {
console.log("返回 resolve 中的结果,res 也是数组", res);
});

Promise.race([])

跟 all 方法有些类似,传递一个数组,数组中的 promise 谁最先 resolve,res 就是谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1111);
resolve("我是1");
}, 2000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(22222);
resolve("我是2");
// reject(2);
}, 3000);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(33333);
resolve("我是3");
}, 1000);
});

// 数组中的 promise 谁最先 resolve
Promise.race([p1, p2, p3]).then((res) => {
console.log(res);
});
// 33333
// 我是3
// 1111
// 22222

以上就是我的总结笔记啦,有用的话请点个赞吧,您的支持可就是我的动力啊,靴靴!
在这里插入图片描述