• 异步编程上篇


    16章异步编程

    随着计算机的不断发展,用户对计算机应用的要求越来越高,需要提供更多、更智能、响应速度 更快的功能。这就离不开异步编程的话题。同时,随着互联网时代的崛起,网络应用要求能够支 持更多的并发量,这显然也要用到大量的异步编程。那么从这节课开始,我们会学习到底什么是 异步编程,以及在JS中如何实现异步编程。

    本章我们将学习如下内容:

    ・什么是异步编程。

    •回调和Promise。

    生成器 Generator。

    • ES7中的异步实现Async和Await。

    16-1异步编程概述

    16-1-1什么是异步编程?

    我们先来看看到底什么是异步。提到异步就不得不提另外一个概念:同步。那什么又叫同步呢。 很多初学者在刚接触这个概念时会想当然的认为同步就是同时进行。显然,这样的理解是错误 的,咱不能按字面意思去理解它。同步,英文全称叫做Synchronization。它是指同一时间只能做 —件事,也就是说一件事情做完了才能做另外一件事。

    比如咱们去火车站买票,假设窗口只有1个,那么同一时间只能处理1个人的购票业务,其余的需 要进行排队。这种one by one的动作就是同步。这种同步的情况其实有很多,任何需要排队的情 况都可以理解成同步。那如果在程序中呢,我们都知道代码的执行是一行接着一行的,比如下面 这段代码:

    let ary = [];

    for(let i = 0;i < 100;i++){

    ary[i] = i;

    }

    console.log(ary);

    这段代码的执行就是从上往下依次执行,循环没执行完,输出的代码就不会执行,这就是典型的 同步。在程序中,绝大多数代码都是同步的。

    同步操作的优点在于做任何事情都是依次执行,井然有序,不会存在大家同时抢一个资源的问
    题。你想想,如果火车站取消排队机制,那么大家势必会争先恐后去抢着买票,造成的结果就是 秩序大乱,甚至可能引发一系列安全问题。如果代码不是同步执行的又会发生什么呢?有些代码 需要依赖前面代码执行后的结果,但现在大家都是同时执行,那结果就不一定能获取到。而且这 些代码可能在对同一数据就进行操作,也会让这个数据的值出现不确定的情况。

    当然同步也有它的缺点。由于是依次进行,假如其中某一个步骤花的时间比较长,那么后续动作 就会等待它的完成,从而影响效率。

    不过,在有些时候我们还是希望能够在效率上有所提升,也就是说可以让很多操作同时进行。这 就是另外一个概念:异步。假设火车站有10个人需要买票,现在只有1个窗口提供服务,如果平 均每个人耗费5分钟,那么总共需要50分钟才能办完所有人的业务。火车站为了提高效率,加开 9个窗口,现在一共有10个窗口提供服务,那么这10个人就可以同时办理了,总共只需要5 钟,他们所有人的业务都可以办完。这就是异步带来的优势。

    16-1-2异步的实现

    1. 多线程

    像刚才例子中开多个窗口的方式称为多线程。线程可以理解成一个应用程序中的执行任务,每个 应用程序至少会有一个线程,它被称为主线程。如果你想实现异步处理,就可以通过开启多个线 程,这些线程可以同时执行。这是异步实现的一种方式。不过这种方式还是属于阻塞式的。

    什么叫做阻塞式呢。你想想,开10个窗口可以满足10个人同时买票。但是现在有100个人呢?不 可能再开90个窗口吧,所以每个窗口实际上还是需要排队。也就是说虽然我可以通过开启多个线 程来同时执行很多任务,但是每个任务中的代码仍然是同步的。当某个任务的代码执行时间过 长,也只会影响到当前线程的代码,而其他线程的代码不会受到影响。

    1. 单线程非阻塞式

    假设现在火车站不想开那么多窗口,还是只有1个窗口提供服务,那如何能够提高购票效率呢? 我们可以这样做,把购票的流程分为两步,第一步:预定及付款。第二步:取票。其中,第一步 可以让购票者在网上操作。第二步到火车站的窗口取票。这样,最耗时的工作已经提前完成,不 需要排队。到火车站时,虽然只有1个窗口,1次也只能接待1个人,但是取票的动作很快,平均 每个人耗时不到1分钟,10个人也就不到10分钟就可以处理完成。这样既提高了效率,又少开了 窗口。这也是一种异步的实现。我们可以看到,开1个窗口,就相当于只有1个线程。然后把耗时 的一些操作分成两部分,先把快速能做完的事情做了,这样保证它不会阻塞其他代码的运行。剩 下耗时的部分再单独执行。这就是单线程阻塞式的异步实现机制。

    16-1-3 JS中的异步实现

    我们知道JS引擎就是以单线程的机制来运行代码。那么在JS代码中想要实现异步就只有采用单

    线程非阻塞式的方式。比如下面这段代码:

    console.log("start");

    setTimeout(function(){ console.log("timeout");

    },5000);

    console.log("end");

    这段代码先输出一个字符串"start",然后用时间延迟函数,等到5000秒钟后输出"timeout",在代 码的最后输出"end"。最后的执彳丁结果是:

    start

    end

    //等待5秒后

    timeout

    从结果可以看到end的输出并没有等待时间函数执行完,实际上setTimeout就是异步的实现。代 码的执行流程如下:

    首先执行输出字符串"start",然后开始执行setTimeout函数。由于它是一个异步操作,所以它会 被分为两部分来执行,先调用setTimeout方法,然后把要执行的函数放到一个队列中。代码继续 往下执行,当把所有的代码都执行完后,放到队列中的函数才会被执行。这样,所有异步执行的 函数都不会阻塞其他代码的执行。虽然,这些代码都不是同时执行,但是由于任何代码都不会被 阻塞,所以执行效率会很快。

     

    大家认真看这个图片,然后思考一个问题:当setTimeout执行后,什么时候开始计时的呢?由于 单线程的原因,不可能在setTimeout后就开始执行,因为一个线程同一时间只能做一件事情。执 行后续代码的同时就不可能又去计时。那么只可能是在所有代码执行完后才开始计时,然后5 后执行队列中的回调函数,是这样吗?我们用一段代码来验证下:

    console.log("start");

    setTimeout(function(){

    console.log("timeout");

    },5000);

    for(let i = 0;i <= 500000;i++){

    console.log("i:",i);

    }

    console.log("end");

    这段代码在之前的基础上加了一个循环,循环次数为50万次,然后每次输出i的值。这段循环是 比较耗时的,从实际运行来看,大概需要14秒左右(具体时间可自行测算)。这个时间已经远远 大于setTimeout的等待时间。按照之前的说法,应该先把所有同步的代码执行完,然后再执行异 步的回调方法,结果应该是:

    start

    i:1

    (...)//一直输出到500000

    //耗时 14秒左右

    end

    //等待5秒后

    timeout

    但实际的运行结果是:

    start

    i:1

    (...)//一直输出到500000

    //耗时 14秒左右

    end

    //没有等待

    timeout

    从结果可以看UsetTimeout的计时应该是早就开始了,但是JS是单线程运行,那谁在计时呢?要 解释这个问题,大家一定要先搞明白一件事。JS的单线程并不是指整个JS引擎只有1个线程。它 是指运行代码只有1个线程,但是它还有其他线程来执行其他任务。比如时间函数的计时、AJAX 技术中的和后台交互等操作。所以,实际情况应该是:JS引擎中执行代码的线程开始运行代码,
    当执行到异步方法时,把异步的回调方法放入到队列中,然后由专门计时的线程开始计时。代码 线程继续运行。如果计时的时间已到,那么它会通知代码线程来执行队列中对应的回调函数。当 然,前提是代码线程已经把同步代码执行完后。否则需要继续等待,就像这个例子中一样。

     

    最后,大家一定要注意一件事情,由于执行代码只有1个线程,所以在任何同步代码中出现死循 环,那么它后续的同步代码以及异步的回调函数都无法执行,比如:

    console.log("start");

    setTimeout(function(){

    console.log("timeout"); },5000); console.log("end"); for(;;){}

    timeout用于也不会输出,因为执行代码的线程已经陷入死循环中。

    16-2 Promise实现异步

    前面一讲中我们了解了什么是异步,以及JS中实现异步的原理。这一节咱们将学习JS中实现异 步的具体方法。前面我们已经看到了一个用setTimeout实现的异步操作:

    console.log("start");

    setTimeout(function(){

    console.log("timeout");

    },5000);

    console.log("end");

    16-2-1回调函数

    在调用setTimeout函数时我们传递了一个函数进去,这个函数并没有立即被调用,而是在5秒后 被调用。这种函数也被称为回调函数(关于回调函数请参看前面的内容)。由于JS中的函数是一 等公民,它和其他数据类型一样,可以作为参数传递也可以作为返回值返回,所以经常能够看到 回调函数使用。

    回调地狱

    在异步实现中,回调函数的使用是不可避免的。之前我不是讲过吗,JS的异步是单线程非阻塞式 的。它将一个异步动作分为两步,第一步执行异步方法,然后代码接着往下执行。然后在后面的 某个时刻调用第二步的回调函数,完成后续动作。有的时候,我们希望在异步操作中加入同步的 行为。比如,我想打印4句话,但是每句话都在前一句话的基础上延迟2秒输出。代码如下:

    setTimeout(function(){

    console.log("first");

    setTimeout(function(){

    console.log("second");

    setTimeout(function(){

    console.log("third");

    setTimeout(function(){

    console.log("fourth");

    },2000);

    },2000);

    },2000);

    },2000);

    这段代码能够实现想要的功能,但是总觉得哪里不对。如果输出的内容越来越多,嵌套的代码也 会增多。那无论是编写还是阅读起来都会很恐怖。造成这种情况的罪魁祸首就是回调函数。因为
    你想在前面的异步操作完成后再进行接下来的动作,那只能在它的回调函数中进行,这样就会越 套越多,代码越来越来复杂,俗称"回调地狱"。

    16-2-2 Promise

    为了解决这个问题,在ES6中加入了一个新的对象Promise。Promise提供了一种更合理、更强大 的异步解决方案。接下来我们来看看它的用法。

    new Promise(function(resolve,reject){

    //dosomething

    });

    首先需要创建一个Promise对象,该对象的构造函数中接收一个回调函数,回调函数中可以接收 两个参数,resolve和reject。注意,这个回调函数是在Promise创建后就会调用。它实际上就是异 步操作的第一步。那第二步操作再在哪里做呢? Promise把两个步骤分开了,第二步通过Promise 对象的the n方法实现。

    let pm = new Promise(function(resolve,reject){

    //dosomething

    });

    console.log("go on");

    pm.then(function(){ console.log("异步完成");

    });

    不过要注意的是,then方法的回调函数不是说只要then方法一调用它就会调用,而是在Promise 的回调函数中通过调用resolve触发的。

    let pm = new Promise(function(resolve,reject){

    resolve();

    });

    console.log("go on");

    pm.then(function(){ console.log("异步完成");

    });

    实际上Promise实现异步的原理和之前纯用回调函数的原理是一样的。只是Promise的做法是显示 的将两个步骤分开来写。then方法的回调函数同样会先放入队列中,等待所有的同步方法执行完 后,同时Promise中的resolve也被调用后,该回调函数才会执行。

    调用resolve时还可以把数据传递给then的回调函数。

    let pm = new Promise(function(resolve,reject){ resolve("this is data");

    });

    console.log("go on"); pm.then(function(data){

    console.log("异步完成",data);

    });

    效果:

    Jie-Xie:desktop Jie$ node 1

    go on

    异步完成this is data

    reject是出现错误时调用的方法。它触发的不是then中的回调函数,而是catch中的回调函数。比 如:

    let err = false;

    let pm = new Promise(function(resolve,reject){

    if(!err){

    resolve("this is data");

    }else{ reject("fail");

    }

    }); console.log("go on"); pm.then(function(data){ console.log("异步完成",data);

    });

    pm.catch( function(err){ console.log("出现错误",e rr);

    });

    下面,我把刚才时间函数的异步操作用Promise实现一次。当然,其中setTimeout还是需要使 用,只是在它外面包裹一个Promise对象。

    let pm = new Promise(function(resolve,reject){ setTimeout( function(){

    resolve();

    },2000);

    });

    console.log("go on");

    pm.then(function(){ console.log("异步完成");

    });

    效果和之前一样,但是代码复杂了不少,感觉有点多此一举。接下来做做同步效果。

    let timeout = function(time){ return new Promise(function(resolve,reject){ setTimeout( function(){

    resolve(); },time);

    });

    }

    console.log("go on"); timeout(2000).then(function(){ console.log("first"); return timeout(2000);

    }).then(function(){ console.log("second"); return timeout(2000);

    }).then(function(){ console.log("third");

  • 相关阅读:
    汽车驾驶盲区 无论新手老手都要看看
    看看大货车到底有多少盲区,肯定用得到!救命的!
    大货车的盲区很大的,所以在大货车周围 超车 并线的时候 最好鸣喇叭提示一下...
    换挡时机
    新手眼中的葵花宝典,手把手教你成为一名老司机!
    后视镜什么时候看?老司机也不一定知道
    【调查】开车时,你多长时间看一下后视镜?(安全驾驶)
    究竟什么时候该看哪个后视镜?老司机用经验告诉你答案
    java selenium (五) 元素定位大全
    java selenium (八) Selenium IDE 用法
  • 原文地址:https://www.cnblogs.com/jrzqdlgdx/p/11350727.html
Copyright © 2020-2023  润新知