什么是异步
异步在JS中的表现分为三类:
- 时间延迟,比如setTimeout,setInterval这种时间处理函数
- 事件处理,比如onClick(点击事件),onChange(输入框内容改变回调),onMouseover(鼠标移入事件),onMouseout(鼠标移出事件),onMouseEnter(鼠标移入事件),onMouseLeave(鼠标移出事件)等等
- xhr、fetch请求等等
为什么会出现异步
由于JS是单线程(同一时间,只能做一件事情)的语言,为了实现时间延迟、事件处理和xhr、fetch请求处理这些必须是异步的操作,JS必须处理单线程怎样异步的问题。
为什么JS要使用单线程
JS使用单线程,是由于JS中不仅仅有ECMAScript,还有DOM(Document Object Modal),html渲染DOM,JS也可以动态改变DOM,假如JS不是单线程,同一时间可以处理多件事情,html渲染DOM和JS动态改变DOM就会混乱,浏览器会不知道是先渲染html DOM,还是先执行JS动态改变DOM,即使是实现了多线程的WebWorker,也没有触及DOM部分。
关于异步
JS使用单线程,运行时,先会运行同步代码,如果遇到异步代码,就会先将异步代码放进内存的异步队列中,待到同步代码运行完毕,就会去轮询异步队列中的异步代码。
假如异步闭包中还存在异步闭包,在异步队列中,就会将里层的异步闭包push到外层的异步闭包后面,待到外层的异步闭包执行完毕之后,再执行里层的异步闭包。
使用异步出现的问题
JS使用异步解决了JS单线程的问题,但是异步却又引起了另外一个问题,就是异步情况下的同步执行,尤其是xhr、fetch的请求,由于响应时间不确定,很容易引起异步执行混乱的问题。
异步的演变过程
setTimeout callback
之前还没有使用nodejs的时候,解决异步问题基本上使用的是setTimeout使用异步闭包去解决异步的方式,比如在react中,setState就是异步执行的,假如name的初始值为Gary
componentWillMount() {
this.setState({
name: "Yinwk"
});
this.setState({
name: "CLAY"
});
//这时候由于setState是异步的,所以会先执行同步代码,也就是说这时候this.state.name的值依然为Gary
console.log(this.state.name);
}
componentWillMount() {
setTimeout(function timer(){
this.setState({
name: "Yinwk"
});
//这时候this.state.name的值就变为了Yinwk
console.log(this.state.name);
setTimeout(function timer(){
this.setState({
name: "CLAY"
});
//这时候this.state.name的值就变为了CLAY
console.log(this.state.name);
}.bind(this), 0);
}.bind(this), 0)
}
EventEmitter(并不是异步问题的解决方案)
nodejs出现之后,event模块有一个类——EventEmitter,一直以为它是一个消息管理模块,跟Event Loop异步队列有着千丝万缕的关系,后来看了源码,发现它并不是一个消息管理模块,跟消息队列没有任何关系,on/emit只是一个监听的观察者模式,on时添加/删除listener,emit时运行回调函数。还是拿上一个例子进行举例,假如name的初始值为Gary
import {EventEmitter} from "events";
const eventEmitter = new EventEmitter();
componentWillMount(){
eventEmitter.on("gary", () => {
this.setState({
name: "Yinwk"
});
console.log(this.state.name);
});
//这里打印出的this.state.name还是Gary,由于EventEmitter跟消息队列没有任何关系,所以并不是异步,只是一个监听的观察者模式,所以是同步的
eventEmitter.emit("gary");
}
Promise
ES6语法发布之后,Promise成为了新的解决异步问题的方案,实质的也是利用异步闭包来解决异步问题。还是拿最初的例子进行举例,假如name的初始值为Gary
ComponentWillMount() {
(function iifeSetName() {
return new Promise(function promise(resolve, reject) {
this.setState({
name: "Yinwk"
});
resolve();
}.bind(this));
}.bind(this))().then(function resolve() {
//这里打印出的this.state.name就是Yinwk
console.log(this.state.name);
}.bind(this), function reject() {
}.bind(this));
}
yield and *
再到后来最新的解决异步的方案:yield *配合Promise,也就是Generator控制迭代器,执行时每执行到yield时,就会停止执行并返回一个Iterator迭代器,且会保留上下文,直到下次运行,社区tj大神的co函数同步读取文件就是使用了yield *配合Promise
const fs = require("fs");
function readFile(file){
return new Promise((resolve, reject) => {
fs.readFile((err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
function *gc() {
let a = yield readFile("../data/file_first.txt");
console.log(a);
let b = yield readFile("../data/file_second.txt");
console.log(b);
return b;
}
co(gc).then(function resolve(data){
console.log(data);
}.bind(this), function reject(){
}.bind(this));
//co函数利用yield * + Promise同步读取文件
function co(gc_co){
let gc = gc_co();
return new Promise((resolve, reject) => {
//这里实现一个Generator控制迭代器的迭代循环
(function readData(data){
let gcNext = gc.next(data),
value = gcNext.value,
done = gcNext.done;
if(done) {
resolve(value);
} else {
value.then(readData, reject);
}
})();
});
}
async and await
现如今的推出的解决异步问题的最终解决方案:async和await,很好的解决了异步的问题,且当await Promise时,返回的是fulfilled和rejected返回的结果。还是拿最初的例子进行举例,假如name的初始值为Gary
ComponentWillMount() {
(async function iifeSetName() {
await this.setState({
name: "Yinwk"
});
//这里打印出的this.state.name就是Yinwk
await console.log(this.state.name);
}.bind(this))();
}
还可以这样
ComponentWillMount() {
(async function iifeSetName() {
let name = await new Promise((resolve, reject) => {
this.setState({
name: "Yinwk"
});
resolve();
}).then(()=>{
return this.state.name;
}, ()=>{
});
//这里打印出的this.state.name就是Yinwk
await console.log(name);
//这里打印出的this.state.name还是Yinwk
await console.log(this.state.name);
}.bind(this))();
}