JavaScript异步编程设计快速响应的网络应用

JavaScript已然成为了多媒体、多任务、多内核网络世界中的一种单线程语言。其利用事件模型处理异步触发任务的行为成就了JavaScript作为开发语言的利器。如何深入理解和掌握JavaScript异步编程变得尤为重要!!!《JavaScript异步编程设计快速响应的网络应用》提供了一些方法和灵感。

一、深入理解JavaScript事件

1. 事件的调度

JavaScript事件处理器在线程空闲之前不会运行(空闲时运行)。

var start = new Date();
setTimeout(function() {
    var end = new Date();
    console.log('Time elapsed:', + (end - start), 'ms');
}, 500);
while(new Date - start < 1000) {};
// 结果:Time elapsed: 1001 ms(至少是1000)

因为setTimeout回调在while循环结束运行之前不可能被触发!
调用setTimeout时,会有一个延时事件排入队列。然后继续执行下一行代码,直到再没有任何代码(处理器空闲时),才执行setTimeout回调函数(前提已到达其延迟时间)。
JavaScript代码永远不会被中断,这是因为代码在运行期间内只需要安排队事件即可,而这些事件在代码运行结束之前不会被触发!
请参考:JavaScript事件驱动机制&定时器机制

2. 异步函数的类型

JavaScript异步函数可分为两大类:I/O函数(非阻塞)和计时函数

/* test.js */
var obj = {};
console.log(obj);
obj.foo = 'bar';

WebKit的console.log由于表现出异步行为而让很多开发者惊诧不已。在Chrome或Safari中,以下这段代码会在控制台记录{foo:bar}
WebKit的console.log并没有立即拍摄对象快照,相反,它只存储了一个指向对象的引用,然后在代码返回事件队列时才去拍摄快照。
Node的console.log是另一回事,它是严格同步的,因此同样的代码输出的却为{}
注意:在控制台记录{foo:bar},是在先执行后打开控制台!我们通过console调试代码时,要格外注意。

3. 异步函数的编写

调用一个函数(异步函数)时,程序只在该函数返回之后才能继续。这个函数会到导致将来再运行另一个函数(回调函数)。
JavaScript并没有提供一种机制以阻止函数在其异步操作结束之前返回。
有些函数既返回有用的值,又要取用回调。这种情况下,切记回调有可能被同步调用(返值之前),也有可能被异步调用(返值之后)。
永远不要定义一个潜在同步而返值却有可能用于回调的函数(回调依赖返回值)。

function test(callback) {
    var obj = {
        sendData: function() {
            console.log(arguments);
        }
    };
    callback();     // setTimeout(callback, 0); 正确写法
    return obj;
}

var obj = test(function(){
    obj.sendData("test callback");      // 返值用于了回调的函数中
});

如果一个函数既返回值又运行回调,则需确保回调在返值之后才运行!!

4. 异步错误的处理

try{
    setTimeout(function() {
        throw new Error("Catch me if you can!");
    }, 0);
} catch(e) {
    console.log(e);
}

try/catch语句只能捕获setTimeout函数自身内部发生的错误!
所以,只能在回调内部处理源于回调的异步错误。

setTimeout(function() {
    try{
        throw new Error("Catch me if you can!");
            } catch(e) {
        console.log(e);
    }
}, 0);

对于未捕获异常的处理:
(1)浏览器环境中

window.onerror = function(err) {
    return true;    // 彻底忽略所有错误
}

(2)Node环境中

process.on('uncaughtException', function(err) {
    console.log(err);   // 避免程序关闭
})

5. 嵌套式回调的解嵌套

JavaScript中最常见的反模式做法是,回调内部再嵌套回调。
请避免两层以上的函数嵌套。关键是找到一种在激活异步调用之函数的外部存储异步结果的方式,这样回调本身就没有必要再嵌套了。

二、分布式事件

