05月29, 2018

聊聊JS异步

如你所知,JS语言是一门单线程语言。所以 异步 是JS中实现异步的关键点,今天我们聊聊异步。

同步?异步?

什么叫做同步?什么叫做异步?我们用一个例子来深入浅出的描述下。

现在,你走进一家理发店,理发店里只有一个理发师,你前面有一个美美的小仙女在剪头发,所以现在的你只能等待。十分钟后,小仙女的头发剪好了,她决定再烫个时髦的卷发,理发师给他做完造型后,需要等待半小时的时间来固定发型,在这半小时的时间内,理发师什么都不能做,只能静静的等着小仙女。半小时后,理发师赶紧给小仙女洗了定型膏。至此小仙女的服务完成了,可以开始给你剪发了,整个这个过程就是一个同步过程,其中小仙女染发的半小时,理发师什么都没有做,只能等待,直到小仙女的剪发流程全部完成,才能释放出来。

相反,如果当小仙女剪完头发,烫头的发型做好后,等待的这半小时,理发师可以释放出来先给你和你后面的客人理发,当半小时时间到了之后,再去给小仙女洗定型膏,这样就能极大程度的提高效率,这就是异步操作。

接下来我们换成js概念理解一下,理发店中的理发师就是单线程语言引擎,也就是说它同一时间就只能干一件事情,换言之他只能给一个人做头发,而剪头发动作就是一条执行语句,如果中间理发师在等待小仙女发型固定的半小时内去给别的客人理头发,那么染发的过程就是一个异步语句。由于染发的这个动作时间会比较长,如果同步执行的话,会阻塞IO,降低执行效率,所以异步操作就是将该条语句放到异步队列里,然后继续向下执行,当异步语句就绪后,再进一步执行这条异步语句的回调方法。

JS异步原理

上面提到了异步执行原理,我们现在来讲JS的异步原理,JS异步实现基于以上我们所说的异步,但是略有不同。

我们还是理发店这个例子讲起,之前我们提到异步就是小仙女头发定型的半小时之内,理发师去给别的可以理发,当半小时时间到了,理发师会中断手头的工作,优先处理小仙女的头发。 但是, 在JS语言中,JS是不会中断目前执行的语句,也就是说JS这位理发师会把理发店里需要理发的客人都剪完了之后(这里我们说理发的动作就是一个同步执行的过程),再回来处理小仙女的头发。

所以不同于操作系统中异步处理方式,比如,时间片轮转调度等优化算法,JS语言处理异步回调的时机发生在同步语句执行完成之后,也因此异步的时间会出现误差。

接下来,我们用js举个具体的例子来看看。

var start = new Date;
setTimeout(function(){
    var end = new Date;
    console.log('Time elapsed:', end - start, 'ms');
}, 500);
console.log('同步语句')
while (new Date - start < 1000) {};

上面这个代码,我们分析一下,Timer是一个异步操作,按照套路,执行的Timer的时候,就会将Timer中传入的匿名函数塞到异步队列里,然后接着向下执行,输出 同步语句 , 然后执行 While 语句,中间到了500ms了,就会优先来知情Timer中匿名函数中的console.log。我们预期输出的结果是:

  1. 输出:同步语句
  2. 500ms后输出:Time elapsed 500ms
  3. 继续将While语句执行完成

然鹅,我们执行的结果却是这样的:

alt

很明显,第二和第三的执行结果并不符合我们预期。现实是,JS将While语句执行完成之后,再来执行Timer的回调函数,所以即使500ms的时间已经到了,也不会马上执行异步方法的回调。

JS vs 宿主环境

我们知道js是单线程语言,JS所运行的环境,无论是操作系统还是浏览器都是多线程的,所以我们来讲讲JS在宿主环境中的运行机制。

依然抛出理发店的例子,假设现在在你进入理发店理发的时候,你前面有5个小仙女都在烫头发,不同的是他们开始烫发的时间不同,所以结束烫发的时间也不同,但都处于定型的状态。这时候理发师依然要给后面的客人理发,但是,理发师雇了一个小助理,小助理每间隔一段时间就会询问烫头发的小仙女时间到没到,如果时间到了,会第一时间通知理发师去优先处理到时间的小仙女。这里要特别说的是,JS理发师这里的处理方式会有一些不同,在小仙女烫头发时间到了之后,JS理发师不会停下来手头的理发工作,小助理知道小仙女烫发时间到了的话,会按照时间的先后顺序将到时间的小仙女的名字罗列到备忘录中,等到JS理发师理完店里所有理发的客人后,再去按备忘录的顺序挨个处理到时间的小仙女们的头发。

上面的例子中,理发师就是JS引擎,它依然是单位时间内只能处理一个动作(如理发),但是理发店是JS的宿主环境,小助理就是另一个线程,用来协助JS理发师工作的。宿主环境中协助线程会维护一个执行队列,循环检测,调度JavaScript线程来执行。

alt

上图是浏览器中运行机制,其中 stack 模块就是JS单线程引擎, event loop 就是一个辅助线程(理发店的小助理)用来轮询异步回调函数的状态,如果状态就绪就将回调函数塞到任务队列中,一旦JS stack 中的任务执行完成,就会执行任务执行队列。

