异步操作

  • 单线程模型

    • 指的是,JavaScript 只在一个线程上运行
    • 也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待
      • 注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。
    • 事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合
    • JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。
      • JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,
      • 因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。
      • 好处:
        • 实现起来比较简单,执行环境相对单纯
        • Node 可以用很少的资源,应付大流量访问的原因
      • 坏处:
        • 只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。
        • 常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行
      • JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。
      • JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的 “事件循环”机制(Event Loop)
      • 为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。
    • 同步任务和异步任务

程序里面所有的任务,可以分成两类:同步任务(synchronous)异步任务(asynchronous)

      • 同步任务

        • 是那些没有被引擎挂起、在主线程上排队执行的任务。
        • 只有前一个任务执行完毕,才能执行后一个任务。
      • 异步任务

        • 是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。
        • 只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。
        • 排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有”堵塞“效应。
        • 举例来说
            • Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。

              • 如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;
              • 如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数
    • 任务队列和事件循环

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),

里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)

  • 首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。
  • 如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。
  • 等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
  • 异步任务的写法通常是回调函数。

    • 一旦异步任务重新进入主线程,就会执行对应的回调函数。
    • 如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
  • JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?

    • 答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了
    • 这种循环检查的机制,就叫做事件循环(Event Loop)。
    • 维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
    • 异步操作的模式
      • 回调函数

        • 是异步操作最基本的方法

          • 下面是两个函数f1f2,编程的意图是f2必须等到f1执行完成,才能执行

            function f1() {
            // ...
            } function f2() {
            // ...
            } f1();
            f2();

            上面代码的问题在于,如果f1是异步操作,f2会立即执行,不会等到f1结束再执行

            • 这时,可以考虑改写f1,把f2写成f1的回调函数

              function f1(callback) {
              // ...
              callback();
              } function f2() {
              // ...
              } f1(f2);
        • 回调函数的优点:

          • 简单、容易理解和实现
        • 回调函数的缺点:
          • 不利于代码的阅读和维护,
          • 各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),
          • 而且每个任务只能指定一个回调函数
      • 事件监听
        • 采用事件驱动模式。
        • 异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
        • 以 f1 和 f2 为例。首先,为 f1 绑定一个事件(这里采用的 jQuery 的写法)
          • f1.on('done', f2);    // 当 f1 发生 done 事件,就执行 f2

            f1进行改写:

            • function f1() {
              setTimeout(function () {
              // ...
              f1.trigger('done'); // 表示,执行完成后,立即触发 done 事件,从而开始执行f2
              }, 1000);
              }
        • 优点:
          • 比较容易理解,可以绑定多个事件,
          • 每个事件可以指定多个回调函数,
          • 而且可以”去耦合“(decoupling),有利于实现模块化
        • 缺点:
          • 整个程序都要变成事件驱动型,运行流程会变得很不清晰。
          • 阅读代码的时候,很难看出主流程。
      • ”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)
        • 事件完全可以理解成”信号“,如果存在一个”信号中心“,
        • 某个任务执行完成,就向信号中心 ”发布“(publish)一个信号,
        • 其他任务可以向信号中心”订阅“(subscribe)这个信号,从而知道什么时候自己可以开始执行。
        • 可以用多种方式实现这个模式

          • 采用的是 Ben Alman 的 Tiny Pub/Sub,这是 jQuery 的一个插件
            • 首先,f2 向信号中心 jQuery 订阅 done 信号

              • jQuery.subscribe('done', f2);
            • 然后,f1 进行如下改写
              • function f1() {
                setTimeout(function () {
                // ...
                jQuery.publish('done');
                }, 1000);
                }

                f1 执行完成后,向信号中心 jQuery 发布 done 信号,从而引发 f2 的执行

            • f2 完成执行后,可以取消订阅(unsubscribe)
              • jQuery.unsubscribe('done', f2);
            • 性质与“事件监听”类似,但是明显优于后者。
              • 因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行
    • 异步操作的流程控制

        • 如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序
      • 串行执行:
        • 我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。

          var items = [ 1, 2, 3, 4, 5, 6 ];
          var results = []; function async(arg, callback) {
          console.log('参数为 ' + arg +' , 1秒后返回结果');
          setTimeout(function () { callback(arg * 2); }, 1000);
          } function final(value) {
          console.log('完成: ', value);
          } function series(item) {
          if(item) {
          async( item, function(result) {
          results.push(result);
          return series(items.shift());
          });
          } else {
          return final(results[results.length - 1]);
          }
          } series(items.shift());
        • 函数 series() 就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。
        • items[] 数组保存每一个异步任务的参数,results[] 数组保存每一个异步任务的运行结果。
        • 注意,上面的写法需要六秒,才能完成整个脚本。
      • 并行执行

        • 流程控制函数也可以并行执行,即所有异步任务同时执行,等到全部完成以后,才执行 final 函数

          • var items = [ 1, 2, 3, 4, 5, 6 ];
            var results = []; function async(arg, callback) {
            console.log('参数为 ' + arg +' , 1秒后返回结果');
            setTimeout(function () { callback(arg * 2); }, 1000);
            } function final(value) {
            console.log('完成: ', value);
            } items.forEach(function(item) {
            async(item, function(result){
            results.push(result);
            if(results.length === items.length) {
            final(results[results.length - 1]);
            }
            })
            });
          • 上面代码中,forEach方法会同时发起六个异步任务,等到它们全部完成以后,才会执行 final 函数
        • 相比而言,上面的写法只要一秒,就能完成整个脚本。
        • 这就是说,并行执行的效率较高,比起串行执行一次只能执行一个任务,较为节约时间。
        • 但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式
      • 并行与串行的结合

        • 所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行 个异步任务,这样就避免了过分占用系统资源。

          • var items = [ 1, 2, 3, 4, 5, 6 ];
            var results = [];
            var running = 0;
            var limit = 2; function async(arg, callback) {
            console.log('参数为 ' + arg +' , 1秒后返回结果');
            setTimeout(function () { callback(arg * 2); }, 1000);
            } function final(value) {
            console.log('完成: ', value);
            } function launcher() {
            while(running < limit && items.length > 0) {
            var item = items.shift();
            async(item, function(result) {
            results.push(result);
            running--;
            if(items.length > 0) {
            launcher();
            } else if(running == 0) {
            final(results);
            }
            });
            running++;
            }
            } launcher();

            上面代码中,最多只能同时运行两个异步任务。变量 running 记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于 0,就表示所有任务都执行完了,这时就执行 final() 函数。

        • 这段代码需要三秒完成整个脚本,处在串行执行和并行执行之间。
        • 通过调节limit变量,达到效率和资源的最佳平衡。