事件的蝴蝶偶然扇动了下翅膀,整个应用到处都引发了反应。
这里描述的方式为发布/订阅模式,即观察者模式。曾在我的博客中介绍过:JavaScript设计模式–观察者模式

1. Node中的EventEmitter对象

ode里面的许多对象都会分发事件:一个net.Server对象会在每次有新连接时分发一个事件, 一个fs.readStream对象会在文件被打开的时候发出一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。 你可以通过require("events")来访问该模块。

// 加载EventEmitter类
var EventEmitter = require('events').EventEmitter;

var emitter = new EventEmitter();
// 监听事件
emitter.on("myCustomerEvent", function(message) {
    console.log(message);
});
// 触发事件
emitter.emit("myCustomerEvent", "ligang");

2. 实现自己的事件发布系统

function MyEvents() {
    this._events = {};
    /**
     * 事件监听
     * @param names 事件名称
     * @param callback 事件处理函数
     * @param data 注册事件时传递的参数,在callback 中用this.data 获取该值
     */
    this.on = function(names, callback,data) {
        // 支持多个事件,共享一个处理函数
        // 多个事件使用“逗号、空格、分号”间隔
        var nameList = names.split(/[\,\s\;]/);
        var index = nameList.length;
        while (index) {
            index--;
            var name = nameList[index];
            if (!this._events[name]) {
                this._events[name] = [];
            }
            this._events[name].push({callback:callback,data:data});
        }
    };
    /**
     * 事件移除
     * @param name 事件名称
     * @param callback 事件处理函数
     */
    this.off = function(name, callback) {
        // 不传入任何事件名,移除全部事件
        if (!name) {
            this._events = {};
            return;
        }
        var event = this._events[name];
        // 不存在当前事件,直接返回
        if (!event) {
            return;
        }
        // 支持同一事件,被多次绑定
        if (!callback) {
            delete this._events[name];
        } else {
            var length = event.length;
            while (length > 0) {
                length--;
                if (event[length].callback === callback) {
                    event.splice(length, 1);
                }
            }
        }
    };
    /**
     * 触发事件
     * Eg:A.B.C
     * 触发顺序:A.B.C ==> A.B ==> A
     * @param name 事件名称
     * @param args 参数
     */
    this.emit = function(name, args) {
        var handleEvent = name,
            namesAry = handleEvent.split(".");
        for(var i = 0, len = namesAry.length; i < len; i++) {
            var event = this._events[handleEvent];
            if (event) {
                var j = 0, length = event.length;
                while (j < length) {
                    event[j].callback(args);
                    j++;
                }
            }
            namesAry.pop();
            handleEvent = namesAry.join(".");
        }
    };  
}

3. 同步性

$("input[type='submit']")
    .on("click", function(){
       console.log("click");
    }).trigger("click");    // 触发事件
console.log("lalala");
// 输出结果为:click lalala

这证明了click事件的处理函数因为trigger方法而立即被激活。事实上,只要触发了jQuery事件,就会不被中断地按顺序执行其所有事件处理函数。
需要明确一点,如果用户点击submit按钮时,这确实是一个异步事件!!!

4. jQuery自定义事件

自定义事件是jQuery被低估的功能之一,它简化了强大分布式事件系统向任何Web应用程序的移植,而且无需额外的库。
补充一下:冒泡
只要某个DOM元素触发了某个事件,其父元素就会接着触发这个事件,接着是父元素的父元素,以此类推,一直追溯到根元素document;除非在这条冒泡之路的某个地方调用了事件的stopPropagation方法(如果事件处理函数返回false,则jQuery会替我们自动调用stopPropagation方法)。需要注意的是,blur、focus、mouseenter、mouseleave不支持冒泡。
示例:jQuery自定义事件同样支持冒泡

$(".pt-login-logo-signin, document").on("fizz", function(){
   console.log("fizz");
}).trigger("fizz");

