JavaScript 浏览器事宜剖析

原文:https://keelii.github.io/2016/09/29/javascript-browser-event/

JavaScript、浏览器、事宜之间的关联

JavaScript 递次采用了异步事宜驱动编程(Event-driven programming)模子,维基百科对它的诠释是:

事宜驱动递次设想(英语:Event-driven programming)是一种电脑递次设想模子。这类模子的递次运转流程是由用户的行动(如鼠标的按键,键盘的按键行动)或者是由其他递次的音讯来决议的。相对于批处置惩罚递次设想(batch programming)而言,递次运转的流程是由递次员来决议。批量的递次设想在低级递次设想教授教养课程上是一种体式格局。然则,事宜驱动递次设想这类设想模子是在交互递次(Interactive program)的状况下孕育而生的

简页言之,在 web 前端编程内里 JavaScript 经由过程浏览器供应的事宜模子 API 和用户交互,吸收用户的输入

由于用户的行动是不确定的,也就是说不晓得用户什么时刻发作点击、转动这些行动。这类场景是传统的同步编程模子没法处置惩罚的,由于你不能够等用户操纵完了才实行背面的代码

比方我们在 Python 内里挪用吸收用户输入的要领 raw_input() 后终端就会一向守候用户的输入,直到输入完成才会实行背面的代码逻辑。然则在下面这段 NodeJS 代码中,吸收用户输入的要领 process.stdin.read 是在一个事宜中挪用的。背面的代码不会被壅塞(blocked)

'use strict';

process.stdin.on('readable', () => {
    var chunk = process.stdin.read();
    if (chunk !== null) {
        process.stdout.write(`Async output data: ${chunk}`);
    }
});

process.stdin.on('end', () => {
    process.stdout.write('end');
});

console.log('Will not be blocked');

事宜驱动递次模子基础的完成道理基础上都是运用 事宜轮回(Event Loop),这部分内容触及浏览器事宜模子、回调道理,有兴致的去看链接内里的视频进修下

须要申明的是在客户端 JavaScript 中像 setTimeout, XMLHTTPRequest 这类 API 并非 JavaScript 言语自身就有的。而是 JavaScript 的宿主环境(在客户端 JavaScript 中就是浏览器),一样像 DOM、BOM、Event API 都是浏览器供应的

事宜绑定的要领

DOM 元素行内绑定

直接在 DOM 元素上经由过程设置 on + eventType 来绑定事宜处置惩罚递次

<a href="#none" onclick="alert('clicked.')">点击我</a>

这类绑定要领是最原始的,有两个瑕玷:

1 事宜处置惩罚递次和 HTML 构造混淆在一同

初期在构造、款式、表现星散的时期被视为最好实践,如今看来在许多 MVX 框架中将事宜绑定和 DOM 构造放在一同处置惩罚,如许好像更轻易保护(不必往返切换 HTML,JavaScript 文件),而且也相符可预感(predictable)性的划定规矩

2 定名空间争执

由于 onclick 中的 JavaScript 代码片断实行环境是全局作用域。然则在 JavaScript 言语中并没有相干的定名空间特征。所以就很轻易形成定名空间的争执,非要用这类要领绑定事宜的话只能用对象来做一些封装

陈旧的绑定要领

运用 DOM Element 上面的 on + eventType 属性 API

<a href="#none" id="button">click me</a>
<script>
    var el = getElementById('button');
    el.onclick = function() { alert('button clicked.') };
    el.onclick = function() { alert('button clicked (Rewrite event handler before).') };
</script>

这类要领也有一个瑕玷,由于属性赋值会掩盖原值的。所以没法绑定 多个 事宜处置惩罚函数,如果我们要注册多个 onload 事宜处置惩罚递次的话就得本身封装一个要领来防备这类事变发作,下面这个例子能够处置惩罚这个题目

<script>
function addLoadEvent(fn) {
    var oldonLoad = window.onload;
    if (typeof oldonLoad !== 'function') {
        window.onload = fn;
    } else {
        window.onload = function() {
            oldonLoad();
            fn();
        }
    }
}

