(82)Wangdao.com第十六天_JavaScript 异步操做

异步操做git

 

  • 单线程模型
    • 指的是,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)github

 

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

 

      • 异步任务
        • 是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。
        • 只有引擎认为某个异步任务能够执行了(好比 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');    // 表示,执行完成后,当即触发 事件,从而开始执行
                  }, 1000);
              }donef2

               

        • 优势:
          • 比较容易理解,能够绑定多个事件,
          • 每一个事件能够指定多个回调函数,
          • 并且能够”去耦合“(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变量,达到效率和资源的最佳平衡。
相关文章
相关标签/搜索