深入理解JavaScript编程中的同步与异步机制


Posted in Javascript onJune 24, 2015

 JavaScript的优势之一是其如何处理异步代码。异步代码会被放入一个事件队列,等到所有其他代码执行后才进行,而不会阻塞线程。然而,对于初学者来说,书写异步代码可能会比较困难。而在这篇文章里,我将会消除你可能会有的任何困惑。
理解异步代码

JavaScript最基础的异步函数是setTimeout和setInterval。setTimeout会在一定时间后执行给定的函数。它接受一个回调函数作为第一参数和一个毫秒时间作为第二参数。以下是用法举例:
 

console.log( "a" );
setTimeout(function() {
  console.log( "c" )
}, 500 );
setTimeout(function() {
  console.log( "d" )
}, 500 );
setTimeout(function() {
  console.log( "e" )
}, 500 );
console.log( "b" );

正如预期,控制台先输出“a”、“b”,大约500毫秒后,再看到“c”、“d”、“e”。我用“大约”是因为setTimeout事实上是不可预知的。实际上,甚至 HTML5规范都提到了这个问题:

  •     “这个API不能保证计时会如期准确地运行。由于CPU负载、其他任务等所导致的延迟是可以预料到的。”

有趣的是,直到在同一程序段中所有其余的代码执行结束后,超时才会发生。所以如果设置了超时,同时执行了需长时间运行的函数,那么在该函数执行完成之前,超时甚至都不会启动。实际上,异步函数,如setTimeout和setInterval,被压入了称之为Event Loop的队列。

Event Loop是一个回调函数队列。当异步函数执行时,回调函数会被压入这个队列。JavaScript引擎直到异步函数执行完成后,才会开始处理事件循环。这意味着JavaScript代码不是多线程的,即使表现的行为相似。事件循环是一个先进先出(FIFO)队列,这说明回调是按照它们被加入队列的顺序执行的。JavaScript被 node选做为开发语言,就是因为写这样的代码多么简单啊。

Ajax

异步Javascript与XML(AJAX)永久性的改变了Javascript语言的状况。突然间,浏览器不再需要重新加载即可更新web页面。 在不同的浏览器中实现Ajax的代码可能漫长并且乏味;但是,幸亏有jQuery(还有其他库)的帮助,我们能够以很容易并且优雅的方式实现客户端-服务器端通讯。

我们可以使用jQuery跨浏览器接口$.ajax很容易地检索数据,然而却不能呈现幕后发生了什么。比如:

var data;
$.ajax({
  url: "some/url/1",
  success: function( data ) {
    // But, this will!
    console.log( data );
  }
})
// Oops, this won't work...
console.log( data );

较容易犯的错误,是在调用$.ajax之后马上使用data,但是实际上是这样的:
 

xmlhttp.open( "GET", "some/ur/1", true );
xmlhttp.onreadystatechange = function( data ) {
  if ( xmlhttp.readyState === 4 ) {
    console.log( data );
  }
};
xmlhttp.send( null );

底层的XmlHttpRequest对象发起请求,设置回调函数用来处理XHR的readystatechnage事件。然后执行XHR的send方法。在XHR运行中,当其属性readyState改变时readystatechange事件就会被触发,只有在XHR从远端服务器接收响应结束时回调函数才会触发执行。

处理异步代码

异步编程很容易陷入我们常说的“回调地狱”。因为事实上几乎JS中的所有异步函数都用到了回调,连续执行几个异步函数的结果就是层层嵌套的回调函数以及随之而来的复杂代码。

node.js中的许多函数也是异步的。因此如下的代码基本上很常见:
 

var fs = require( "fs" );
fs.exists( "index.js", function() {
  fs.readFile( "index.js", "utf8", function( err, contents ) {
    contents = someFunction( contents ); // do something with contents
    fs.writeFile( "index.js", "utf8", function() {
      console.log( "whew! Done finally..." );
    });
  });
});
console.log( "executing..." );

