首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript异步编程设计快速响应的网络应用

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

作者头像
奋飛
发布2019-08-15 09:52:28
2K0
发布2019-08-15 09:52:28
举报
文章被收录于专栏:Super 前端Super 前端

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对象详解 示例:进度通知

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>progress Demo</title>
    <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>

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

1. 脚本的延迟运行
<script defer src="resource.js"></script>

其相当于告知浏览器:“请马上开始加载这个脚本,但是,请等到文档就绪且所有此前具有defer属性的脚本都结束运行之后再运行它” 在文档<head>标签里放入延迟脚本,既能带来脚本置于<body>标签时的全部好处,又能让大文档的加载速度大幅提升。 提示:目前存在部分浏览器不支持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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016年07月10日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、深入理解JavaScript事件
    • 1. 事件的调度
      • 2. 异步函数的类型
        • 3. 异步函数的编写
          • 4. 异步错误的处理
            • 5. 嵌套式回调的解嵌套
            • 二、分布式事件
              • 1. Node中的EventEmitter对象
                • 2. 实现自己的事件发布系统
                  • 3. 同步性
                    • 4. jQuery自定义事件
                    • 三、Promise对象和Deferred对象
                    • 四、Async.js的工作流控制
                      • 1. 异步函数按顺序运行
                        • 2. 异步函数并行运行
                          • 3. 极简主义Step的工作流控制
                          • 五、worker对象的多线程技术
                            • 1. 网页版worker对象
                              • 2. cluster带来的Node版worker
                              • 六、异步的脚本加载
                                • 1. 脚本的延迟运行
                                  • 2. 脚本的异步运行
                                    • 3. 动态加载脚本
                                    相关产品与服务
                                    云数据库 Redis
                                    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档