有时我们不想让其冒泡,幸运的是jQuery提供了对应的方法triggerHandler():
这个特别的方法将会触发指定的事件类型上所有绑定的处理函数。但不会执行浏览器默认动作,也不会产生事件冒泡。
这个方法的行为表现与trigger类似,但有以下三个主要区别:
* 第一,他不会触发浏览器默认事件。
* 第二,只触发jQuery对象集合中第一个元素的事件处理函数。
* 第三,这个方法的返回的是事件处理函数的返回值,而不是据有可链性的jQuery对象。此外,如果最开始的jQuery对象集合为空,则这个方法返回 undefined

// 浏览器默认动作将不会被触发,只会触发你绑定的动作。即鼠标光标不能聚焦到input元素上
$("input").triggerHandler("focus");

三、Promise对象和Deferred对象

Promise
jQuery的deferred对象详解
示例:进度通知


<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>progress Demotitle>
    <script src="../../lib/jquery/dist/jquery.min.js">script>
head>
<body>
    <input type="text" id="number">
    <span id="tips">span>

    <script>
        var $input = $("#number"),
            $tips = $("#tips");
        var def = $.Deferred();
        var goalCount = 20;
        def.progress(function(currentCount){
            var percentComplete = Math.floor(currentCount / goalCount * 100);
            $tips.text(percentComplete + "% complete");
        });
        def.done(function(){
            $tips.text("good job!");
        });

        $input.on("keypress", function(){
            var count = $(this).val().split("").length;
            if(count >= goalCount) {
                def.resolve();
            }
            // notify,调用一个给定args的递延对象上的进行中的回调(progressCallbacks)
            def.notify(count);
        });
    script>
body>
html>

四、Async.js的工作流控制

1. 异步函数按顺序运行

假设我们希望某一组异步函数能依次运行。

funcs[0](function(){
    funcs[1](function(){
        funcs[2](function(){
            ...
        });
    });
});
// async.js
var async = require("async");

var start = new Date().getTime();
async.series([
    function(callback){
        setTimeout(callback, 100);
    }, function(callback){
        setTimeout(callback, 200);
    }, function(callback){
        setTimeout(callback, 300);
    }
],function(err, result){
    console.log(new Date().getTime() - start + "ms");   // 612
});
async.series([
    function(callback){
        callback(null, 'one');
    }, function(callback){
        callback(null, 'two');
    }
],function(err, result){
    console.log(result);    // ["one", "two"]
});

2. 异步函数并行运行

var async = require("async");

var start = new Date().getTime();
async.parallel([
    function(callback){
        setTimeout(callback, 100);
    }, function(callback){
        setTimeout(callback, 200);
    }, function(callback){
        setTimeout(callback, 300);
    }
],function(err, result){
    console.log(new Date().getTime() - start + "ms");   // 312
});

3. 极简主义Step的工作流控制

var fs = require("fs");
var path = require("path");
var Step = require("step"); // https://github.com/creationix/step

// 按顺序执行
Step(function readSelf() {
        fs.readFile(__filename, this);
    }, function capitalize(err, text) {
        if (err) throw err;
        return new Buffer(text).toString().toUpperCase();
    }, function showIt(err, newText) {
        if (err) throw err;
        console.log(newText);
    }
);
// 并发执行
Step(
    // Loads two files in parallel
    function loadStuff() {
        console.log(".."+__dirname)
        fs.readFile(__dirname + "/a.txt", 'UTF-8', this.parallel());
        fs.readFile(__dirname + "/b.txt", this.parallel());
    },
    // Show the result when done
    function showStuff(err, a, b) {
        if (err) throw err;
        console.log(a);
        console.log("=============");
        console.log(new Buffer(b).toString());
    }
);
// 动态
Step(
    function readDir() {
        fs.readdir(__dirname, this);
    },
    function readFiles(err, results) {
        if (err) throw err;
        // Create a new group
        var group = this.group();
        results.forEach(function (filename) {
            if (/\.js$/.test(filename)) {
                fs.readFile(__dirname + "/" + filename, 'utf8', group());
            }
        });
    },
    function showAll(err , files) {
        if (err) throw err;
        console.dir(files);
    }
);

