2018-04-27 关于异步

什么是异步

异步在JS中的表现分为三类:

  1. 时间延迟,比如setTimeout,setInterval这种时间处理函数
  2. 事件处理,比如onClick(点击事件),onChange(输入框内容改变回调),onMouseover(鼠标移入事件),onMouseout(鼠标移出事件),onMouseEnter(鼠标移入事件),onMouseLeave(鼠标移出事件)等等
  3. 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))();
}