下面的客户端代码也很多见:

 

GMaps.geocode({
  address: fromAddress,
  callback: function( results, status ) {
    if ( status == "OK" ) {
      fromLatLng = results[0].geometry.location;
      GMaps.geocode({
        address: toAddress,
        callback: function( results, status ) {
          if ( status == "OK" ) {
            toLatLng = results[0].geometry.location;
            map.getRoutes({
              origin: [ fromLatLng.lat(), fromLatLng.lng() ],
              destination: [ toLatLng.lat(), toLatLng.lng() ],
              travelMode: "driving",
              unitSystem: "imperial",
              callback: function( e ){
                console.log( "ANNNND FINALLY here's the directions..." );
                // do something with e
              }
            });
          }
        }
      });
    }
  }
});

Nested callbacks can get really nasty, but there are several solutions to this style of coding.

嵌套的回调很容易带来代码中的“坏味道”,不过你可以用以下的几种风格来尝试解决这个问题

  •     The problem isn't with the language itself; it's with the way programmers use the language — Async Javascript.

    没有糟糕的语言,只有糟糕的程序猿 ——异步JavaSript

命名函数

清除嵌套回调的一个便捷的解决方案是简单的避免双层以上的嵌套。传递一个命名函数给作为回调参数,而不是传递匿名函数:
 

var fromLatLng, toLatLng;
var routeDone = function( e ){
  console.log( "ANNNND FINALLY here's the directions..." );
  // do something with e
};
var toAddressDone = function( results, status ) {
  if ( status == "OK" ) {
    toLatLng = results[0].geometry.location;
    map.getRoutes({
      origin: [ fromLatLng.lat(), fromLatLng.lng() ],
      destination: [ toLatLng.lat(), toLatLng.lng() ],
      travelMode: "driving",
      unitSystem: "imperial",
      callback: routeDone
    });
  }
};
var fromAddressDone = function( results, status ) {
  if ( status == "OK" ) {
    fromLatLng = results[0].geometry.location;
    GMaps.geocode({
      address: toAddress,
      callback: toAddressDone
    });
  }
};
GMaps.geocode({
  address: fromAddress,
  callback: fromAddressDone
});

此外, async.js 库可以帮助我们处理多重Ajax requests/responses. 例如:
 

async.parallel([
  function( done ) {
    GMaps.geocode({
      address: toAddress,
      callback: function( result ) {
        done( null, result );
      }
    });
  },
  function( done ) {
    GMaps.geocode({
      address: fromAddress,
      callback: function( result ) {
        done( null, result );
      }
    });
  }
], function( errors, results ) {
  getRoute( results[0], results[1] );
});

这段代码执行两个异步函数,每个函数都接收一个名为"done"的回调函数并在函数结束的时候调用它。当两个"done"回调函数结束后,parallel函数的回调函数被调用并执行或处理这两个异步函数产生的结果或错误。

Promises模型
引自 CommonJS/A:

  •     promise表示一个操作独立完成后返回的最终结果。

有很多库都包含了promise模型,其中jQuery已经有了一个可使用且很出色的promise API。jQuery在1.5版本引入了Deferred对象,并可以在返回promise的函数中使用jQuery.Deferred的构造结果。而返回promise的函数则用于执行某种异步操作并解决完成后的延迟。
 

var geocode = function( address ) {
  var dfd = new $.Deferred();
  GMaps.geocode({
    address: address,
    callback: function( response, status ) {
      return dfd.resolve( response );
    }
  });
  return dfd.promise();
};
var getRoute = function( fromLatLng, toLatLng ) {
  var dfd = new $.Deferred();
  map.getRoutes({
    origin: [ fromLatLng.lat(), fromLatLng.lng() ],
    destination: [ toLatLng.lat(), toLatLng.lng() ],
    travelMode: "driving",
    unitSystem: "imperial",
    callback: function( e ) {
      return dfd.resolve( e );
    }
  });
  return dfd.promise();
};
var doSomethingCoolWithDirections = function( route ) {
  // do something with route
};
$.when( geocode( fromAddress ), geocode( toAddress ) ).
  then(function( fromLatLng, toLatLng ) {
    getRoute( fromLatLng, toLatLng ).then( doSomethingCoolWithDirections );
  });

