JavaScript是如何处理事件?

#思特沃克好声音# (图片:网络)

想必大家都知道JavaScript一般都是在浏览器中执行,大家也知道可以通过事件调用JavaScript函数,可是大家清楚JavaScript是如何处理事件的吗?

西安办公室的贺亮通过一篇理解事件队列为大家答疑解惑。

理解事件队列

这篇文章的原型是来自于JavaScript Tutorial(作者:Ilya Kantor)的其中一小节Events and timing in-depth,不能算是翻译,因为我不会把一整节内容都搬过来,只写关键的事件队列部分。

浏览器中的JavaScript引擎是一种基于事件驱动的单线程模型,无论在什么时候都只且只有一个JavaScript线程在运行程序,事件可以看作是浏览器分发给JavaScript引擎的许多任务,这些任务可以是JavaScript引擎当前执行的代码块,也可以来自浏览器内核的其它线程,比如鼠标点击事件,定时器时间到达通知,异步请求状态变更通知等,JavaScript引擎一直等待着任务队列中任务的到来,由于JavaScript单线程的关系,这些任务必须得排队等着被引擎挨个收拾。

浏览器本身是允许多个线程异步执行的,除了JavaScript引擎线程以外还有GUI渲染线程(负责界面渲染)、浏览器事件触发线程、定时触发线程、HTTP请求线程、AJAX请求线程、下载线程等等,其中前三个线程属于常驻线程,说到这里不得不提一下GUI渲染线程,虽说浏览器支持线程异步执行,但是JavaScript线程和GUI渲染线程是互斥的,也就是说在JavaScript脚本操作DOM时,GUI渲染线程处于挂起状态不会有任何动作,比如添加元素、删除元素或者改变元素外观等等,界面的更新并不会立即体现出来,所有的操作都会保存在一个队列中,直到脚本运行结束后,GUI渲染线程发现脚本执行触发了界面的Reflow或者Repaint动作(关于这两个动作的区别和触发时机不在本文详细说明,有兴趣的可以自行google),此时才会接手对界面进行渲染(这也是为什么网页优化建议中js文件要放在html内容的最后,就是因为加载js的时候,会阻塞DOM树的构建),下面我们看个小栗子:

(function() {
    var htmlStr = '';
    var el = document.getElementById('main');

    for (var i = 0; i <= 100; i++) {
        var str = '<div id="test' + i + '">test' + i + '</div>';
        htmlStr += str;
    }

    var el = document.getElementById('main');
    el.innerHTML = htmlStr;

    // Timeout 2 seconds!
    sleep(2000);

    var last = document.getElementById('test100');
    last.addEventListener('click', function(e) {
        alert(this.id);
    }, false);

    function sleep(numberMillis) {
        var now = new Date();
        var exitTime = now.getTime() + numberMillis;
        while (true) {
            now = new Date();
            if (now.getTime() > exitTime)
                return;
        }
    }
})();

代码中使用了一个小手段模拟挂起函数,此时浏览器的行为并不是先显示出插入的所有节点然后再执行事件绑定,而是会有两秒钟的等待时间,然后GUI渲染线程才会讲被插入的元素进行更新和显示。

接下来是见证奇迹的时刻,如果我们把代码改成下面这个样子你猜会发生什么事情?

(function() {
    var htmlStr = '';
    var el = document.getElementById('main');

    // Wrapped by setTimeout!
    setTimeout(function() {
        for (var i = 0; i <= 100; i++) {
            var str = '<div id="test' + i + '">test' + i + '</div>';
            htmlStr += str;
        }
    }, 0);

    var el = document.getElementById('main');
    el.innerHTML = htmlStr;

    var last = document.getElementById('test100');
    last.addEventListener('click', function(e) {
        alert(this.id);
    }, false);      
})();

不管是用firebug还是web developer tool,在控制台里都会看到 “Uncaught TypeError: Cannot call method ‘addEventListener’ of null”,也就是说压根没找到last这个元素,这究竟是怎么回事?setTimeout是延迟执行某段脚本,但是如果延迟时间设置为0不是就等于没有延迟么?

这就和任务(事件)队列有关系了,前面说过JavaScript引擎会一直等待任务队列中任务的到来,而setTimeout就会使定时触发线程产生 异步定时事件 放在任务队列的最后,等队列中排在它前面的事件执行完了之后才会执行,setTimeout的执行时间点只是加入javascript主执行队列中的时间点,至于什么时候执行,是由js引擎线程按顺序执行的队列来决定,因此虽然我们设置了0毫秒延时,但是由于跳出了当前js执行线程的上下文环境,所以还是会有一个等待的时间,许多文章会说这个等待时间的极限(如果队列中没有其他事件的话)是16ms,但是现如今这个时间已经被大大缩短:

