理解javascript异步编程

一、异步机制

JavaScript的执行环境是单线程的,单线程的好处是执行环境简单,不用去考虑诸如资源同步,死锁等多线程阻塞式编程等所需要面对的恼人的问题。但带来的坏处是当一个任务执行时间较长时,后面的任务会等待很长时间。在浏览器端就会出现浏览器假死,鼠标无法响应等情况。所以在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应。所谓异步执行,不同于同步执行(程序的执行顺序与任务的排列顺序是一致的、同步的),每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。既然Javascript是单线程的,那它又如何能够异步的执行呢?

二、Javascript线程模型和事件驱动

JavaScript有一个基于事件循环的并发模式。这个模式与C语言和java有很大不同。

运行时的概念


函数调用形成堆栈帧。

function f(b){
  var a = 12;
  return a+b+35;
}

function g(x){
  var m = 4;
  return f(m*x);
}

g(21);

当调用函数g时,创建第一个包含g参数和局部变量的帧。当g函数调用f函数时,创建包含f参数和局部变量第二个堆栈帧并推到第一个堆栈帧的顶部。当f返回时,顶部的堆栈帧元素被弹出(只留下g调用)。当g函数返回时,堆栈为空。


堆是一个大型的非结构化区域,对象被分配到堆中。
队列
一个javascript运行环境包含一个信息队列,这个队列是一系列将被执行的信息列表。每一个消息被关联到一个函数上。当堆栈为空时,从消息队列中取出一个消息并进行处理。该处理包含调用相关的函数(以及因此产生一个初始化的堆栈帧)。当堆栈再次为空时,消息处理结束。
事件循环

事件循环的名字源于它的实现,经常像下面这样:

while(queue.waitForMessage()){
 queue.processNextMessage();
}

queue.waitForMessage同步等待一个消息。

1、运行到完成
每个消息完全处理之后,其它消息才会被处理。这样的好处就是当一个函数不能被提前,只能等其他函数执行完毕(并且可以修改数据的函数操作)。这不同于C,例如,如果一个函数在一个线程运行时,它可以停在任何点运行在另一个线程一些其他的代码。这种模式的缺点是,如果一个消息时间过长完成,Web应用程序无法处理像点击或滚动的用户交互。该浏览器可缓解此与“脚本花费的时间太长运行”对话框。一个很好的做法,遵循的是使信息处理短,如果可能削减一个消息到几条消息。
2、添加消息
在网页浏览器中,事件可以在任何时候添加,一个事件发生并伴随事件监听绑定到事件上。如果没有事件监听,则事件丢失。就像点击一个元素,元素上绑定点击事件。调用setTimeout时,当函数的第二个参数时间被传递进去,将添加一个消息到队列中。如果在队列中没有其他消息,该消息被立即处理;然而,如果有消息,则setTimeout的信息将必须等待其它消息以进行处理。由于这个原因,第二个参数是最小的时间,而不是一个保证时间。
3、几个运行环境之间的通信
一个web worker或跨域iframe都有自己的堆栈,堆,和消息队列。两个不同的运行环境只能通过postMessage的方法发送消息进行通信。这种方法增加了一个消息到其他运行时,如果后者监听消息事件。
从不阻塞

事件循环模型是javascript的一个很有意思的属性,不像其它语言,它从不阻塞。假定浏览器中有一个专门用于事件调度的实例(该实例可以是一个线程,我们可以称之为事件分发线程event dispatch thread),该实例的工作就是一个不结束的循环,从事件队列中取出事件,处理所有很事件关联的回调函数(event handler)。注意回调函数是在Javascript的主线程中运行的,而非事件分发线程中,以保证事件处理不会发生阻塞。通过事件和回调的I/O操作是一个典型的表现,所以当应用等待索引型数据库查询返回或XHR请求返回时,它仍然可以处理其他事情比如用户输入。

三、回调

回调是javascript的基础,函数被作为参数进行传递。像下面:

f1();
f2();
f3();