这允许你执行两个异步函数后,等待它们的结果,之后再用先前两个调用的结果来执行另外一个函数。

  •     promise表示一个操作独立完成后返回的最终结果。

在这段代码里,geocode方法执行了两次并返回了一个promise。异步函数之后执行,并在其回调里调用了resolve。然后,一旦两次调用resolve完成,then将会执行,其接收了之前两次调用geocode的返回结果。结果之后被传入getRoute,此方法也返回一个promise。最终,当getRoute的promise解决后,doSomethingCoolWithDirections回调就执行了。
 
事件
事件是另一种当异步回调完成处理后的通讯方式。一个对象可以成为发射器并派发事件,而另外的对象则监听这些事件。这种类型的事件处理方式称之为 观察者模式 。 backbone.js 库在withBackbone.Events中就创建了这样的功能模块。
 

var SomeModel = Backbone.Model.extend({
  url: "/someurl"
});
var SomeView = Backbone.View.extend({
  initialize: function() {
    this.model.on( "reset", this.render, this );
    this.model.fetch();
  },
  render: function( data ) {
    // do something with data
  }
});
var view = new SomeView({
  model: new SomeModel()
});

还有其他用于发射事件的混合例子和函数库,例如 jQuery Event Emitter , EventEmitter , monologue.js ,以及node.js内建的 EventEmitter 模块。

  •     事件循环是一个回调函数的队列。

一个类似的派发消息的方式称为 中介者模式 , postal.js 库中用的即是这种方式。在中介者模式,有一个用于所有对象监听和派发事件的中间人。在这种模式下,一个对象不与另外的对象产生直接联系,从而使得对象间都互相分离。

绝不要返回promise到一个公用的API。这不仅关系到了API用户对promises的使用,也使得重构更加困难。不过,内部用途的promises和外部接口的事件的结合,却可以让应用更低耦合且便于测试。

在先前的例子里面,doSomethingCoolWithDirections回调函数在两个geocode函数完成后执行。然后,doSomethingCoolWithDirections才会获得从getRoute接收到的响应,再将其作为消息发送出去。
 

var doSomethingCoolWithDirections = function( route ) {
  postal.channel( "ui" ).publish( "directions.done", {
    route: route
  });
};

这允许了应用的其他部分不需要直接引用产生请求的对象,就可以响应异步回调。而在取得命令时,很可能页面的好多区域都需要更新。在一个典型的jQuery Ajax过程中,当接收到的命令变化时,要顺利的回调可能就得做相应的调整了。这可能会使得代码难以维护,但通过使用消息,处理UI多个区域的更新就会简单得多了。
 

var UI = function() {
  this.channel = postal.channel( "ui" );
  this.channel.subscribe( "directions.done", this.updateDirections ).withContext( this );
};
UI.prototype.updateDirections = function( data ) {
  // The route is available on data.route, now just update the UI
};
app.ui = new UI();

另外一些基于中介者模式传送消息的库有 amplify, PubSubJS, and radio.js。

结论

JavaScript 使得编写异步代码很容易. 使用 promises, 事件, 或者命名函数来避免“callback hell”. 为获取更多javascript异步编程信息,请点击Async JavaScript: Build More Responsive Apps with Less . 更多的实例托管在github上,地址NetTutsAsyncJS,赶快Clone吧 !

