目录
异步操作前置知识
- JS是单线程的
单线程即一个时间只能处理一个任务。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
- 同步任务与异步任务
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
const a = 2
const b = 3
console.log(a + b) // 同步任务
// 异步任务
setTimeout(() => { // 延迟1s执行
console.log(a + b)
}, 1000)
console.log(1)
setTimeout(() => {
console.log(2)
}, 1000)
console.log(3)
// 1 3 2
console.log(1)
//不管延迟时间是多少,它都是异步任务,必须等主线程任务执行完成之后再执行
setTimeout(() => {
console.log(2)
}, 0)
console.log(3)
//1 3 2
- Ajax原理
Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据。
function ajax(url, callback) {
// 1、创建XMLHttpRequest对象
var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
// 兼容早期浏览器
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
// 2、发送请求
xmlhttp.open("GET", url, true); // 指定发送请求的操作
xmlhttp.send(); // 发送
// 3、服务端响应
// 监听onreadystatechange 方法
xmlhttp.onreadystatechange = function () {
// 事件处理函数
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
var obj = JSON.parse(xmlhttp.responseText);
// console.log(obj)
callback(obj); // 将响应得到的数据传给回调,回调函数是自己定义并传入的
}
};
}
//获取随机猫咪照片
var url = "https://api.thecatapi.com/v1/images/search?limit=1";
ajax(url, (res) => {
console.log(res);
});
- Callback Hell
回调函数可规范调用的顺序,但是当代码层层嵌套越写越深,代码的可维护性、可读性都会降低,就会造成Callback Hell
// 1 -> 2 -> 3
// callback hell
ajax('static/a.json', res => {
console.log(res)
ajax('static/b.json', res => {
console.log(res)
ajax('static/c.json', res => {
console.log(res)
})
})
})
Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log("zzz");
resolve();
}, 1000);
}).then(
(res) => {
console.log("成功");
},
(err) => {
console.log("失败");
}
);
如果Promise内部没有写任何异步操作,那么它是会立即执行的。then方法相当于promise的回调函数(它的微任务),待promise内的函数执行完成便执行。
let p = new Promise((resolve, reject) => {
console.log(1);
resolve();
});
p.then((res) => {
console.log(3);
});
console.log(2);
//依次输出:1 2 3
Promise状态一旦确定下来就无法再改变
let p = new Promise((resolve, reject) => {
resolve(1);
reject(2);
});
p.then(
(res) => {
console.log(res); // 1
},
(err) => {
console.log(err);
}
);
改造回调深渊Callback Hell
// 传入成功回调与失败回调
function ajax(url, successCallback, failCallback) {
// 1、创建XMLHttpRequest对象
var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
// 兼容早期浏览器
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
// 2、发送请求
xmlhttp.open("GET", url, true);
xmlhttp.send();
// 3、服务端响应
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
var obj = JSON.parse(xmlhttp.responseText);
// console.log(obj)
successCallback && successCallback(obj);
} else if (xmlhttp.readyState === 4 && xmlhttp.status === 404) {
failCallback && failCallback(xmlhttp.statusText);
}
};
}
function getPromise(url) {
return new Promise((resolve, reject) => {
ajax(
url,
(res) => {
resolve(res);
},
(err) => {
reject(err);
}
);
});
}
getPromise("static/a.json")
.then((res) => {
console.log(res);
return getPromise("static/b.json");
})
.then((res) => {
console.log(res);
return getPromise("static/c.json");
})
.then((res) => {
console.log(res);
});
静态方法:
- Promise.resolve() 表示成功的状态
let p1 = Promise.resolve("success");
p1.then((res) => {
console.log(res); // success
});
- Promise.reject() 表示失败的状态
let p2 = Promise.reject("fail");
p2.catch((err) => {
console.log(err); // fail
});
- Promise.all() 传入一个数组作为参数,数组内的每一个内容都对应一个Promise对象
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1);
resolve("1成功");
}, 1000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2);
//resolve("2成功");
reject('2失败')
}, 2000);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(3);
resolve("3成功");
}, 3000);
});
//p1,p2,p3都resolve了才执行后面的then,只要有一个失败了就进入失败状态
Promise.all([p1, p2, p3]).then((res) => {
console.log(res);
},err=>{
console.log(err)
});
Promise.all
(下文都称p)的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
- Promise.race() 将多个 Promise 实例,包装成一个新的 Promise 实例
//只要有一个率先完成,那整个状态就是完成的,只要有一个率先失败,那整个状态就是失败的
Promise.race([p1, p2, p3]).then(
(res) => {
console.log(res);
},
(err) => {
console.log(err);
}
);
只要p1
、p2
、p3
之中有一个实例率先改变状态,Promise.race
的状态就跟着改变。
Promise的应用场景举例:
①上传图片
const imgArr = ["1.jpg", "2.jpg", "3.jpg"];
let promiseArr = [];
imgArr.forEach((item) => {
promiseArr.push(
new Promise((resolve, reject) => {
//图片上传的操作
resolve();
})
);
});
Promise.all(promiseArr).then((res) => {
console.log("图片全部上传成功");
});
②加载图片
function getImg() {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = function () {
resolve(img.src);
};
// img.src = "http://www/xxx.com/xx.jpg";
img.src = "https://cdn2.thecatapi.com/images/6fk.jpg";
});
}
function timeout() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("图片请求超时");
}, 2000);
});
}
Promise.race([getImg(), timeout()])
.then((res) => {
console.log("加载图片成功", res);
})
.catch((err) => {
console.log(err);
});
Generator
原理
- 从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。 - 形式上,Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式, 定义不同的内部状态( yield 在英语里的意思就是“产出”)。
- Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
- 必须调用遍历器对象的 next 方法(next方法可以传递参数),使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的, yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。
用法
Generator是可以暂停的,需要调用next方法手动执行, yield指令只能在生成器内部使用。
由于 Generator 函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的next
方法的运行逻辑如下。
(1)遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式。
(3)如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。
(4)如果该函数没有return
语句,则返回的对象的value
属性值为undefined
。
//普通函数
function foo() {
for (let i = 0; i < 3; i++) {
console.log(i);
}
}
foo(); // 直接输出 0,1,2
// Generator
function* _foo() {
for (let i = 0; i < 3; i++) {
yield i;
}
}
let f = _foo();
// 通过next手动执行
console.log(f.next()); // { value: 0, done: false }
console.log(f.next()); // { value: 1, done: false }
console.log(f.next()); // { value: 2, done: false }
console.log(f.next()); // { value: undefined, done: true }
// yield指令只能在生成器内部使用
// function* gen(args) {
// args.forEach(item => {
// yield item + 1
// })
// }
需要注意的是,yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* gen(x) {
let y = 2 * (yield x + 1);
let z = yield y / 3;
return x + y + z;
}
let g = gen(5);
console.log(g.next().value); // 6
// 上一次yield表达式没有返回y值,所以变成y = 2* undefined => NaN
console.log(g.next().value); // NaN
console.log(g.next().value); // NaN y / 3 => NaN
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
//①
let g = gen(5);
// x+1 = 6
console.log(g.next().value);// 输出6
// 传递参数6,前面的 (yield x + 1) = 6,所以y = 2* 6 = 12, y /3 = 4
console.log(g.next(6).value); //输出4
// 传递参数4,前面的 ( yield y / 3 ) = 4,所以z = 4,return x+y+z = 5+12+4 = 21
console.log(g.next(4).value); //输出21
//②
let g = gen(5);
// x+1 = 6
console.log(g.next().value); //输出6
//传递参数8,前面的 (yield x + 1) = 8,所以y = 2* 8 = 16, y /3 = 5.3333
console.log(g.next(8).value); //输出5.3333
// 传递参数12,前面的(yield y /3 ) = 12,所以z = 12, return x+y+z = 5+16+12 = 33
console.log(g.next(12).value); //输出33
可以写一个计数器,每次遇到7的倍数就停止执行
//计数器 遇到7的倍数就停止
function* count(x = 1) {
while (true) {
if (x % 7 === 0) {
yield x;
}
x++;
}
}
let n = count();
console.log(n.next().value); // 7
console.log(n.next().value); // 14
console.log(n.next().value); // 21
console.log(n.next().value); // 28
console.log(n.next().value); // 35
异步状态管理
以ajax为例,请求顺序a→b→c
function ajax(url, callback) {
// 1、创建XMLHttpRequest对象
var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp = new XMLHttpRequest();
} else {
// 兼容早期浏览器
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
// 2、发送请求
xmlhttp.open("GET", url, true);
xmlhttp.send();
// 3、服务端响应
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
var obj = JSON.parse(xmlhttp.responseText);
// console.log(obj)
callback(obj);
}
};
}
// 封装请求方法,调用ajax
function request(url) {
ajax(url, (res) => {
getData.next(res); // 每一次request请求都会调用next,使Generator对象继续执行
});
}
function* gen() {
let res1 = yield request("static/a.json");
console.log(res1);
let res2 = yield request("static/b.json");
console.log(res2);
let res3 = yield request("static/c.json");
console.log(res3);
}
let getData = gen();
getData.next();
Iterator
Iterator是一种接口,为各种不同的数据结构提供统一的访问机制,使得不支持遍历的数据结构“可遍历”。Iterator 接口主要供for...of
消费。
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next
方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next
方法,直到它指向数据结构的结束位置。
每一次调用next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
function makeIterator(arr) {
let nextIndex = 0;
return {
next() {
return nextIndex < arr.length
? {
value: arr[nextIndex++],
done: false,
}
: {
value: undefined,
done: true,
};
},
};
}
let it = makeIterator(["a", "b", "c"]);
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
原生具备 Iterator 接口的数据结构
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
下面的例子是数组和Map的Symbol.iterator
属性。
let arr = ['a', 'b', 'c'] // 对于数组这样的可迭代结构,它里面自带了Symbol.iterator
console.log(arr) // ["a", "b", "c"]
let it = arr[Symbol.iterator]()
console.log(it.next()) // {value: "a", done: false}
console.log(it.next())
console.log(it.next())
console.log(it.next())// {value: undefined, done: true} ,迭代结束
let map = new Map()
map.set('name', 'es')
map.set('age', 5)
let it = map[Symbol.iterator]()
console.log(it.next())
console.log(it.next())
console.log(it.next())
将不可迭代对象改造成符合上述两种协议的结构,便能够实现遍历:
let courses = {
allCourse: {
fronted: ["ES", "Vue", "小程序"],
backend: ["JAVA", "Python"],
webapp: ["Android", "IOS"],
},
};
for (let c of courses) {
console.log(c); // 报错,不可迭代
}
// 可迭代协议:Symbol.iterator
// 迭代器协议:return { next(){ return { value,done } } }
courses[Symbol.iterator] = function () {
let allCourse = this.allCourse;
let keys = Reflect.ownKeys(allCourse);
let values = [];
return {
next() {
if (!values.length) {
if (keys.length) {
values = allCourse[keys[0]];
keys.shift();
}
}
return {
done: !values.length,
value: values.shift(),
};
},
};
};
for (let c of courses) {
console.log(c); // 可迭代
}
Generator遍历不可迭代对象
Generator自带next方法,所以它也能够帮助不可迭代对象遍历。
courses[Symbol.iterator] = function* () {
let allCourse = this.allCourse;
let keys = Reflect.ownKeys(allCourse);
let values = [];
while (1) {
if (!values.length) {
if (keys.length) {
values = allCourse[keys[0]];
keys.shift();
yield values.shift();
} else {
return false;
}
} else {
yield values.shift();
}
}
};
for (let c of courses) {
console.log(c); // 报错,不可迭代
}
模块化规范
CommonJS:Node.js
AMD:require.js
CMD:sea.js
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
import { a } from "./module.js";
console.log(a);
模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量。
如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import { a as aa } from "./module.js";
console.log(aa);
使用import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
// module.js
const a = 5;
export default a;
//index.js
import aa from "./module.js";
console.log(aa);
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default
命令。