addLoadEvent(function() { alert('onload 1') });
addLoadEvent(function() { alert('onload 2') });
</script>

注重这只是个示例,临盆环境很少会用到。平常用 DOM Ready 就能够了,由于 JavaScript 的实行一般不必比及页面资本悉数加载完,DOM 加载完就能够了

当代/规范的绑定要领

规范的绑定要领有两种,addEventListenerattachEvent 前者是规范浏览器支撑的 API,后者是 IE 8 以下浏览器支撑的 API。一般须要我们做个兼容封装

<script>
    function addEvent(target, type, handler) {
        if (target.addEventListener) {
            target.addEventListener(type, handler, false);
        } else {
            target.attachEvent('on' + type, handler)
        }
    }

    addEvent(document, 'click', function() { alert(this === document) });
    addEvent(document, 'click', function() { alert(this === document) });
</script>

上面的例子在 IE 8 以下和规范浏览器的效果是不一样的,题目就在于 addEventListener 中的事宜回调函数中的 this 指向元素(target)自身,而 attachEvent 则指向 window 为了修复这个题目上面的 attachEvent 能够做一点小调解让其坚持和 addEventListener 的效果一样,不过如许的话注册的 handler 就是个匿名函数,没法再移除!

<script>
    function addEvent(target, type, handler) {
        if (target.addEventListener) {
            target.addEventListener(type, handler, false);
        } else {
            target.attachEvent('on' + type, function() {
                return handler.call(target)
            });
        }
    }

    addEvent(document, 'click', function() { alert(this === document) });
</script>

当上面这几种状况同时涌现的时刻就比较有意思了,能够尝尝下面这段代码的你输出

<a href="javascript:alert(1)" onclick="alert(2)" id="link">click me</a>
<script>
    var link = document.getElementById('link');
    link.onclick = function() { alert(3); }

    $('#link').bind('click', function() { alert(4); });
    $('#link').bind('click', function() { alert(5); });
</script>

准确的效果应该是 3,4,5,1,依据效果我们能够得出以下结论:

  • 链接上的 href 伪 javascript 协定相当于在浏览器地址栏实行了一段 JavaScript 代码,链接如果是这类花样,点击的时刻相当于实行了这段 JavaScript 剧本

  • 行内的事宜绑定和元素挪用 onclick 绑定事宜会掩盖

  • 运用 jQuery(内部运用规范事宜注册 API)能够绑定多个事宜处置惩罚递次

事宜冒泡

大部分事宜会沿着事宜触发的目的元素往上流传。比方:body>div>p>span 如果他们都注册了点击事宜,那末在 span 元素上触发点击事宜后 p,div,body 各自的点击事宜也会按递次触发

事宜冒泡是能够被住手的,下面这个函数封闭了住手事宜冒泡的要领:

<script>
    function stopPropagation(event) {
        event = event || window.event;
        if (event.stopPropagation) {
            event.stopPropagation()
        } else {// IE
            event.cancelBubble = true
        }
    }

    addEvent('ele', 'click', function(e) {
        // click handler
        stopPropagation(e);
    });
</script>

事宜对象

规范浏览器中在事宜处置惩罚递次被挪用时 事宜对象 会经由过程参数传递给处置惩罚递次,IE 8 及以下浏览器中事宜对象能够经由过程全局的 window.event 来访问。比方我们要猎取当前点击的 DOM Element

<script>
    addEvent(document, 'click', function(event) {
        // IE 8 以下 => undefined
        console.log(event);
    });
    addEvent(document, 'click', function(event) {
        event = event || window.event;
        // 规范浏览器 => [object HTMLHtmlElement]
        // IE 8 以下 => undefined
        console.log(event.target);
        var target = event.target || event.srcElement;

        console.log(target.tagName);
    });
</script>

事宜代办

有时刻我们须要给 不存在的(能够未来会有)的一段 DOM 元素绑定事宜,比方给一段 Ajax 要求完成后衬着的 DOM 节点绑定事宜。平常绑定的逻辑会在衬着前实行,绑定的时刻找不到元素所以并不能胜利,固然你也能够把绑定事宜的代码放在 Ajax 要求以后。如许做在一些事宜逻辑简朴的运用内里没题目,然则会加重数据衬着逻辑和事宜处置惩罚的逻辑耦合。一但事宜处置惩罚递次迥殊多的时刻,我们一般发起把事宜的逻辑和别的代码逻辑星散,如许轻易保护。