Javascript 相关文章推荐
自写的一个jQuery圆角插件
Oct 26 Javascript
如何创建一个JavaScript弹出DIV窗口层的效果
Sep 25 Javascript
JS实现金额转换(将输入的阿拉伯数字)转换成中文的实现代码
Sep 30 Javascript
js动态修改input输入框的type属性(实现方法解析)
Nov 13 Javascript
javascript中全局对象的parseInt()方法使用介绍
Dec 19 Javascript
js获取和设置属性的方法
Feb 20 Javascript
基于Bootstrap实现的下拉菜单手机端不能选择菜单项的原因附解决办法
Jul 22 Javascript
js简单获取表单中单选按钮值的方法
Aug 23 Javascript
浅析script标签中的defer与async属性
Nov 30 Javascript
js操作浏览器的参数方法
Jan 21 Javascript
AngularJS之ionic 框架下实现 Localstorage本地存储
Apr 22 Javascript
JS 调试中常见的报错问题解决方法
May 20 Javascript
详解JavaScript中的客户端消息框架设计原理
Jun 24 #Javascript
jquery实现从数组移除指定的值
Jun 24 #Javascript
浅谈关于JavaScript API设计的一些建议和准则
Jun 24 #Javascript
详解JavaScript的策略模式编程
Jun 24 #Javascript
jquery控制页面部分刷新的方法
Jun 24 #Javascript
js实现延迟加载的方法
Jun 24 #Javascript
介绍JavaScript的一个微型模版
Jun 24 #Javascript
You might like
PHP获取毫秒级时间戳的方法
2015/04/15 PHP
细品javascript 寻址,闭包,对象模型和相关问题
2009/04/27 Javascript
javascript 自动填写表单的实现方法
2010/04/09 Javascript
JQuery入门——用one()方法绑定事件处理函数(仅触发一次)
2013/02/05 Javascript
js动态给table添加/删除tr的方法
2013/08/02 Javascript
原生JS实现拖拽图片效果
2020/08/27 Javascript
JavaScript位移运算符(无符号) >>> 三个大于号 的使用方法详解
2016/03/31 Javascript
JavaScript中获取HTML元素值的三种方法
2016/06/20 Javascript
判断数组的最佳方法(推荐)
2016/10/11 Javascript
Bootstrap基本样式学习笔记之标签(5)
2016/12/07 Javascript
Angular使用$http.jsonp发送跨站请求的方法
2017/03/16 Javascript
基于vue实现滚动条滚动到指定位置对应位置数字进行tween特效
2019/04/18 Javascript
layui 监听select选择 获取当前select的ID名称方法
2019/09/24 Javascript
vuex实现数据状态持久化
2019/11/11 Javascript
JavaScript中继承原理与用法实例入门
2020/05/09 Javascript
vue使用vant中的checkbox实现全选功能
2020/11/17 Vue.js
Vue+scss白天和夜间模式切换功能的实现方法
2021/01/05 Vue.js
Eclipse + Python 的安装与配置流程
2013/03/05 Python
python实现调用其他python脚本的方法
2014/10/05 Python
Python解决八皇后问题示例
2018/04/22 Python
对python自动生成接口测试的示例讲解
2018/11/30 Python
Django Aggregation聚合使用方法解析
2019/08/01 Python
Django 自动生成api接口文档教程
2019/11/19 Python
Pycharm如何导入python文件及解决报错问题
2020/05/10 Python
基于Python的Jenkins的二次开发操作
2020/05/12 Python
乐天旅游台湾网站:Rakuten Travel TW
2017/06/01 全球购物
法国滑雪假期的专家:Ski Planet
2019/11/02 全球购物
公司庆典活动邀请函
2014/01/09 职场文书
节约电力资源的建议书
2014/03/12 职场文书
公司承诺书怎么写
2014/05/24 职场文书
法人任命书范本
2014/06/04 职场文书
2015年党员干部承诺书
2015/01/21 职场文书
2015年政协委员工作总结
2015/05/20 职场文书
青年干部培训班学习心得体会
2016/01/06 职场文书
创业计划书之酒厂
2019/10/14 职场文书
用PYTHON去计算88键钢琴的琴键频率和音高
2022/04/10 Python