在早期,js的callback执行,是依赖CPU的中断来进行控制的,如果两个中断之间时间太短会导致,CPU性能消耗很高,同时影响能耗,于是微软和英特公司为了解决这个问题,就约定每个中断之间的间隔是15.6ms(64 fps)所以就是我们常见的约等于16ms的间隔。不过随着web的要求不断增加,大家希望放宽这个时间,于是在高端浏览器,这个性能被提升了4倍左右,所以在chrome,ie10等浏览器,setTimeout的间隔缩短到了4ms(250 fps)。

注:浏览器模型定时计数器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的,如果处于阻塞线程状态就无法计时,因此它必须依赖外部来计时并触发定时。

利用setTimeout(callbakFunction, 0)这个特性,我们可以解决很多问题,比如:

<input id='my' type="text">
<script>
document.getElementById('my').onkeypress = function(event) {
    this.value = this.value.toUpperCase();
}
</script>

这段代码实际上是无效的,因为keypress执行时浏览器还没有把输入值渲染到DOM结构中,因此也无法讲其转换为大写字母,但是如果我们使用 setTimeout(callbackFunction, 0) 就可以搞掂它:

<input id='my' type="text">
<script>
document.getElementById('my').onkeypress = function(event) {
    var self = this;
    setTimeout(function() {
        self.value = self.value.toUpperCase()
    }, 0);
}
</script>

最后,再说回GUI渲染线程和JavaScript线程互相阻塞的问题,有没有办法使二者无阻塞运行呢?答案是“有!”

随着HTML5技术的发展,在浏览器GUI线程外运行javascript代码成为了可能。WebWorker规范 提供了一个简单的方式让javascript代码在后台线程运行而不影响UI线程。每一个webworker间都是相互独立的,都在自己的线程中运行,现阶段各浏览器对规范的实现并不统一,但是我们仍然对其充满期待,因为它的多线程特性为基于Web系统开发的程序猿们提供了强大的并发程序设计功能,允许开发人员设计开发出性能和交互更好的富客户端应用程序。

原文发布于微信公众号 - 思特沃克(ThoughtWorks)

原文发表时间:2014-06-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏前端新视界

Vue.js 系列教程 3:Vue-cli,生命周期钩子

原文:intro-to-vue-3-vue-cli-lifecycle-hooks 译者:nzbin 这是 JavaScript 框架 Vue.js 五篇教...

35250
来自专栏coding for love

浏览器加载解析渲染机制的全面解析

(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

11810
来自专栏格子的个人博客

MongoDB 3.4 - 复制集、鉴权、主从同步以及读写分离

老惯例之碎碎念。 厦门的夏天又来了,热得整个人都没脾气了。 最近忙得连轴转,博客也停了很久,空闲下来还是要继续写的。

22820
来自专栏啸天"s blog

制作MAGISK字体模块

自从上了8.1后,手机换字体只能通过magisk模块进行更改,用其他方式就会翻车,无奈之下去找字体包,可是感觉有的自己喜欢的字体大多数是ttf格式就很不开心。

8.7K20
来自专栏CRPER折腾记

Angular 2 + 折腾记 :(8) 动手写一个不怎么靠谱的上传组件

上传功能在任何一个网站中的地位都是举足轻重的,这篇文章主要扯下如何实现一个上传组件

16910
来自专栏超然的博客

mpvue-小程序之蹲坑记

mpvue 是兼容微信小程序的生命周期与 vue 的生命周期,vue 实例会接管小程序 Page 实例的生命钩子,因此需要使用到小程序的生命周期钩子时,可将相应...

70220
来自专栏進无尽的文章

扒虫篇-Bug日志Ⅴ

解决方法:首先这个警告不会造成上传失败,也不会造成审核被拒。其次可以通过移除代码中警告的那些代码,并移除多余不使用的系统类库(framework),使警告消失。

14210
来自专栏企鹅号快讯

使用纯粹的JS构建 Web Component

问题:我怎么才能收到你们公众号平台的推送文章呢? Web Component 出现有一阵子了。 Google 费了很大力气去推动它更广泛的应用,但是除 Oper...

25660
来自专栏繁花云

liunx下利用某软件创建图形伪界面

Liunx下的dialog是一个可以创建对话框的工具,每个对话框提供的输出有两种形式:1、将所有输出到stderr,不显示到屏幕;2、使用退出状态码,OK为0...

10900
来自专栏编程直播室

Angular 2的基本构建块

22530

扫码关注云+社区

领取腾讯云代金券