为了处置惩罚这个题目,我们一般运用事宜代办/托付(Event Delegation )。而且一般来讲运用 事宜代办的机能会比零丁绑定事宜高 许多,我们来看个例子

<ul id="list">
    <li id="item-1">item1</li>
    <li id="item-2">item2</li>
    <li id="item-3">item3</li>
    <li id="item-4">item4</li>
    <li id="item-5">item5</li>
</ul>

如果 ul 中的 HTML 是 Ajax 异步插进去的,一般我们的做法是 插进去完成后遍历每一个 li 绑定事宜处置惩罚递次

<ul id="list"></ul>
<script>
    function bindEvent(el, n) {
        addEvent(lis[i], 'click', function() { console.log(i); });
    }
    // 用 setTimeout 模仿 Ajax 伪代码
    setTimeout(function() {
        var ajaxData = '<li id="item-1">item1</li> <li id="item-2">item2</li> <li id="item-3">item3</li> <li id="item-4">item4</li> <li id="item-5">item5</li>';
        var ul = document.getElementById('list')
        ul.innerHTML(ajaxData);
        var lis = ul.getElementsByTagName('li');

        for (var i = 0; i < lis.length; i++) {
            bindEvent(lis[i], i);
        }
    }, 1000);
</script>

我们再运用事宜代办把事宜绑定到 ul 元素上,我们晓得许多事宜能够冒并沿着 DOM 树流传到一切的父元素上,我们只须要推断目的元素是否是我们想绑定的真正元素即可

<ul id="list"></ul>
<script>
function delegateEvent(el, eventType, fn) {
    addEvent(el, eventType, function(event) {
        event = event || window.event;
        var target = event.target || event.srcElement;
        fn(target);
    });
}

var el = document.getElementById('list');
// 用 setTimeout 模仿 Ajax 伪代码
setTimeout(function() {
    var ajaxData = '<li id="item-1">item1</li> <li id="item-2">item2</li> <li id="item-3">item3</li> <li id="item-4">item4</li> <li id="item-5">item5</li>';
    el.innerHTML(ajaxData)
}, 1000);

delegateEvent(el, 'click', function(target) {
    console.log(target.id);
});
</script>

明显运用了事宜代办以后,代码变少了。逻辑也很清楚,关键是之前须要 N 次的绑定操纵如今只须要一次

jQuery 中的事宜绑定

以 jQuery1.6.4 为例,jQuery 供应了许多事宜绑定的 API。比方: delegate(), bind(), click(), hover(), one(), live(),这些要领实在都是一些别号,中心是挪用了 jQuery 底层事宜的 jQuery.event.add 要领。其完成也是上文提到的 addEventListenerattachEvent 两个 API

这些 API 重要是为了轻易绑定事宜的种种场景,而且内部处置惩罚好了兼容性题目。另有一个比较好用的处所就是 事宜定名空间。比方:两个弹出层都向 document 绑定了点击封闭事宜,然则如果只想解绑个中一个。这时刻运用定名空间再适宜不过了。能够尝尝这个小例子 Event Binding

<script>
    $(document).bind('click.handler1', function() { console.log(1);})
    $(document).bind('click.handler2', function() { console.log(2);})

    $(document).unbind('click.handler2');   // 消除指定的
    $(document).unbind('click');            // 消除一切点击事宜
    $(document).unbind()                    // 消除一切事宜
</script>

自定义事宜与宣布/定阅者设想形式

自定义事宜是设想形式中的 宣布/定阅者 的一种完成。宣布者与定阅者松散地耦合,而且不须要体贴对方的存在。这里有 NC 巨匠的一种完成。实际运用过程当中,重要被运用在异步操纵比较多的场景和差别体系之间音讯通讯,之前的文章中有过一些实例

援用

    原文作者:keelii
    原文地址: https://segmentfault.com/a/1190000007056189
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