五、worker对象的多线程技术

我们会经常看到,在JavaScript中事件是多线程技术的替代品;但是其更准确来说,事件只能代替一种特殊的多线程。
在JavaScript中我们可以利用worker单开一个单独的线程,其交互方式类似于I/O操作。
注意:同一个进程内的多个线程之间可以分享状态,而彼此独立的进程之间则不能。

1. 网页版worker对象

想要生成worker对象,只需以脚本URL为参数来调用全局Worker构造函数即可。

/* main.js */
var worker = new Worker("sub.js");
// 创建worker对象
worker.addEventListener("message", function(e){
    // 接收sub消息
    console.log(e.data);
});
//sub发送消息
worker.postMessage("football");
worker.postMessage("baseball");
/* sub.js */
/**
 * 在worker线程中,我们可以做一些耗时较大的计算,但是其计算结果要发送给主线程,由主线程去更新页面.
 * 为什么不在worker线程中直接更新页面呢?
 * 主要是为了保护JavaScript异步抽象概念,使其免受影响.
 * 如果worker对象可以改变页面,最终的下场可能就像java一样,必须将DOM操作代码封装成互斥量和信号量,避免竞争状态.
 * 基于类似情况,worker对象中也看不到全局的window对象和主线程及其他worker线程中的其他任何对象.
 * worker对象只能看到自己的全局对象self,以及self以捆绑的所有东西.
 * 包括:setTimeout,XMLHttpRequest对象等
 */
self.addEventListener("message", function(e){
    self.postMessage(e.data);
});

2. cluster带来的Node版worker

var cluster = require("cluster");

if(cluster.isMaster) {
    var coreCount = require("os").cpus().length;
    for(var i = 0; i < coreCount; i++) {
        var worker = cluster.fork();
        worker.send("Hello worker!");
        worker.on("message", function (message) {
            // Node基于worker对象发送自己的消息,命令格式为
            // {cmd: 'online', _queryId: 1, _workerId: 1}
            if(message._queryId) return;
            console.log(message);
        });
    }
}else {
    process.send("Hello, main process!");
    process.on("message", function (message) {
        console.log(message);
    })
}

注意:cluster支持并发运行同一脚本,为了尽可能减少线程间的通信开销,线程间分享的状态应该存储在像Redis这样的外部数据库中.

六、异步的脚本加载

<script src="resource.js">script>

在文档
上述加载js为同步阻塞加载(脚本下载完毕并运行之后,浏览器才会加载后续资源),为了避免一些不必要的问题,我们一般把必须立即加载的放到中,可以稍后加载的放到中。

1. 脚本的延迟运行

<script defer src="resource.js">script>

其相当于告知浏览器:“请马上开始加载这个脚本,但是,请等到文档就绪且所有此前具有defer属性的脚本都结束运行之后再运行它”
在文档标签里放入延迟脚本,既能带来脚本置于标签时的全部好处,又能让大文档的加载速度大幅提升。
提示:目前存在部分浏览器不支持defer,可以将延迟脚本中的代码封装诸如$(document).ready的结构中。

2. 脚本的异步运行

<script async src="resource.js">script>

脚本会以任意次序运行,而且只要JavaScript引擎可用就会立即运行,而不论文档就绪与否。

注意
(1)在同时支持这两个属性的浏览器中使用,async会覆盖掉defer。
(2)使用异步或延迟加载的脚本中,不能使用document.write,其会表现出不可预知的行为。

3. 动态加载脚本

var head = document.getElementsByTagName("head")[0];
var script = document.createElement("script");
script.src = "resource.js";
head.appendChild(script);
script.onload = function(){
    // 可以调用动态加载脚本中的函数了
};

注意:onload兼容性问题
所以这里还是推荐大家使用第三方库,比如:requirejs

你可能感兴趣的:(JavaScript异步编程,JavaScript异步编程)