惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V2EX - 技术
V2EX - 技术
P
Privacy International News Feed
Security Latest
Security Latest
H
Hacker News: Front Page
T
Tenable Blog
The Hacker News
The Hacker News
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Security @ Cisco Blogs
Project Zero
Project Zero
O
OpenAI News
AI
AI
Spread Privacy
Spread Privacy
C
CERT Recently Published Vulnerability Notes
The Last Watchdog
The Last Watchdog
G
GRAHAM CLULEY
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Scott Helme
Scott Helme
Application and Cybersecurity Blog
Application and Cybersecurity Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
NISL@THU
NISL@THU
A
Arctic Wolf
T
Threat Research - Cisco Blogs
PCI Perspectives
PCI Perspectives
N
News and Events Feed by Topic
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cybersecurity and Infrastructure Security Agency CISA
Simon Willison's Weblog
Simon Willison's Weblog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
Google Online Security Blog
Google Online Security Blog
罗磊的独立博客
L
LINUX DO - 最新话题
U
Unit 42
S
Security Affairs
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
博客园 - 【当耐特】
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
月光博客
月光博客
Engineering at Meta
Engineering at Meta
腾讯CDC
F
Full Disclosure
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Recorded Future
Recorded Future
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 司徒正美
The Cloudflare Blog

博客园 - bigwhiteshark(云飞扬)

linux下svn定时更新项目 PHP合成图片、生成文字、居中对齐、画线、矩形、三角形、多边形、图片抗锯齿、不失真 高性能源码示例 Html5 Canvas transform setTransform uv纹理坐标设定与贴图规则 容易答错的JS笔试题 JavaScript严谨模式(Strict Mode) property和attribute的区别 深入理解javascript 中的 delete(转) javascript中的call()和apply()方法的使用 Javascript 异步加载详解(转) array2json() - Convert PHP arrays to JSON 一些Vim使用的小技巧 virtualbox centos安装增强工具和Centos与VirtualBox共享文件夹设置 yum搭建lnmp环境(CentOS6.4) CentOS中文man安装配置 在Linux里设置环境变量的方法(export PATH) 更新CentOS防火墙设置开启80端口访问 centos 6.4 server 安装nginx Centos安装vim
Javascript框架的自定义事件(转)
bigwhiteshark(云飞扬) · 2014-05-17 · via 博客园 - bigwhiteshark(云飞扬)

很多 javascript 框架都提供了自定义事件(custom events),例如 jquery、yui 以及 dojo 都支持“document ready”事件。而部分自定义事件是源自回调(callback)。

回调将多个事件句柄存储在数组中,当满足触发条件时,回调系统则会从数组中获取对应的句柄并执行。那么,这会有什么陷阱呢?在回答这个问题之前,我们先看下代码。

下面是两段代码依次绑定到 domcontentloaded 事件中

document.addeventlistener("domcontentloaded", function() {
  console.log("init: 1");
  does_not_exist++; // 这里会抛出异常
}, false);

document.addeventlistener("domcontentloaded", function() {
  console.log("init: 2");
}, false);

那么运行这段代码会返回什么信息?显然,会看见这些(或者类似的):

init: 1
error: does_not_exist is not defined
init: 2

可以看出,两段函数都被执行。即使第一个函数抛出了个异常,但并不影响第二段代码运行。

麻烦

ok,我们回来看下常见框架中的回调系统。首先,我们看下 jquery 的(因为它很流行):

$(document).ready(function() {
  console.log("init: 1");
  does_not_exist++; // 这里会抛出异常
});

$(document).ready(function() {
  console.log("init: 2");
});

然后控制台中输出了什么?

init: 1
error: does_not_exist is not defined

这样问题就很明了了。回调系统其实很脆弱 -- 如果中间有段代码抛出了异常,那么其余将不会被执行。想象下在实际情况中,这后果可能会更严重,譬如有些糟糕的插件可能会“一粒老屎坏了一锅粥”。

其他的框架,dojo 的情况和 jquery 类似,不过 yui 的情况有些许不同。在它的回调系统中,使用了 try/catch 语句避免因异常发生的中断。但有个小小的负面影响,就是看不到相应的异常了。

yahoo.util.event.ondomready(function() {
  console.log("init: 1");
  does_not_exist++; // 这里会抛出异常
});

yahoo.util.event.ondomready(function() {
  console.log("init: 2");
});

输出:

init: 1
init: 2

那么,有无完美的解决方案呢?

解决方案

我想到了个解决方案,就是将回调和事件结合起来。可以先建立个事件,当回调触发时才运行它。由于每个事件都有其独立的运行环境(execution context),那么即使其中某个事件抛出了异常将不会影响其他的回调。

这听起来有点复杂,还是代码说话吧。

var currenthandler;

// 标准事件支持
if (document.addeventlistener) {
    document.addeventlistener("fakeevents", function() {
        // 执行回调
        currenthandler();
    }, false);

    // 新建事件
    var dispatchfakeevent = function() {
        var fakeevent = document.createevent("uievents");
        fakeevent.initevent("fakeevents", false, false);
        document.dispatchevent(fakeevent);
    };
} else {
    // 针对 ie 的代码在后面详细阐述
}

var onloadhandlers = [];