(82)Wangdao.com第十六天_JavaScript 异步操作的更多相关文章

  1. (80)Wangdao.com第十六天_JavaScript Object 对象的相关方法

    Object 对象的相关方法 Object.getPrototypeOf() 返回参数对象的原型. 这是获取某对象的原型对象的标准方法. var F = function () {}; var f = ...

  2. (81)Wangdao.com第十六天_JavaScript 严格模式

    严格模式 除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode).顾名思义,这种模式采用更加严格的 JavaScript 语法 同样的代码,在正常模式和严格模 ...

  3. (59)Wangdao.com第十天_JavaScript 对象在 栈和堆

    对象的属性值 如果要使用特殊的属性名,需 对象["属性名"] = 属性值       // 存 对象["属性名"]       // 取 obj["1 ...

  4. (62)Wangdao.com第十天_JavaScript 变量的作用域

    在 js 中有两种作用域:全局作用域,局部作用域. 全局作用域 直接写在 <script> 标签中的变量和方法. 在网页打开时创建,在网页关闭时销毁. 全局作用域有一个全局对象 windo ...

  5. (60)Wangdao.com第十天_JavaScript 函数_作用域_闭包_IIFE_回调函数_eval

    函数        实现特定功能的 n 条语句封装体. 1. 创建一个函数对象 var myFunc = new Function(); // typeof myFunc 将会打印 function ...

  6. (61)Wangdao.com第十天_JavaScript 立即执行函数

    1. 立即执行函数 创建完了就执行,只执行完就不再执行了. ( function(){} )(); 例 ( function(a,b){ alert("Hello ,我是一个匿名函数!&qu ...

  7. (64)Wangdao.com第十天_JavaScript 对象的 toString() 方法改变输出

    JavaScript 对象的 toString() 方法改变输出 在平常,我们 console.log(对象);    // 会打印 [Object Object] 但是我们想要更详细的输出,此时,我 ...

  8. (65)Wangdao.com第十天_JavaScript 垃圾回收机制 GC

    垃圾积累过多,致使程序运行缓慢,什么是垃圾? 当堆中某个内容,再也没有指针指向它,我们将再也用不了它,此时就是一个垃圾. 出现这种情况是因为 obj = null; 此时,js 中的垃圾回收机制会自动 ...

  9. (86)Wangdao.com第十九天_JavaScript 接口之 ParentNode 和 ChildNode

    ParentNode 接口,ChildNode 接口 节点对象除了继承 Node 接口以外,还会继承其他接口. ParentNode 接口 表示当前节点是一个父节点,提供一些处理子节点的方法. Chi ...

随机推荐

  1. DirectX11 With Windows SDK--14 深度测试

    前言 当使用加法/减法/乘法颜色混合,或者使用透明混合的时候,在经过深度测试时可能会引发一些问题.例如现在我们需要使用加法混合来绘制一系列对象,而这些对象彼此之间不会相互阻挡.若我们仍使用原来的深度测 ...

  2. WPF中的常用布局

    一 写在开头1.1 写在开头评价一门技术的好坏得看具体的需求,没有哪门技术是面面俱到地好. 1.2 本文内容本文主要内容为WPF中的常用布局,大部分内容转载至https://blog.csdn.net ...

  3. oldboy s21day07(深浅拷贝及文件操作)

    #!/usr/bin/env python# -*- coding:utf-8 -*- # 1.看代码写结果'''v1 = [1, 2, 3, 4, 5]v2 = [v1, v1, v1]v1.app ...

  4. tomcat无法正常关闭问题分析及解决

    问题描述 通常,我们都会直接使用tomcat提供的脚本执行关闭操作,如下: # sh bin/shutdown.sh Using CATALINA_BASE: /usr/local/apache-to ...

  5. [物理学与PDEs]第5章习题6 各向同性材料时强椭圆性条件的等价条件

    在线性弹性时, 证明各向同性材料, 强椭圆性条件 (5. 6) 等价于 Lam\'e 常数满足 $$\bex \mu>0,\quad \lm+2\mu>0.  \eex$$ 证明: (1) ...

  6. v-charts 和 websocket实现数据展示动态推送

    v-charts https://v-charts.js.org/#/ ELEMENT力作: 在使用 echarts 生成图表时,经常需要做繁琐的数据类型转化.修改复杂的配置项,v-charts 的出 ...

  7. 【hdu 5632】Rikka with Array

    Description As we know, Rikka is poor at math. Yuta is worrying about this situation, so he gives Ri ...

  8. 在JS中如何判断所输入的是一个数、整数、正数、非数值?

    1.判断是否为一个数字: Number(num)不为 NaN,说明为数字 2. 判断一个数为正数: var num=prompt("请输入:"); if(Number(num)&g ...

  9. spring cloud(学习笔记)高可用注册中心(Eureka)的实现(一)

    最近在学习的时候,发现微服务架构中,假如只有一个注册中心,那这个注册中心挂了可怎么办,这样的系统,既不安全,稳定性也不好,网上和书上找了一会,发现这个spring cloud早就想到了,并帮我们解决了 ...

  10. CSS盒模型(Box Model)

    阅读目录 1. 什么是CSS盒模型 2. IE盒模型和W3C盒模型 3. CSS3属性box-sizing 4. 关于盒模型的使用 在最初接触CSS的时候,对于CSS盒模型的不了解,撞了很多次的南墙呀 ...