如果f1中执行了大量的耗时操作,而且f2需要在f1之后执行。则程序可以改为回调的形式。如下:

function f1(callback){
  setTimeout(function () {
    // f1的大量耗时任务代码并的到三个结果i,l,you.
    console.log("this is function1");
    var i = "i", l = "love", y = "you";
    if (callback && typeof(callback) === "function") {
      callback(i,l,y);
    }
  }, 50);
}

function f2(a, b, c) {
  alert(a + " " + b + " " + c);
  console.log("this is function2");
}

function f3(){console.log("this is function3");}
f1(f2);
f3();

运行结果:

this is function3
this is function1
i love you
this is function2

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单,轻量级(不需要额外的库)。缺点是各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。某个操作需要经过多个非阻塞的IO操作,每一个结果都是通过回调,产生意大利面条式(spaghetti)的代码。

operation1(function(err, result) {
  operation2(function(err, result) {
    operation3(function(err, result) {
      operation4(function(err, result) {
        operation5(function(err, result) {
          // do something useful
        })
      })
    })
  })
})

四、事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

// plain, non-jQuery version of hooking up an event handler
var clickity = document.getElementById("clickity");
clickity.addEventListener("click", function (e) {
  //console log, since it's like ALL real world scenarios, amirite?
  console.log("Alas, someone is pressing my buttons…");
});

// the obligatory jQuery version
$("#clickity").on("click", function (e) {
  console.log("Alas, someone is pressing my buttons…");
});

也可以自定义事件进行监听,关于自定义事件,属于另外一部分的内容。这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

五、观察者模式

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

var pubsub = (function(){
  var q = {}
    topics = {},
    subUid = -1;
  //发布消息
  q.publish = function(topic, args) {
    if(!topics[topic]) {return;}
    var subs = topics[topic],
      len = subs.length;
    while(len--) {
      subs[len].func(topic, args);
    }
    return this;
  };
  //订阅事件
  q.subscribe = function(topic, func) {
    topics[topic] = topics[topic] ? topics[topic] : [];
    var token = (++subUid).toString();
    topics[topic].push({
      token : token,
      func : func
    });
    return token;
  };
  return q;
  //取消订阅就不写了,遍历topics,然后通过保存前面返回token,删除指定元素
})();
//触发的事件
var f2 = function(topics, data) {
  console.log("logging:" + topics + ":" + data);
  console.log("this is function2");
}

function f1(){
   setTimeout(function () {
      // f1的任务代码
      console.log("this is function1");
    //发布消息'done'
    pubsub .publish('done', 'hello world');
    }, 1000);
}
pubsub.subscribe('done', f2);
f1();

上面代码的运行结果为:

this is function1
logging:done:hello world
this is function2

观察者模式的实现方法有很多种,也可以直接借用第三方库。这种方法的性质与"事件监听"类似(观察者模式和自定义事件非常相似),但是明显优于后者。观察者模式和事件监听一样具有良好的去耦性,并且有一个消息中心,通过对消息中心的处理,可以良好地监控程序运行。

六、Promises对象

Promises的概念是由CommonJS小组的成员在 Promises/A规范 中提出来的。Promises被逐渐用作一种管理异步操作回调的方法,但出于它们的设计,它们远比那个有用得多。Promise允许我们以同步的方式写代码,同时给予我们代码的异步执行。

function f1(){
  var def = $.Deferred();
  setTimeout(function () {
    // f1的任务代码
    console.log("this is f1");
    def.resolve();
  }, 500);
  return def.promise();
}

function f2(){
  console.log("this is f2");
}

f1().then(f2);

上面代码的运行结果为:

this is f1
this is f2