如果同步执行函数有嵌套关系,那么会依次插入执行栈(stack)的顺序回一次堆叠:

function multiply() {
    return a * b
}

function square(n) {
    return multiply(n, n)
}

function printSquare(n) {
    var square = square(n)
}

function main() {
    printSquare(4)
}
main()

比如上面这段小demo,在stack中的插入机制如下:

alt

可以看到首先执行 main 函数,所以会先将main插入到stack中,执行 main 函数的时候会遇到 printSquare, 于是 printSquare 再插入到 stack 中,依次类推,直到 multiply 方法入栈后,返回了具体的值后 multiply 函数出栈,依次再执行 square 函数,再出栈 square 依次类推。

实现异步的几种方式

在JS 中实现异步有几种方式呢?我们来罗列一下:

  • 事件绑定
  • 回调函数
  • promise
  • async/await
  • generator
  • sub/pub

我们依次举例说明:

事件绑定

alt

事件绑定是比较基础和古老的一种实现异步的方式,缺点是不够灵活。

回调函数

alt

回调函数层层嵌套,会有 回调地狱 的问题,增加了代码维护成本。

Promise

Promise是ES6提出的概念,所有的promise对象都有一个then方法,来处理回调。通过then来向下传递数据。

let P = function () {
    return new Promise(function(resolve, reject) {
        fs.readFile('./lala.txt', (err, source) => {
              if (err) {
                console.error(err);
                reject(err)
              } else {
                console.log(source);
                resolve(source)
              }
            });
        }).then((res)=>{
            console.log("res==>",res)
            console.log("BiuBiu")
        })
}
P();

我们定义P函数会返回一个promise对象,promise接受两个参数:resolve、reject。promise有三种状态:Pending,Fullfilled,Rejected。Promise从pending状态转换到Fullfilled时,也就是例子中如果读文件成功了,err为空,就会走到else中,我们就执行resolve来处理数据,如果读取文件失败了(如:没找到文件),那么就会执行reject操作。当readFile返回结果后,会接着这行then里面的方法。

async/await

ES7 中提到了async/await 的概念,进一步优化了异步回调的写法,按照同步的写法写异步。

let P = function () {
    return new Promise(function(resolve, reject) {
        fs.readFile('./lala.txt','utf-8', (err, source) => {
              if (err) {
                console.error(err);
                reject(err)
              } else {
                console.log(source);
                resolve(source)
              }
            });
        })
}



async function test() {
    await P();
    console.log("BiuBiu")
}

上面的例子中,我们声明用关键字 async 来声明test方法,使用 await 关键字来修饰 P 方法的执行。这时,BiuBiu会在P方法执行之后再执行。如果使用Promise的话,我们需要写在then 方法中,如下,也可以实现同样的效果。

function test() {
    P().then(function(){
            console.log("BiuBiu")
        })
}

generator

generator也可以实现异步,我们来通过一个例子来讲一下。

let P = function () {
    return new Promise(function(resolve, reject) {
        fs.readFile('./lala.txt','utf-8', (err, source) => {
              if (err) {
                console.error(err);
                reject(err)
              } else {
                console.log(source);
                resolve(source)
              }
            });
        })
}


function* gen(){
    var f1 = yield P();
    console.log("BiuBiu")
}

let g = gen()
let res = g.next()
res.value.then((data)=>{
    g.next();
})

我们 通过 * 关键字定义一个生成器函数,生成器函数在执行时能暂停,后面又能从暂停处继续执行。

调用一个生成器函数(语句:let g = gen())并不会马上执行它里面的语句,而是返回一个这个生成器的 迭代器 (iterator )对象,也就是本例中的 g 对象。当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止,yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。

next()方法返回一个对象,这个对象包含两个属性:value 和 done,value 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器后续是否还有 yield 语句,即生成器函数是否已经执行完毕并返回。

所以,我们执行 g.next()时候,会执行到 console.log("BiuBiu") 这条语句之前,这是 P 会返回一个 thenable 的 promise对象,接下来我们在then方法中再次执行 g.next(),就完成了 console 的输出。

Pub/Sub

var fs = require ('fs')
/**
 * 观察者模式
 */
//订阅
var PubSub = {};
var eventObj = {};
PubSub.subscribe = function(event, fn) {
    eventObj[event] = fn;
}
//发布
PubSub.publish = function(event) {
    if(eventObj[event]) return eventObj[event]();
}
//退订
PubSub.off = function(event, fn) {
    if(eventObj[event]) eventObj[event] = null;
}
PubSub.subscribe('read1', function(){
    fs.readFile('./lala.txt','utf-8', (err, source) => {
      if (err) {
        console.error(err);
      } else {
        console.log("read1===>",source);
        PubSub.publish('read2');
      }
    });    
})
PubSub.subscribe('read2', function(){
    fs.readFile('./let.js','utf-8', (err, source) => {
      if (err) {
        console.error(err);
      } else {
        console.log("read2===>",source);
      }
    });
})

PubSub.publish('read1')

console.log("同步执行")

我可以在任何时候去 publish 订阅的promise 事件

本文链接:https://www.imwineki.cn/post/jsasync.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。