JS高级--异步编程
console.log("Hello");
console.log("World");
for(let i=0;i<5;i++){
console.log(i);
}
console.log("F71");
通过上面的代码,我们可以看出同步代码的特点是由上至下依次执行。
异步代码,往往是非常消耗时间的代码,发送网络请求获取数据、读取文件。
console.log("Hello");
console.log("World");
// 发送网络请求,获取数据,然后打印出来
console.log("F71");
上面的代码,如果是以同步的方式来执行,那么“发送网络请求”这个操作就会阻塞后面的代码。
由于 js 是单线程语言。所以当 js 遇到异步的问题的时候,处理的方式是优先处理同步代码,异步的代码扔给异步处理模块。异步处理模块处理完成之后,扔到任务队列里面,然后主线程再一个一个的从任务队列里面去获取异步操作的结果。
下面我们来写一段代码验证上面的说法:
-
setTimeout(fn,time) 异步代码
-
setInterval(fn,time) 异步代码
console.log("Hello");
console.log("World");
setTimeout(function(){
console.log("异步代码一")
},3000);
setTimeout(function(){
console.log("异步代码二")
},1000);
console.log("F71"); //Hello World F71 异步代码二 异步代码一
Ajax
什么是 ajax ?
ajax 是在 2004 年左右提出的一项技术,但是这并不是一门全新的技术,而是早就存在的好几项技术的整合。用到了 DOM 编程、CSS、XHR 的对象。ajax 翻译成中文叫做“异步无刷新”技术,主要就是用来和服务器端进行通信。ajax 技术出来之后,大大提升了用户体验。因为在 ajax 中,是由 XHR 对象来发送请求,用户还可以继续操作现有的网页。ajax 技术现在基本上是无处不在。例如谷歌地图、搜索词汇联想、注册的时候验证用户名是否被占用,这些都用到了 ajax 技术。
ajax 技术一个实现步骤
-
创建 XMLHttpRequest 对象(创建一个跑腿的人)
-
由这个对象来和服务器进行通信(发送 HTTP 请求)
-
欢迎这个人回来,它把数据带回来了
-
通过 DOM 技术进行局部的刷新,将带回来的数据渲染在页面上面
mockjs
使用 mockjs 可以生成随机的数据,拦截 ajax 请求。
下面是一个 mockjs 的使用示例:
<body>
<button id="btn">生成随机数据</button>
<script src="https://cdn.bootcdn.net/ajax/libs/Mock.js/1.0.1-beta3/mock-min.js"></script>
<script>
// 如果是 id,可以不用获取 dom,直接通过该 id 就可以拿到
btn.onclick = function () {
const data = Mock.mock({
// 随机生成一个包含 4 个对象的数组
'list|4': [{
// 对象的 id 属性名依次加 1
'id|+1': 1,
// 随机产生一个中文的姓名
"name": '@cname()',
// 随机生成一个地址
addr: '@county(true)',
// 随机生成一个数字 大小在 18 到 30
'age|18-30': 1,
// 随机生成一个日期
birth: '@date()',
// 随机生成一个整数,0/1 ,根据这个来给“男”、“女”
gender: '@integer(0, 1)',
// 随机生成一个邮箱
email: '@EMAIL()',
// 在数组中随机找一个
'like|1': ['看书', '运动', '听音乐'],
// 1-100 中随机生成一个保留两位小数点
'score|1-100.2': 1,
// 随机生成一个日期
time: "@date('yyyy-MM-dd')",
// 用正则匹配 1 开头的 11 位数字的手机号
mobile: /^1[3-9]\d{9}$/
}]
})
console.log(data);
}
</script>
</body>
具体示例看下面的代码:
<body>
<button id="btn">发送 ajax 请求</button>
<script src="https://cdn.bootcdn.net/ajax/libs/Mock.js/1.0.1-beta3/mock-min.js"></script>
<script>
// 使用 Mock 来拦截上面的 ajax 请求
Mock.mock(/getStu/, function () {
return Mock.mock({
// 随机生成一个包含 4 个对象的数组
'list|4': [{
// 对象的 id 属性名依次加 1
'id|+1': 1,
// 随机产生一个中文的姓名
"name": '@cname()',
// 随机生成一个地址
addr: '@county(true)',
// 随机生成一个数字 大小在 18 到 30
'age|18-30': 1,
// 随机生成一个日期
birth: '@date()',
// 随机生成一个整数,0/1 ,根据这个来给“男”、“女”
gender: '@integer(0, 1)',
// 随机生成一个邮箱
email: '@EMAIL()',
// 在数组中随机找一个
'like|1': ['看书', '运动', '听音乐'],
// 1-100 中随机生成一个保留两位小数点
'score|1-100.2': 1,
// 随机生成一个日期
time: "@date('yyyy-MM-dd')",
// 用正则匹配 1 开头的 11 位数字的手机号
mobile: /^1[3-9]\d{9}$/
}]
})
})
btn.onclick = function () {
// 1. 创建 XMLHttpRequest 对象(创建一个跑腿的人)
let xhr = null;
// 解决兼容性问题
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
console.log(xhr);
// 2. 由这个对象来和服务器进行通信(发送 HTTP 请求)
// GET 和 POST 的区别
// 如果是 GET 请求,往服务器带过去的数据是拼接在 URL 后面的
// 如果是 POST 请求,往服务器带过去的数据放在请求体
xhr.open('GET', '/getStu/', true); // 打开一个链接
xhr.send(null); // 正式的发送请求
// xhr.open('POST','/login',true);
// // 设置 HTTP 的请求头
// xhr.setRequestHeader("Content-type","application/x-www-form-urlencodeed;charset=UTF-8");
// xhr.send("name='xiejie'&age=18");
// 3. 欢迎这个人回来,它把数据带回来了
// xhr 去服务器取数据,去的过程是有一个状态记录的
// 请求未初始化(0):即还没有调用 send() 方法;
// 服务器连接已建立(1):即已调用 send() 方法,正在发送请求;
// 请求已接收(2):即 send() 方法执行完成;
// 请求处理中(3):即正在解析响应内容;
// 请求已完成(4):且响应已就绪,即响应内容解析完成,可以在客户端进行调用了;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// 4. 通过 DOM 技术进行局部的刷新,将带回来的数据渲染在页面上面
console.log(xhr.responseText);
}
}
}
</script>
</body>
异步实现的形式有哪些:
- 回调
- promise
- 生成器
- async await
在最早期的时候,要想拿到异步的结果,只能使用回调。
需求:每隔 2 秒打印一个数,从 1 开始到 3
setTimeout(function(){
console.log(1);
},2000);
setTimeout(function(){
console.log(2);
},2000);
setTimeout(function(){
console.log(3);
},2000);
根据上面的需求,你很有可能就写出上面的代码,但是你会发现上面的代码并不能实现我们的需求。究其原因,是因为三段代码都是异步代码,基本上是同时被扔到异步处理模块,然后基本上是同时一起在计时。基本上也是同一时间被扔到任务队列里面。这里涉及到一个核心,就是我们要等待上一次的异步操作结束后,再开始下一次的操作。
setTimeout(function(){
console.log(1);
setTimeout(function(){
console.log(2);
setTimeout(function(){
console.log(3);
},2000);
},2000);
},2000);
所以在早期的 js 当中,要想某一个操作在异步之后,只有通过回调的形式来实现,也就是说,将后一步操作,写在异步操作的回调函数中。
回调地狱问题(callback hell)
每隔两秒打印一个数,从 1~10
setTimeout(function(){
console.log(1);
setTimeout(function(){
console.log(2);
setTimeout(function(){
console.log(3);
setTimeout(function(){
console.log(4);
setTimeout(function(){
console.log(5);
setTimeout(function(){
console.log(6);
setTimeout(function(){
console.log(7);
setTimeout(function(){
console.log(8);
setTimeout(function(){
console.log(9);
setTimeout(function(){
console.log(10);
},2000);
},2000);
},2000);
},2000);
},2000);
},2000);
},2000);
},2000);
},2000);
},2000);
通过上面的代码,我们已经看出问题来了,通过回调来解决异步的问题,有一个最大的问题,就是代码一直在往右边走,导致可读性很差,我们称它为回调地狱问题。
Promise
Promise 是异步的一种解决方案,最早是由社区提出来一种异步解决方案,主要就是解决回调地狱的问题。从 ES6 开始,Promise 被写入了官方标准。所以我们现在不需要任何的插件,就可以直接使用 Promise。
const pm = new Promise(function(resolve,reject){
// resolve 和 reject 也是两个函数
// resolve 是在异步操作成功的时候调用
// reject 是在异步操作失败的时候调用
resolve('这是异步操作传递的数据');
})
pm.then((data)=>{
console.log(data);
})
当异步操作完成的时候,我们直接 resolve,此时就会来到 then 方法,这个 then里面的函数就相当于以前嵌套的那个函数。
接下来我们来研究 promise 的一些语法细节。
console.log('1');
const pm = new Promise(function(resolve,reject){
console.log("Hello"); // 同步
resolve('这是异步操作传递的数据'); // 异步代码
console.log("World"); // 同步
})
pm.then((data)=>{
console.log(data);
})
console.log('2');
// 1
// Hello
// World
// 2
// 这是异步操作传递的数据
通过上面的代码,我们可以看出,promise 本身里面的代码是同步的,只不过在调用 resolve 的时候是异步代码。
const pm = new Promise(function(resolve,reject){
resolve();
})
pm.then(()=>{
console.log("成功的时候打印这句话")
},()=>{
console.log('失败的时候打印这句话');
})
const pm = new Promise(function(resolve,reject){
// resolve();
reject();
})
pm.then(()=>{
console.log("成功的时候打印这句话")
}).catch(()=>{
console.log("失败的时候打印这句话")
})
每隔两秒打印一个数:
new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(1);
resolve(2);
},2000)
}).then((data)=>{
return new Promise((resolve,reject)=>{
setTimeout(function(){
console.log(data);
resolve(3);
},2000);
})
}).then((data)=>{
setTimeout(function(){
console.log(data);
},2000);
})
promise 相关的 api
Promise.all :用于多个 Promise 的实例,包装成一个新的 Promise 实例,异步的执行顺序会按照 all 方法里面指定的顺序来执行,,如果有一个为 reject,那么最终的结果就是 reject。
Promise.race : 和上面一样,同样是将多个 promise 封装成一个 promise,看哪个异步最先完成,得到的是最先完成的异步结果。
Promise.resolve : 可以将现有的对象转换为 Promise 对象
Promise.resolve('Hello');
// 等价于
new Promise(resolve=>resolve('Hello'));
宏任务和微任务
在主线程中遇到异步任务,会被扔到异步处理模块,异步处理模块处理完之后,会被放入到任务队列。但是在任务队列中,我们的任务其实分成了宏任务和微任务。
执行的顺序是:首先将所有的微任务执行一遍,然后再取出一个宏任务执行。又去检查微任务队列,看微任务队列有没有新的微任务,如果有,将微任务队列里面的所有微任务又全部执行完,之后又去执行一个宏任务....
宏任务:
-
I/O
-
setTimeout
-
setInterval
-
setImmediate
-
requestAnimationFrame
微任务:
-
process.nextTick
-
Promise.then catch finally
console.log('1');
setTimeout(function(){
console.log('2');
process.nextTick(function(){
console.log('3');
})
new Promise(function(resolve){
console.log('4');
resolve();
}).then(function(){
console.log('5')
})
})
process.nextTick(function(){
console.log('6');
})
new Promise(function(resolve){
console.log('7');
resolve();
}).then(function(){
console.log('8');
}) //1、7、6、8、2、4、3、5
生成器
各种遍历
首先我们来看一下数组的遍历,代码如下:
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 接下来,我们来遍历这个数据
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
arr.forEach((item) => {
console.log(item);
})
for (let i in arr) {
console.log(arr[i]);
}
for (let i of arr) {
console.log(i);
}
// 遍历集合
const list = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
for(let ele of list){
console.log(ele);
}
let obj = {
name : 'xiejie',
age : 18,
gender : 'male',
intro : function(){
console.log('Hello~');
}
}
for(let attr in obj){
console.log(attr);
}
在上面的代码中,我们对数组、集合、对象进行了遍历,所谓遍历,就是取出它的每一项。
const arr = [1, 2, 3];
const it = arr[Symbol.iterator](); // 获取数据的迭代器存储到 it 里面
// 迭代器是一个对象,每一次调用 next 方法,就可以获取下一个元素
// 所以当我们使用 for-of 来遍历数组的时候,实际上它的内部是在调用迭代器的 next 方法
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }
生成器就是专门用来生成迭代器的
function* say(){
yield "开始";
yield "执行";
yield "结束";
}
const it = say(); // 生成一个迭代器
console.log(it.next()); // { value: '开始', done: false }
console.log(it.next()); // { value: '执行', done: false }
console.log(it.next()); // { value: '结束', done: false }
console.log(it.next()); // { value: undefined, done: true }
每隔两秒打印一个数:
function test(n){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(n);
resolve()
},2000);
})
}
// 接下来书写一个生成器,生成器里面就封装了一系列的异步操作
function* say(){
yield test(1);
yield test(2);
yield test(3);
yield test(4);
yield test(5);
}
// 接下来需要书写一个执行器
const it = say(); // next 方法 返回 {value : Promise, done : flase}
it.next().value.then(()=>{
return it.next().value;
}).then(()=>{
return it.next().value;
}).then(()=>{
return it.next().value;
}).then(()=>{
return it.next().value;
})
因为书写执行器非常的麻烦,所以出现了一个叫做 co 的模块,可以自动的执行生成器。
// 接下来我们使用生成器来改写读取文件的操作
const fs = require('fs');
const co = require('co');
let arr = [];
function test(fileName){
return new Promise((resolve,reject)=>{
fs.readFile(fileName,(err,data)=>{
arr.push(data.toString());
resolve();
});
})
}
function* say(){
yield test('./file1.txt');
yield test('./file2.txt');
yield test('./file3.txt');
console.log(arr);
}
co(say);
异步终结解决方案 async、await
function timer(){
return new Promise((resolve)=>{
setTimeout(()=>{
console.log("Hello");
resolve();
},2000);
})
}
// 一旦我的函数前面添加了 async 关键字
// 那么就说明这是一个异步函数,说明我可以使用 await 关键字来等待上一次的异步结果
async function getTimer(){
await timer();
console.log("World");
}
getTimer();
每隔 2 秒打印一个数:
function timer(num){
return new Promise((resolve)=>{
setTimeout(()=>{
console.log(num);
resolve();
},2000);
})
}
// 一旦我的函数前面添加了 async 关键字
// 那么就说明这是一个异步函数,说明我可以使用 await 关键字来等待上一次的异步结果
async function getTimer(){
await timer(1);
await timer(2);
await timer(3);
}
getTimer();

浙公网安备 33010602011771号