上面引用的是jquery对Promises/A的实现,jquery中还有一系列方法,具体可参考:Deferred Object.关于Promises,强烈建议读一下You're Missing the Point of Promises.还有很多第三方库实现了Promises,如:Q、Bluebird、 mmDeferred 等。Promise(中文:承诺)其实为一个有限状态机,共有三种状态:pending(执行中)、fulfilled(执行成功)和rejected(执行失败)。其中pending为初始状态,fulfilled和rejected为结束状态(结束状态表示promise的生命周期已结束)。状态转换关系为:pending->fulfilled,pending->rejected。随着状态的转换将触发各种事件(如执行成功事件、执行失败事件等)。 下节具体讲述状态机实现js异步编程。

七、状态机

Promises的本质实际就是通过状态机来实现的,把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度。关于Promises可参考:JS魔法堂:剖析源码理解Promises/A规范 。

八、ES6对异步的支持

这是一个新的技术,成为2015年的ECMAScript(ES6)标准的一部分。该技术的规范已经完成,但实施情况在不同的浏览器不同,在浏览器中的支持情况如下。

var f1 = new Promise(function(resolve, reject) {
  setTimeout(function () {
    // f1的任务代码
    console.log("this is f1");
    resolve("Success");

  }, 500);
});
function f2(val){
  console.log(val + ":" + "this is f2");
}
function f3(){
  console.log("this is f3")
}
f1.then(f2);
f3();

以上代码在Chrome 版本43中的运行结果为:

this is f3
this is f1
Success:this is f2

以上就是针对javascript异步编程的了解学习,之后还有相关文章进行分享,不要错过哦。

(0)