// 将回调加入数组中
function addonload(handler) {
    onloadhandlers.push(handler);
};

// 逐条取出回调,并利用上述新建的事件执行
onload = function() {
    for (var i = 0; i < onloadhandlers.length; i++) {
        currenthandler = onloadhandlers[i];
        dispatchfakeevent();
    }
};

万事俱备,让我们将上面坨代码扔到我们新的回调系统中

addonload(function() {
  console.log("init: 1");
  does_not_exist++; // 这里会抛出异常
});

addonload(function() {
  console.log("init: 2");
});

上帝保佑,看运行结果我们看到了如下的信息:

init: 1
error: does_not_exist is not defined
init: 2

赞!这就是我们期望的。这两个回调都运行而且互不影响,并且还能获得异常的信息,太好了!

好了,我们回过头来扶起 internet explorer 这个“阿斗”(我已经听见场下观众的建议了)。internet explorer 不支持 w3c 的标准事件规范,谢天谢地好在它有自身的实现 -- 有个 fireevents 的方法,但只能在用户事件的时候触发(例如用户点击 click)。

不过终于找到了门道,我们来看下具体代码:

var currenthandler;

if (document.addeventlistener) {
    // 省略上述的代码
} else if (document.attachevent) { // msie
    // 利用扩展属性,当此对象被改变时触发
    document.documentelement.fakeevents = 0;
    document.documentelement.attachevent("onpropertychange", function(event) {
        if (event.propertyname == "fakeevents") {
            // 执行回调
            currenthandler();
        }
    });

    dispatchfakeevent = function(handler) {
        // 触发 propertychange 事件
        document.documentelement.fakeevents++;
    };
}

简而言之,殊途同归,只是针对 internet explorer 使用了 propertychange 事件作为触发器。

更新

有些用户留言建议使用 settimeout:

try { callback(); } catch(e){ settimeout(function(){ throw e; }, 0); }

而下面是我的考虑

如没特别的要求,其实定时器的确也能搞定这问题。
上面仅仅是举例说明了这一技术的可行性。

意义在于,目前很多框架在回调系统的实现都非常的
脆弱,这或许能给这些框架能它们提供更优化的思路。
而定时器的实现并非实际的触发了事件,在实际事件
中,事件会被顺序的执行、可相互影响(譬如冒泡)、
还可以停止 -- 而这些是定时器无法做到的。

总之,最重要的是已经实现了包括 internet explorer 在内,使用事件执行回调的实现。如果你正编写基于事件代理的回调系统,我想你会对这一技术感兴趣的。

更新2

prototype 在针对 internet explorer 的自定义事件处理上,也是同上述的方法触发回调:

http://andrewdupont.net/2009/03/24/link-dean-edwards/

译注,prototype 1.6 对应的代码,摘记如下:

function createwrapper(element, eventname, handler) {
    var id = geteventid(element); // 获取绑定事件的 id
    var c = getwrappersforeventname(id, eventname); // 获取对应的事件的所有回调
    if (c.pluck("handler").include(handler)) return false; // 避免重复绑定

    // 新建回调
    var wrapper = function(event) {
        if (!event || !event.extend ||
                (event.eventname && event.eventname != eventname))
            return false;

        event.extend(event);
        handler.call(element, event);
    };

    // 加入到回调数组
    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
}

function observe(element, eventname, handler) {
    element = $(element);                  // 对应事件的元素
    var name = getdomeventname(eventname); // 事件执行方式

    var wrapper = createwrapper(element, eventname, handler); // 封装回调

    if (!wrapper) return element;

    // 绑定事件
    if (element.addeventlistener) {
        element.addeventlistener(name, wrapper, false);
    } else {
        element.attachevent("on" + name, wrapper);
    }

    return element;
}

// 调用方式
document.observe("dom:loaded", function() {
    console.log("init: 1");
    does_not_exist++;
});

document.observe("dom:loaded", function() {
    console.log("init: 2");
});

看把 prototype 的作者给乐的 :-/

-- split --

在本人看来,原文的作者表述的技术点,除了如何创建健壮的回调系统外,其实还有两条。

其一,就是如何保证在出现异常的时,继续运行期望的代码;其二,就是如何创建互不干扰的“运行环境”。

原文提到的 createevent 和 settimeout 都是好办法,只是处理原作者所言在回调系统中,的确使用 createevent 会比较合适。settimeout 相对应的详细信息,可移步到 realazy 兄的相关文章。

而即使出错也能继续运行期望的代码,其实可以考虑使用 finally 语句,下面是个例子:

var callbacks = [
  function() { console.log(0); },
  function() { console.log(1); throw new error; },
  function() { console.log(2); },
  function() { console.log(3); }
];

for(var i = 0, len = callbacks.length; i < len; i++) {
    try {
        callbacks[i]();
    } catch(e) {
        console.info(e); // 获得异常信息
    } finally {
        continue;
    }
}

这一灵感同样来自 dean edwards 文章后的回复,在这里也贴下吧:

function iterate(callbacks, length, i) {
    if (i >= length) return;

    try {
        callbacks[i]();
    } catch(e) {
        throw e;
    } finally {
        iterate(callbacks, length, i+1);
    }
}

最后,留个小问题。谁知道上述的代码中,留言者提出的为什么异常到最后才打印出来不?