深入理解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 相关文章推荐
HTML中不支持静态Expando的元素的问题
Mar 08 Javascript
javascript 窗口加载蒙板 内嵌网页内容
Nov 19 Javascript
node.js中的console.trace方法使用说明
Dec 09 Javascript
jquery实现可关闭的倒计时广告特效代码
Sep 02 Javascript
JS实现黑色大气的二级导航菜单效果
Sep 18 Javascript
微信小程序前端源码逻辑和工作流
Sep 25 Javascript
bootstrap table实现单击单元格可编辑功能
Mar 28 Javascript
详解Vue中使用v-for语句抛出错误的解决方案
May 04 Javascript
简单了解JavaScript中常见的反模式
Jun 21 Javascript
vue 中固定导航栏的实例代码
Nov 01 Javascript
解决vue elementUI中table里数字、字母、中文混合排序问题
Jan 07 Javascript
微信小程序canvas截取任意形状的实现代码
Jan 13 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
我的群发邮件程序
2006/10/09 PHP
关于php支持分块与断点续传文件下载功能代码
2014/05/09 PHP
php实现建立多层级目录的方法
2014/07/19 PHP
php is_executable判断给定文件名是否可执行实例
2016/09/26 PHP
Laravel框架中Blade模板的用法示例
2017/08/30 PHP
许愿墙中用到的函数
2006/10/07 Javascript
通过身份证号得到出生日期和性别的js代码
2009/11/23 Javascript
js 编写规范
2010/03/03 Javascript
Javascript中Eval函数的使用
2010/03/23 Javascript
基于jquery的无限级联下拉框js插件
2011/10/29 Javascript
javascript客户端遍历控件与获取父容器对象示例代码
2014/01/06 Javascript
js(JavaScript)实现TAB标签切换效果的简单实例
2014/02/26 Javascript
node.js Web应用框架Express入门指南
2014/05/28 Javascript
innerHTML动态添加html代码和脚本兼容多个浏览器
2014/10/11 Javascript
javascript中的Base64、UTF8编码与解码详解
2015/03/18 Javascript
javascript高级选择器querySelector和querySelectorAll全面解析
2016/04/07 Javascript
javaScript中定义类或对象的五种方式总结
2016/12/04 Javascript
Vue和Bootstrap的整合思路详解
2017/06/30 Javascript
vue采用EventBus实现跨组件通信及注意事项小结
2018/06/14 Javascript
JavaScript实现美化滑块效果
2019/05/17 Javascript
微信小程序实现图片选择并预览功能
2019/07/25 Javascript
合并Excel工作薄中成绩表的VBA代码,非常适合教育一线的朋友
2009/04/09 Python
Django集成百度富文本编辑器uEditor攻略
2014/07/04 Python
Redis使用watch完成秒杀抢购功能的代码
2018/05/07 Python
Django学习笔记之为Model添加Action
2019/04/30 Python
Python(PyS60)实现简单语音整点报时
2019/11/18 Python
使用Python生成200个激活码的实现方法
2019/11/22 Python
200行python代码实现贪吃蛇游戏
2020/04/24 Python
美国名牌香水折扣网站:Hottperfume
2021/02/10 全球购物
护理专业自我鉴定
2014/01/30 职场文书
大学生村官考核材料
2014/05/23 职场文书
英语分层教学实施方案
2014/06/15 职场文书
读后感作文评语
2014/12/25 职场文书
年会邀请函的格式及范文五篇
2019/11/02 职场文书
让文件路径提取变得更简单的Python Path库
2021/05/27 Python
分享几种python 变量合并方法
2022/03/20 Python