相关推荐

  • Javascript异步编程模型Promise模式详细介绍

    Promise 编程模式也被称为 thenable,可以理解为 延迟后执行.每个 Promise 都拥有一个叫做 then 的唯一接口,当 Promise 失败或成功时,它就会进行回调.它代表了一种可能会长时间运行而且不一定必须完成的操作结果.这种模式不会阻塞和等待长时间的操作完成,而是返回一个代表了承诺的(promised)结果的对象. 当前的许多 JavaScript 库(如 jQuery 和 Dojo.AngularJS)均添加了这种称为 Promise 的抽象.通过这些库,开发人员能够在

  • javascript异步编程

    就好像排队,前面的人忙着忙着突然上厕所了,后面的人阻塞在这里,因此我们就需要让前面的人死到一边去,让后面的人跟进--AJAX就是这个概念,请求还在继续,但我们还可以做其他事. javascript中实现这个功能的是来自BOM的一个函数setTimeout,但相关的DOM操作也提供了一系列实现.如XMLHttpRequest对象与script标签的onreadystatechange回调,image的onload与onerror回调,iframe的onload,DOM元素的事件回调,HTML5的跨

  • Javascript异步编程的4种方法让你写出更出色的程序

    你可能知道,Javascript语言的执行环境是"单线程"(single thread). 所谓"单线程",就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推.  这种模式的好处是实现起来比较简单,执行环境相对单纯:坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其

  • javascript异步编程代码书写规范Promise学习笔记

    最近工作轻松了点,想起了以前总是看到的一个单词promise,于是耐心下来学习了一下. 一:Promise是什么?为什么会有这个东西? 首先说明,Promise是为了解决javascript异步编程时候代码书写的方式产生的. 随着javascript的发展,异步的场景越来越多.前端有AJAX,setTimeout等,后端Node异步更多.按照传统的做法,那么就是各种回调嵌回调.代码可以把人绕晕. 这个时候,CommonJS社区提出了一个叫做Promise/A+的规范,这个规范定义了如何书写异步代

  • 跟我学习javascript解决异步编程异常方案

    一.JavaScript异步编程的两个核心难点 异步I/O.事件驱动使得单线程的JavaScript得以在不阻塞UI的情况下执行网络.文件访问功能,且使之在后端实现了较高的性能.然而异步风格也引来了一些麻烦,其中比较核心的问题是: 1.函数嵌套过深 JavaScript的异步调用基于回调函数,当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成 金字塔型结构.这不仅使得代码变难看难懂,更使得调试.重构的过程充满风险. 2.异常处理 回调嵌套不仅仅是使代码变得杂乱,也使得错误处理更复杂.这

  • JavaScript中实现异步编程模式的4种方法

    你可能知道,Javascript语言的执行环境是"单线程"(single thread). 所谓"单线程",就是指一次只能完成一件任务.如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推. 这种模式的好处是实现起来比较简单,执行环境相对单纯:坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行.常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他

  • 详解JavaScript异步编程中jQuery的promise对象的作用

    Promise, 中文可以理解为愿望,代表单个操作完成的最终结果.一个Promise拥有三种状态:分别是unfulfilled(未满足的).fulfilled(满足的).failed(失败的),fulfilled状态和failed状态都可以被监听.一个愿望可以从未满足状态变为满足或者失败状态,一旦一个愿望处于满足或者失败状态,其状态将不可再变化.这种"不可改变"的特性对于一个Promise来说非常的重要,它可以避免Promise的状态监听器修改一个Promise的状态导致别的监听器的行

  • javascript使用Promise对象实现异步编程

    Promise对象是CommonJS工作组为异步编程提供的统一接口,是ECMAScript6中提供了对Promise的原生支持,Promise就是在未来发生的事情,使用Promise可以避免回调函数的层层嵌套,还提供了规范更加容易的对异步操作进行控制.提供了reject,resolve,then和catch等方法. 使用PROMISE Promise是ES6之后原生的对象,我们只需要实例化Promise对象就可以直接使用. 实例化Promise: var promise = new Promis

  • 基于javascript的异步编程实例详解

    本文实例讲述了基于javascript的异步编程.分享给大家供大家参考,具体如下: 异步函数这个术语有点名不副实,调用一个函数后,程序只在该函数返回后才能继续.JavaScript程序员如果称一个函数为异步的,其意思就是这个函数会导致将来再运行另一个函数,后者取自于事件队列.如果后面这个函数是作为参数传递给前者的,则称其为回调函数. callback 回调函数是异步编程最基本的方式. 采用这种方式,我们把同步操作变成了异步操作,主函数不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟

  • JavaScript异步编程:异步数据收集的具体方法

    Asyncjs/seriesByHand.js 复制代码 代码如下: var fs = require('fs');process.chdir('recipes'); // 改变工作目录var concatenation = ''; fs.readdir('.', function(err, filenames) {  if (err) throw err; function readFileAt(i) {    var filename = filenames[i];    fs.stat(f

  • 详谈nodejs异步编程

    目前需求中涉及到大量的异步操作,实际的页面越来越倾向于单页面应用.以后可以会使用backbone.angular.knockout等框架,但是关于异步编程的问题是首先需要面对的问题.随着node的兴起,异步编程成为一个非常热的话题.经过一段时间的学习和实践,对异步编程的一些细节进行总结. 1.异步编程的分类 解决异步问题方法大致包括:直接回调.pub/sub模式(事件模式).异步库控制库(例如async.when).promise.Generator等. 1.1 回调函数 回调函数是常用的解决异

  • JavaScript异步编程Promise模式的6个特性

    在我们开始正式介绍之前,我们想看看Javascript Promise的样子: 复制代码 代码如下: var p = new Promise(function(resolve, reject) {  resolve("hello world");}); p.then(function(str) {  alert(str);}); 1. then()返回一个Forked Promise 以下两段代码有什么区别呢? 复制代码 代码如下: // Exhibit Avar p = new Pr

  • 详谈javascript异步编程

    异步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript越来越广的被使用,大量的异步IO操作使得该问题变得明显.许多不同的方法都可以解决这个问题,本文讨论了一些方法,但并不深入.大家需要根据自己的情况选择一个适于自己的方法. 本文为大家详细介绍js中的异步编程,具体内容如下 一 关于事件的异步 事件是JavaScript中最重要的一个特征,nodejs就是利用js这一异步而设计出来的.所以这里讲一下事件机制. 在一个js文件中,如果要运行某一个函数,有2中手段

随机推荐