首頁  >  文章  >  web前端  >  最詳細的JavaScript與事件解讀

最詳細的JavaScript與事件解讀

黄舟
黄舟原創
2017-02-24 13:55:261375瀏覽

與瀏覽器互動的時候瀏覽器就會觸發各種事件。例如當我們打開某一個網頁的時候,瀏覽器載入完成了這個網頁,就會觸發一個 load 事件;當我們點擊頁面中的某一個“地方”,瀏覽器就會在那個“地方”觸發一個 click 事件。

這樣,我們就可以寫 JavaScript,透過監聽某一個事件,來實現某些功能擴充。例如監聽 load 事件,顯示歡迎訊息,那麼當瀏覽器載入完一個網頁之後,就會顯示歡迎訊息。

下面就來介紹一下事件。

基礎事件操作

監聽事件

瀏覽器會根據某些操作觸發對應事件,如果我們需要針對某種事件進行處理,則需要監聽這個事件。監聽事件的方法主要有以下幾種:

HTML 內嵌屬性(避免使用)

HTML 元素裡面直接填寫事件有關屬性,屬性值為JavaScript 程式碼,即可在觸發該事件的時候,執行屬性值的內容。

例如:

<button onclick="alert(&#39;你点击了这个按钮&#39;);">点击这个按钮</button>

onclick 屬性表示觸發 click,屬性值的內容(JavaScript 程式碼)會在按一下該 HTML 節點時執行。

顯而易見,使用這種方法,JavaScript 程式碼與 HTML 程式碼耦合在了一起,不便於維護和開發。所以除非在必須使用的情況(例如統計連結點擊資料)下,盡量避免使用這種方法。

DOM 屬性綁定

也可以直接設定DOM 屬性來指定某個事件對應的處理函數,這個方法比較簡單:

element.onclick = function(event){
    alert(&#39;你点击了这个按钮&#39;);
};

上面程式碼就是監聽element 節點的click 事件。它比較簡單易懂,而且有較好的相容性。但也有缺陷,因為直接賦值給對應屬性,如果你在後面程式碼中再次為 element 綁定一個回呼函數,會覆寫之前回呼函數的內容。

雖然也可以用一些方法實現多個綁定,但還是推薦下面的標準事件監聽函數。

使用事件監聽函式

標準的事件監聽函式如下:

element.addEventListener(<event-name>, <callback>, <use-capture>);

表示在element 這個物件上面加上一個事件監聽器,當監聽到有 事件發生的時候,呼叫 這個回呼函數。至於 這個參數,表示該事件監聽是在「捕獲」階段中監聽(設定為 true)還是在「冒泡」階段監聽(設定為 false)。關於捕獲和冒泡,我們會在下面講解。

用標準事件監聽函數改寫上面的範例:

var btn = document.getElementsByTagName(&#39;button&#39;);
btn[0].addEventListener(&#39;click&#39;, function() {
    alert(&#39;你点击了这个按钮&#39;);
}, false);

這裡最好是為 HTML 結構定義個 id 或 class 屬性,方便選擇,這裡只作為示範使用。

Demo:

移除事件監聽

當我們為某個元素綁定了一個事件,每次觸發這個事件的時候,都會執行事件綁定的回呼函數。如果我們想要解除綁定,需要使用removeEventListener 方法:

element.removeEventListener(<event-name>, <callback>, <use-capture>);

要注意的是,綁定事件時的回呼函數不能是匿名函數,必須是一個宣告的函數,因為解除事件綁定時需要傳遞這個回呼函數的引用,才可以斷開綁定。例如:

var fun = function() {
    // function logic
};

element.addEventListener(&#39;click&#39;, fun, false);
element.removeEventListener(&#39;click&#39;, fun, false);

Demo:

事件觸發過程

在上面大體了解了事件是什麼、如何監聽並執行某些操作,但我們對事件觸發整個過程還不夠了解。

下圖就是事件的觸發過程,借用了W3C 的圖片

#擷取階段(Capture Phase)

當我們在DOM樹的某個節點發生了一些操作(例如點擊、滑鼠移動上去),就會有一個事件發射過去。這個事件從 Window 發出,不斷經過下級節點直到目標節點。在到達目標節點之前的過程,就是捕捉階段(Capture Phase)。

所有經過的節點,都會觸發這個事件。捕獲階段的任務就是建立這個事件傳遞路線,以便後面冒泡階段沿著這條路線回到 Window。

監聽某個在擷取階段觸發的事件,需要在事件監聽函數傳遞第三個參數 true。

element.addEventListener(<event-name>, <callback>, true);

但一般使用時我們往往傳遞 false,會在後面說明原因。

目標階段(Target Phase)

當事件跑啊跑,跑到了事件觸發目標節點那裡,最終在目標節點上觸發這個事件,就是目標階段。

需要注意的時,事件觸發的目標總是最底層的節點。例如你點擊一段文字,你以為你的事件目標節點在 p 上,但實際上觸發在

等子節點上。例如:

在 Demo 中,我監聽點擊事件,並將目標節點的 tag name 彈出。當你點擊加粗字體時,事件的目標節點就為最底層的 節點。

冒泡阶段(Bubbling Phase)

当事件达到目标节点之后,就会沿着原路返回,由于这个过程类似水泡从底部浮到顶部,所以称作冒泡阶段。

在实际使用中,你并不需要把事件监听函数准确绑定到最底层的节点也可以正常工作。比如在上例,你想为这个

绑定单击时的回调函数,你无须为这个

下面的所有子节点全部绑定单击事件,只需要为

这一个节点绑定即可。因为发生它子节点的单击事件,都会冒泡上去,发生在

上面。

针对这三个阶段,wilsonpage 做了一个非常棒的 Demo,可以看下:

为什么不用第三个参数 true

介绍完上面三个事件触发阶段,我们来看下这个问题。

所有介绍事件的文章都会说,在使用 addEventListener 函数来监听事件时,第三个参数设置为 false,这样监听事件时只会监听冒泡阶段发生的事件。

这是因为 IE 浏览器不支持在捕获阶段监听事件,为了统一而设置的,毕竟 IE 浏览器的份额是不可忽略的。

IE 浏览器在事件这方面与标准还有一些其他的差异,我们会在后面集中介绍。

使用事件代理(Event Delegate)提升性能

因为事件有冒泡机制,所有子节点的事件都会顺着父级节点跑回去,所以我们可以通过监听父级节点来实现监听子节点的功能,这就是事件代理。

使用事件代理主要有两个优势:

  1. 减少事件绑定,提升性能。之前你需要绑定一堆子节点,而现在你只需要绑定一个父节点即可。减少了绑定事件监听函数的数量。

  2. 动态变化的 DOM 结构,仍然可以监听。当一个 DOM 动态创建之后,不会带有任何事件监听,除非你重新执行事件监听函数,而使用事件监听无须担忧这个问题。

看一个例子:

上面例子中,为了简便,我使用 jQuery 来实现普通事件绑定和事件代理。我的目标是监听所有 a 链接的单击事件,.ul1 是常规的事件绑定方法,jQuery 会循环每一个 .ul > a 结构并绑定事件监听函数。.ul2 则是事件监听的方法,jQuery 只为 .ul2 结构绑定事件监听函数,因为 .ul2 下面可能会有很多无关节点也会触发 click 事件,所以我在 on 函数里传递了第二个参数,表示只监听 a 子节点的事件。

它们都可以正常工作,但是当我动态创建新 DOM 结构的时候,第一个 ul 问题就出现了,新创建结构虽然还是 .ul1 > a,但是没有绑定事件,所以无法执行回调函数。而第二个 ul 工作的很好,因为点击新创建的 DOM ,它的事件会冒泡到父级节点进行处理。

如果使用原生的方式实现事件代理,需要注意过滤非目标节点,可以通过 id、class 或者 tagname 等等,例如:

element.addEventListener(&#39;click&#39;, function(event) {
    // 判断是否是 a 节点
    if ( event.target.tagName == &#39;A&#39; ) {
        // a 的一些交互操作
    }
}, false);

停止事件冒泡(stopPropagation)

所有的事情都会有对立面,事件的冒泡阶段虽然看起来很好,也会有不适合的场所。比较复杂的应用,由于事件监听比较复杂,可能会希望只监听发生在具体节点的事件。这个时候就需要停止事件冒泡。

停止事件冒泡需要使用事件对象的 stopPropagation 方法,具体代码如下:

element.addEventListener(&#39;click&#39;, function(event) {
    event.stopPropagation();
}, false);

在事件监听的回调函数里,会传递一个参数,这就是 Event 对象,在这个对象上调用 stopPropagation 方法即可停止事件冒泡。举个停止事件冒泡的应用实例:

JS Bin

在上面例子中,有一个弹出层,我们可以在弹出层上做任何操作,例如 click 等。当我们想关掉这个弹出层,在弹出层外面的任意结构中点击即可关掉。它首先对 document 节点进行 click 事件监听,所有的 click 事件,都会让弹出层隐藏掉。同样的,我们在弹出层上面的单击操作也会导致弹出层隐藏。之后我们对弹出层使用停止事件冒泡,掐断了单击事件返回 document 的冒泡路线,这样在弹出层的操作就不会被 document 的事件处理函数监听到。

更多关于 Event 对象的事情,我们会在下面介绍。

事件的 Event 对象

当一个事件被触发的时候,会创建一个事件对象(Event Object),这个对象里面包含了一些有用的属性或者方法。事件对象会作为第一个参数,传递给我们的毁掉函数。我们可以使用下面代码,在浏览器中打印出这个事件对象:

<button>打印 Event Object</button>

<script>
    var btn = document.getElementsByTagName(&#39;button&#39;);
    btn[0].addEventListener(&#39;click&#39;, function(event) {
        console.log(event);
    }, false);
</script>

就可以看到一堆属性列表:

最詳細的JavaScript與事件解讀

事件对象包括很多有用的信息,比如事件触发时,鼠标在屏幕上的坐标、被触发的 DOM 详细信息、以及上图最下面继承过来的停止冒泡方法(stopPropagation)。下面介绍一下比较常用的几个属性和方法:

type(string)

事件的名称,比如 “click”。

target(node)

事件要触发的目标节点。

bubbles (boolean)

表明该事件是否是在冒泡阶段触发的。

preventDefault (function)

这个方法可以禁止一切默认的行为,例如点击 a 标签时,会打开一个新页面,如果为 a 标签监听事件 click 同时调用该方法,则不会打开新页面。

stopPropagation (function)

停止冒泡,上面有提到,不再赘述。

stopImmediatePropagation (function)

与 stopPropagation 类似,就是阻止触发其他监听函数。但是与 stopPropagation 不同的是,它更加 “强力”,阻止除了目标之外的事件触发,甚至阻止针对同一个目标节点的相同事件,Demo:http://www.php.cn/。

cancelable (boolean)

这个属性表明该事件是否可以通过调用 event.preventDefault 方法来禁用默认行为。

eventPhase (number)

这个属性的数字表示当前事件触发在什么阶段。none:0;捕获:1;目标:2;冒泡:3。

pageX 和 pageY (number)

这两个属性表示触发事件时,鼠标相对于页面的坐标。Demo:http://www.php.cn/。

isTrusted (boolean)

表明该事件是浏览器触发(用户真实操作触发),还是 JavaScript 代码触发的。

jQuery 中的事件

如果你在写文章或者 Demo,为了简单,你当然可以用上面的事件监听函数,以及那些事件对象提供的方法等。但在实际中,有一些方法和属性是有兼容性问题的,所以我们会使用 jQuery 来消除兼容性问题。

下面简单的来说一下 jQuery 中事件的基础操作。

绑定事件和事件代理

在 jQuery 中,提供了诸如 click() 这样的语法糖来绑定对应事件,但是这里推荐统一使用 on() 来绑定事件。语法:

.on( events [, selector ] [, data ], handler )

events 即为事件的名称,你可以传递第二个参数来实现事件代理,具体文档.on() 这里不再赘述。

处理过兼容性的事件对象(Event Object)

事件对象有些方法等也有兼容性差异,jQuery 将其封装处理,并提供跟标准一直的命名。

如果你想在 jQuery 事件回调函数中访问原来的事件对象,需要使用 event.originalEvent,它指向原生的事件对象。

触发事件 trigger 方法

点击某个绑定了 click 事件的节点,自然会触发该节点的 click 事件,从而执行对应回调函数。

trigger 方法可以模拟触发事件,我们单击另一个节点 elementB,可以使用:

$(elementB).on(&#39;click&#39;, function(){
    $(elementA).trigger( "click" );
});

来触发 elementA 节点的单击监听回调函数。详情请看文档 .trigger()。

事件进阶话题

IE 浏览器的差异和兼容性问题

IE 浏览器就是特立独行,它对于事件的操作与标准有一些差异。不过 IE 浏览器现在也开始慢慢努力改造,让浏览器变得更加标准。

IE 下绑定事件

在 IE 下面绑定一个事件监听,在 IE9- 无法使用标准的 addEventListener 函数,而是使用自家的 attachEvent,具体用法:

element.attachEvent(<event-name>, <callback>);

其中 参数需要注意,它需要为事件名称添加 on 前缀,比如有个事件叫 click,标准事件监听函数监听 click,IE 这里需要监听 onclick。

另一个,它没有第三个参数,也就是说它只支持监听在冒泡阶段触发的事件,所以为了统一,在使用标准事件监听函数的时候,第三参数传递 false。

当然,这个方法在 IE9 已经被抛弃,在 IE11 已经被移除了,IE 也在慢慢变好。

IE 中 Event 对象需要注意的地方

IE 中往回调函数中传递的事件对象与标准也有一些差异,你需要使用 window.event 来获取事件对象。所以你通常会写出下面代码来获取事件对象:

event = event || window.event

此外还有一些事件属性有差别,比如比较常用的 event.target 属性,IE 中没有,而是使用 event.srcElement 来代替。如果你的回调函数需要处理触发事件的节点,那么需要写:

node = event.srcElement || event.target;

常见的就是这点,更细节的不再多说。在概念学习中,我们没必要为不标准的东西支付学习成本;在实际应用中,类库已经帮我们封装好这些兼容性问题。可喜的是 IE 浏览器现在也开始不断向标准进步。

事件回调函数的作用域问题

与事件绑定在一起的回调函数作用域会有问题,我们来看个例子:

Events in JavaScript: Removing event listeners

回调函数调用的 user.greeting 函数作用域应该是在 user 下的,本期望输出 My name is Bob 结果却输出了 My name is undefined。这是因为事件绑定函数时,该函数会以当前元素为作用域执行。为了证明这一点,我们可以为当前 element 添加属性:

element.firstname = &#39;jiangshui&#39;;

再次点击,可以正确弹出 My name is jiangshui。那么我们来解决一下这个问题。

使用匿名函数

我们为回调函数包裹一层匿名函数。

Events in JavaScript: Removing event listeners

包裹之后,虽然匿名函数的作用域被指向事件触发元素,但执行的内容就像直接调用一样,不会影响其作用域。

使用 bind 方法

使用匿名函数是有缺陷的,每次调用都包裹进匿名函数里面,增加了冗余代码等,此外如果想使用 removeEventListener 解除绑定,还需要再创建一个函数引用。Function 类型提供了 bind 方法,可以为函数绑定作用域,无论函数在哪里调用,都不会改变它的作用域。通过如下语句绑定作用域:

user.greeting = user.greeting.bind(user);

这样我们就可以直接使用:

element.addEventListener(&#39;click&#39;, user.greeting);

常用事件和技巧

用户的操作有很多种,所以有很多事件。为了开发方便,浏览器又提供了一些事件,所以有很多很多的事件。这里只介绍几种常用的事件和使用技巧。

load

load 事件在资源加载完成时触发。这个资源可以是图片、CSS 文件、JS 文件、视频、document 和 window 等等。

比较常用的就是监听 window 的 load 事件,当页面内所有资源全部加载完成之后就会触发。比如用 JS 对图片以及其他资源处理,我们在 load 事件中触发,可以保证 JS 不会在资源未加载完成就开始处理资源导致报错。

同样的,也可以监听图片等其他资源加载情况。

beforeunload

当浏览者在页面上的输入框输入一些内容时,未保存、误操作关掉网页可能会导致输入信息丢失。

当浏览者输入信息但未保存时关掉网页,我们就可以开始监听这个事件,例如:

window.addEventListener("beforeunload", function( event ) {
    event.returnValue = "放弃当前未保存内容而关闭页面?";
});

这时候试图关闭网页的时候,会弹窗阻止操作,点击确认之后才会关闭。当然,如果没有必要,就不要监听,不要以为使用它可以为你留住浏览者。

resize

当节点尺寸发生变化时,触发这个事件。通常用在 window 上,这样可以监听浏览器窗口的变化。通常用在复杂布局和响应式上。

常见的视差滚动效果网站以及同类比较复杂的布局网站,往往使用 JavaScript 来计算尺寸、位置。如果用户调整浏览器大小,尺寸、位置不随着改变则会出现错位情况。在 window 上监听该事件,触发时调用计算尺寸、位置的函数,可以根据浏览器的大小来重新计算。

但需要注意一点,当浏览器发生任意变化都会触发 resize 事件,哪怕是缩小 1px 的浏览器宽度,这样调整浏览器时会触发大量的 resize 事件,你的回调函数就会被大量的执行,导致变卡、崩溃等。

你可以使用函数 throttle 或者 debounce 技巧来进行优化,throttle 方法大体思路就是在某一段时间内无论多次调用,只执行一次函数,到达时间就执行;debounce 方法大体思路就是在某一段时间内等待是否还会重复调用,如果不会再调用,就执行函数,如果还有重复调用,则不执行继续等待。关于它们更详细的信息,我后面会介绍一下发表在我的博客上,这里不再赘述。

error

当我们加载资源失败或者加载成功但是只加载一部分而无法使用时,就会触发 error 事件,我们可以通过监听该事件来提示一个友好的报错或者进行其他处理。比如 JS 资源加载失败,则提示尝试刷新;图片资源加载失败,在图片下面提示图片加载失败等。该事件不会冒泡。因为子节点加载失败,并不意味着父节点加载失败,所以你的处理函数必须精确绑定到目标节点。

需要注意的是,对于该事件,你可以使用 addEventListener 等进行监听,但是有时候会出现失效情况(看这个例子),这是因为 error 事件都触发过了,你的 JS 监听处理代码还没有加载进来执行。为了避免这种情况,用内联法更好一些:

<img  src="not-found.jpg" onerror="doSomething" / alt="最詳細的JavaScript與事件解讀" >

如果还有其他常用事件,欢迎留言补充。

用 JavaScript 模拟触发内置事件

内置的事件也可以被 JavaScript 模拟触发,比如下面函数模拟触发单击事件:

function simulateClick() {
  var event = new MouseEvent(&#39;click&#39;, {
    &#39;view&#39;: window,
    &#39;bubbles&#39;: true,
    &#39;cancelable&#39;: true
  });
  var cb = document.getElementById(&#39;checkbox&#39;); 
  var canceled = !cb.dispatchEvent(event);
  if (canceled) {
    // A handler called preventDefault.
    alert("canceled");
  } else {
    // None of the handlers called preventDefault.
    alert("not canceled");
  }
}

可以看这个 Demo 来了解更多。

自定义事件

我们可以自定义事件来实现更灵活的开发,事件用好了可以是一件很强大的工具,基于事件的开发有很多优势(后面介绍)。

与自定义事件的函数有 Event、CustomEvent 和 dispatchEvent。

直接自定义事件,使用 Event 构造函数:

var event = new Event(&#39;build&#39;);

// Listen for the event.
elem.addEventListener(&#39;build&#39;, function (e) { ... }, false);

// Dispatch the event.
elem.dispatchEvent(event);

CustomEvent 可以创建一个更高度自定义事件,还可以附带一些数据,具体用法如下:

var myEvent = new CustomEvent(eventname, options);

其中 options 可以是:

{
    detail: {
        ...
    },
    bubbles: true,
    cancelable: false
}

其中 detail 可以存放一些初始化的信息,可以在触发的时候调用。其他属性就是定义该事件是否具有冒泡等等功能。

内置的事件会由浏览器根据某些操作进行触发,自定义的事件就需要人工触发。dispatchEvent 函数就是用来触发某个事件:

element.dispatchEvent(customEvent);

上面代码表示,在 element 上面触发 customEvent 这个事件。结合起来用就是:

// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) });

// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}});
obj.dispatchEvent(event);

使用自定义事件需要注意兼容性问题,而使用 jQuery 就简单多了:

// 绑定自定义事件
$(element).on(&#39;myCustomEvent&#39;, function(){});

// 触发事件
$(element).trigger(&#39;myCustomEvent&#39;);

此外,你还可以在触发自定义事件时传递更多参数信息:

$( "p" ).on( "myCustomEvent", function( event, myName ) {
  $( this ).text( myName + ", hi there!" );
});
$( "button" ).click(function () {
  $( "p" ).trigger( "myCustomEvent", [ "John" ] );
});

更详细的用法请看 Introducing Custom Events,这里不再赘述。

在开发中应用事件

当我们操作某一个 DOM,发出一个事件,我们可以在另一个地方写代码捕获这个事件执行处理逻辑。触发操作和捕获处理操作是分开的。我们可以根据这个特性来对程序解耦。

用事件解耦

我们可以将一个整个的功能,分割成独立的小功能,每个小功能绑定一个事件,由一个“控制器”负责根据条件触发某个事件。这样,在外面触发这个事件,也可以调用对应功能,使其更加灵活。

最詳細的JavaScript與事件解讀

在《基于 MVC 的 JavaScript Web 富应用开发》一书中,有更加具体的实例,有兴趣的朋友可以买本看看。

发布(Publish)和订阅(Subscribe)模式

针对上面这种用法,继续抽象一下,就是发布和订阅开发模式。正如其名,这种模式有两个角色:发布者和订阅者,此外有一条信道,发布者被触发往这个信道里面发信,订阅者从这个信道里面收信,如果收到特定信件则执行某个对应的逻辑。这样,发布者和订阅者之间是完全解耦的,只有一条信道连接。这样就非常容易扩展,也不会引入额外的依赖。

这样如果需要添加新功能,只需要添加一个新的订阅者(及其执行逻辑),监听信道中某一类新的信件。再在应用中通过发布者发送一类新的信件即可。

具体实现,这里推荐 cowboy 开发的 Tiny Pub Sub,通过 jQuery 实现,非常简洁直观,jQuery 太赞。代码就这几行:

(function($) {

  var o = $({});

  $.subscribe = function() {
    o.on.apply(o, arguments);
  };

  $.unsubscribe = function() {
    o.off.apply(o, arguments);
  };

  $.publish = function() {
    o.trigger.apply(o, arguments);
  };

}(jQuery));

定义一个对象作为信道,然后提供了三个方法,订阅者、取消订阅、发布者。

 以上就是最详细的JavaScript和事件解读的内容,更多相关内容请关注PHP中文网(www.php.cn)!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn