javascript高級程序設(shè)計(第13章)-- 事件

第十三章:事件

本章內(nèi)容:

  • 理解事件流
  • 使用事件處理程序
  • 不同的事件類型

13.1 事件流

頁面的那個部分擁有特定的事件?想想畫在一張紙上的同心圓,你手指按住了圓心。那么你手指的不是一個圓。而是紙上的所有圓。換句話說,你單擊了頁面上的某個按鈕,你也單擊了按鈕的容器元素,甚至單擊了整個頁面。

事件流表述了從頁面接受事件的順序。

13.1.1 事件冒泡

IE的事件流叫做事件冒泡(event bubbling),即事件開始時從具體的元素接受,然后向上傳播到不具體的節(jié)點(文檔)l

<html>
    <head>
        <title>Event Bubbling Example</title>  
    </head>
    <body>
        <div id='myDiv'>
            click me
        </div>
    </body>
</html>

當(dāng)你點擊div的元素執(zhí)行順序如下圖:

mark

所有現(xiàn)在瀏覽器都支持事件冒泡

13.1.2 事件捕獲

Netscape提出了另一種事件流叫做事件捕獲(event capturing)。事件捕獲的思想是不太具體的節(jié)點應(yīng)該更早接收到事件,而最具體的節(jié)點放到最后.

mark

13.1.3 DOM事件流

“DOM2級事件”規(guī)定事件流包括三個階段:事件捕獲階段,處于目標(biāo)階段和事件冒泡階段。

mark

13.2 事件處理程序

事件就是用戶或瀏覽器自身執(zhí)行的某種動作,而對應(yīng)的相應(yīng)事件的函數(shù)就是事件處理程序(或叫事件監(jiān)聽器)

13.2.1 HTML事件處理程序

<input type='button' value='click me' onclick='alert("click")'>

13.2.2 DOM0級事件處理程序

var btn = document.getElementById('myBtn');
btn.onclick = function(){
    alert("click")
}

缺點: 每個元素只能綁定一個同名事件。

13.2.3 DOM2級事件處理程序

DOM2級別事件定義了兩個方法,用于處理添加和刪除事件的操作:addEventListiner()和removeEventListiner()。

所有DOM都包含這兩個方法,并且接受三個參數(shù)

  1. 要處理的事件名
  2. 作為事件處理的函數(shù)
  3. 一個布爾值,如果是true,表示在捕獲階段調(diào)用處理函數(shù),如果是false,表示在冒泡階段調(diào)用處理函數(shù)。

而且不像DOM0中的onclick只能綁定一次,DOM2級的時間可以綁定多個

var btn = document.getElementById('myBtn');
btn.addEventListener('click',function(){
    alert('click');
}, false)
btn.addEventListener('click',function(){
    alert('hello');
}, false)

通過addEventListner()添加的事件只能通過removeEventListener來移除;移除時候傳遞的參數(shù)與添加處理程序的參數(shù)必須要參數(shù)相同。這也意味著匿名函數(shù)無法移除。

var btn = document.getElementById('myBtn');
btn.addEventListener('click',function(){
    alert('click');
}, false)
btn.removeEventLisner('click',function(){ // 沒有用!?。?    alert('click');
},false)
var btn = document.getElementById('myBtn');
var handler = function(){
    alert(this);
}
btn.addEventListener('click',handler, false)
btn.removeEventLisner('click',handler,false); //有效

因為可以綁定多個事件,函數(shù)是引用類型,匿名函數(shù)會生成一個新的函數(shù)。也即匿名函數(shù)無法移除。

13.3 事件對象

在DOM上觸發(fā)事件時,會產(chǎn)生一個事件對象event。

關(guān)于target、currentTarget、this

在事件處理的程序內(nèi)部,this始終等于currentTarget,而target則包含事件的實際目標(biāo)。

document.body.onclick = function(event){
    alert(evnet.currentTarget == document.body); //ture
    alert(this == document.body); //ture
    alert(evnet.target == document.getElementById('myBtn')); //ture
}

在點擊這個例子的按鈕時候,this和currentTarget都指向body。因為事件處理器注冊到這個元素的。然而target元素卻等于按鈕元素,因為它是click事件真正的目標(biāo)。

13.5 內(nèi)存和性能

13.5.1 事件委托

對”事件處理程序過多“問題的解決方案就是事件委托。事件委托利用了事件的冒泡,只指定了一個事件處理程序,來管理某一類型的所有事件。

<ul id="myLinks">
    <li id='goSomewhere'>go somewhere</li>
    <li id='goSomething'>go something</li>
    <li id='sayHi'>say hi</li>
</ul>

<script>
    var list = document.getElementById('myLinks');
    list.addEventListiner('click', function(event){
        var target = event.target;
        switch(target){
            case 'goSomeWhere': 
                alert('gosomeWhere')
                break;
            case 'goSomething': 
                alert('goSomething')
                break;
            case 'sayHi': 
                alert('sayHi')
                break;
        }
    },false)
</script>

延伸閱讀1: 深入核心,詳解事件循環(huán)機(jī)制

在學(xué)習(xí)事件循環(huán)機(jī)制之前,我默認(rèn)你已經(jīng)懂得了如下概念,如果仍然有疑問,可以回過頭去看看我以前的文章。

  • 執(zhí)行上下文(Execution context)
  • 函數(shù)調(diào)用棧(call stack)
  • 隊列數(shù)據(jù)結(jié)構(gòu)(queue)
  • Promise(我會在下一篇文章專門總結(jié)Promise的詳細(xì)使用)

因為chrome瀏覽器中新標(biāo)準(zhǔn)中的事件循環(huán)機(jī)制與nodejs類似,因此此處就整合nodejs一起來理解,其中會介紹到幾個nodejs有,但是瀏覽器中沒有的API,大家只需要了解就好,不一定非要知道她是如何使用。比如process.nextTick,setImmediate

  • 我們知道JavaScript的一大特點就是單線程,而這個線程中擁有唯一的一個事件循環(huán)。
  • JavaScript代碼的執(zhí)行過程中,除了依靠函數(shù)調(diào)用棧來搞定函數(shù)的執(zhí)行順序外,還依靠任務(wù)隊列(task queue)來搞定另外一些代碼的執(zhí)行。
mark
  • 一個線程中,事件循環(huán)是唯一的,但是任務(wù)隊列可以擁有多個。
  • 任務(wù)隊列又分為macro-task(宏任務(wù))與micro-task(微任務(wù)),在最新標(biāo)準(zhǔn)中,它們被分別稱為task與jobs。
  • macro-task大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
  • setTimeout/Promise等我們稱之為任務(wù)源。而進(jìn)入任務(wù)隊列的是他們指定的具體執(zhí)行任務(wù)。
// setTimeout中的回調(diào)函數(shù)才是進(jìn)入任務(wù)隊列的任務(wù)
setTimeout(function() {
    console.log('xxxx');
})
// 非常多的同學(xué)對于setTimeout的理解存在偏差。所以大概說一下誤解:
// setTimeout作為一個任務(wù)分發(fā)器,這個函數(shù)會立即執(zhí)行,而它所要分發(fā)的任務(wù),也就是它的第一個參數(shù),才是延遲執(zhí)行
  • 來自不同任務(wù)源的任務(wù)會進(jìn)入到不同的任務(wù)隊列。其中setTimeout與setInterval是同源的。
  • 事件循環(huán)的順序,決定了JavaScript代碼的執(zhí)行順序。它從script(整體代碼)開始第一次循環(huán)。之后全局上下文進(jìn)入函數(shù)調(diào)用棧。直到調(diào)用棧清空(只剩全局),然后執(zhí)行所有的micro-task。當(dāng)所有可執(zhí)行的micro-task執(zhí)行完畢之后。循環(huán)再次從macro-task開始,找到其中一個任務(wù)隊列并不是一個任務(wù))執(zhí)行完畢,然后再執(zhí)行所有的micro-task,這樣一直循環(huán)下去。
  • 其中每一個任務(wù)的執(zhí)行,無論是macro-task還是micro-task,都是借助函數(shù)調(diào)用棧來完成。

純文字表述確實有點干澀,因此,這里我們通過2個例子,來逐步理解事件循環(huán)的具體順序。

例1
// demo01  出自于上面我引用文章的一個例子,我們來根據(jù)上面的結(jié)論,一步一步分析具體的執(zhí)行過程。
// 為了方便理解,我以打印出來的字符作為當(dāng)前的任務(wù)名稱
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve){
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
     console.log('promise2');
}).then(function() {
    console.log('then1');
})
console.log('global1');

首先,事件循環(huán)從宏任務(wù)隊列開始,這個時候,宏任務(wù)隊列中,只有一個script(整體代碼)任務(wù)。每一個任務(wù)的執(zhí)行順序,都依靠函數(shù)調(diào)用棧來搞定,而當(dāng)遇到任務(wù)源時,則會先分發(fā)任務(wù)到對應(yīng)的隊列中去,所以,上面例子的第一步執(zhí)行如下圖所示。

mark

第二步:script任務(wù)執(zhí)行時首先遇到了setTimeout,setTimeout為一個宏任務(wù)源,那么他的作用就是將任務(wù)分發(fā)到它對應(yīng)的隊列中。

setTimeout(function() {
    console.log('timeout1');
})
mark

第三步:script執(zhí)行時遇到Promise實例。Promise構(gòu)造函數(shù)中的第一個參數(shù),是在new的時候執(zhí)行,因此不會進(jìn)入任何其他的隊列,而是直接在當(dāng)前任務(wù)直接執(zhí)行了,而后續(xù)的.then則會被分發(fā)到micro-task的Promise隊列中去。

new Promise(function(resolve){
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
     console.log('promise2');
}).then(function() {
    console.log('then1');
})

因此,構(gòu)造函數(shù)執(zhí)行時,里面的參數(shù)進(jìn)入函數(shù)調(diào)用棧執(zhí)行。for循環(huán)不會進(jìn)入任何隊列,因此代碼會依次執(zhí)行,所以這里的promise1和promise2會依次輸出。

mark
mark
mark

script任務(wù)繼續(xù)往下執(zhí)行,最后只有一句輸出了globa1,然后,全局任務(wù)就執(zhí)行完畢了。

第四步:第一個宏任務(wù)script執(zhí)行完畢之后,就開始執(zhí)行所有的可執(zhí)行的微任務(wù)。這個時候,微任務(wù)中,只有Promise隊列中的一個任務(wù)then1,因此直接執(zhí)行就行了,執(zhí)行結(jié)果輸出then1,當(dāng)然,他的執(zhí)行,也是進(jìn)入函數(shù)調(diào)用棧中執(zhí)行的。

mark

第五步:當(dāng)所有的micro-tast執(zhí)行完畢之后,表示第一輪的循環(huán)就結(jié)束了。這個時候就得開始第二輪的循環(huán)。第二輪循環(huán)仍然從宏任務(wù)macro-task開始。

mark

這個時候,我們發(fā)現(xiàn)宏任務(wù)中,只有在setTimeout隊列中還要一個timeout1的任務(wù)等待執(zhí)行。因此就直接執(zhí)行即可。

mark

這個時候宏任務(wù)隊列與微任務(wù)隊列中都沒有任務(wù)了,所以代碼就不會再輸出其他東西了。

例1執(zhí)行結(jié)果
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve){
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
     console.log('promise2');
}).then(function() {
    console.log('then1');
})
console.log('global1');

/* 輸出結(jié)果為:
promise1
promise2
global1
then1
timeout1
*/
例2:
// demo02
console.log('golb1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})

這個例子看上去有點復(fù)雜,亂七八糟的代碼一大堆,不過不用擔(dān)心,我們一步一步來分析一下。

第一步:宏任務(wù)script首先執(zhí)行。全局入棧。glob1輸出。

mark

第二步,執(zhí)行過程遇到setTimeout。setTimeout作為任務(wù)分發(fā)器,將任務(wù)分發(fā)到對應(yīng)的宏任務(wù)隊列中。

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})
mark

第三步:執(zhí)行過程遇到setImmediate。setImmediate也是一個宏任務(wù)分發(fā)器,將任務(wù)分發(fā)到對應(yīng)的任務(wù)隊列中。setImmediate的任務(wù)隊列會在setTimeout隊列的后面執(zhí)行。

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})
mark

第四步:執(zhí)行遇到nextTick,process.nextTick是一個微任務(wù)分發(fā)器,它會將任務(wù)分發(fā)到對應(yīng)的微任務(wù)隊列中去。

process.nextTick(function() {
    console.log('glob1_nextTick');
})
mark

第五步:執(zhí)行遇到Promise。Promise的then方法會將任務(wù)分發(fā)到對應(yīng)的微任務(wù)隊列中,但是它構(gòu)造函數(shù)中的方法會直接執(zhí)行。因此,glob1_promise會第二個輸出。

new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})
mark
mark

第六步:執(zhí)行遇到第二個setTimeout。

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})
mark

第七步:先后遇到nextTick與Promise

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})
mark

第八步:再次遇到setImmediate。

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
mark

這個時候,script中的代碼就執(zhí)行完畢了,執(zhí)行過程中,遇到不同的任務(wù)分發(fā)器,就將任務(wù)分發(fā)到各自對應(yīng)的隊列中去。接下來,將會執(zhí)行所有微任務(wù)隊列中的任務(wù)。

其中,nextTick隊列會比Promie先執(zhí)行。nextTick中的可執(zhí)行任務(wù)執(zhí)行完畢之后,才會開始執(zhí)行Promise隊列中的任務(wù)。

當(dāng)所有可執(zhí)行的微任務(wù)執(zhí)行完畢之后,這一輪循環(huán)就表示結(jié)束了。下一輪循環(huán)繼續(xù)從宏任務(wù)隊列開始執(zhí)行。

這個時候,script已經(jīng)執(zhí)行完畢,所以就從setTimeout隊列開始執(zhí)行。

mark

setTimeout任務(wù)的執(zhí)行,也依然是借助函數(shù)調(diào)用棧來完成,并且遇到任務(wù)分發(fā)器的時候也會將任務(wù)分發(fā)到對應(yīng)的隊列中去。

只有當(dāng)setTimeout中所有的任務(wù)執(zhí)行完畢之后,才會再次開始執(zhí)行微任務(wù)隊列。并且清空所有的可執(zhí)行微任務(wù)。

setTiemout隊列產(chǎn)生的微任務(wù)執(zhí)行完畢之后,循環(huán)則回過頭來開始執(zhí)行setImmediate隊列。仍然是先將setImmediate隊列中的任務(wù)執(zhí)行完畢,再執(zhí)行所產(chǎn)生的微任務(wù)。

當(dāng)setImmediate隊列執(zhí)行產(chǎn)生的微任務(wù)全部執(zhí)行之后,第二輪循環(huán)也就結(jié)束了。

大家需要注意這里的循環(huán)結(jié)束的時間節(jié)點。

當(dāng)我們在執(zhí)行setTimeout任務(wù)中遇到setTimeout時,它仍然會將對應(yīng)的任務(wù)分發(fā)到setTimeout隊列中去,但是該任務(wù)就得等到下一輪事件循環(huán)執(zhí)行了。例子中沒有涉及到這么復(fù)雜的嵌套,大家可以動手添加或者修改他們的位置來感受一下循環(huán)的變化。

例2執(zhí)行結(jié)果
golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then

小結(jié):

事件是將Javascript與網(wǎng)頁聯(lián)系在一起的主要方式。“DOM3級事件”規(guī)范和HTML5定義了常見的大多數(shù)事件。

在使用事件時,需要考慮如下一些內(nèi)存和性能方面的問題。

  • 有必要限制一個頁面中事件處理程序的數(shù)量,數(shù)量太多會導(dǎo)致占用大量內(nèi)存,而且也會讓用戶感覺反映不太靈敏;
  • 建立在事件冒泡機(jī)制上的事件委托技術(shù),可以有效減少事件處理程序的數(shù)量;
  • 建議在瀏覽器卸載頁面之前移除頁面中的所有事件處理程序;

參考:

前端基礎(chǔ)進(jìn)階(十二):深入核心,詳解事件循環(huán)機(jī)制

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 弄懂js異步 講異步之前,我們必須掌握一個基礎(chǔ)知識-event-loop。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,867評論 0 5
  • 靜下心學(xué)了一波事件循環(huán)機(jī)制,好開心,我學(xué)會了,首先還是得感謝作者寫的筆記特別詳細(xì) 鏈接: http://www.c...
    Dianaou閱讀 619評論 0 0
  • 一、JavaScript單線程模型 JavaScript是單線程的,JavaScript只在一個線程上運行,但是瀏...
    Brolly閱讀 1,231評論 4 6
  • 今日塔二電廠聯(lián)系水冷壁失效分析報告,經(jīng)過尉總審核認(rèn)為泄漏原因為焊接缺陷,主要為應(yīng)力集中導(dǎo)致,國電檢測公司重新做。李...
    hjppljun閱讀 192評論 0 0
  • 早上聽了一節(jié)英語課,然后處理國網(wǎng)學(xué)籍。中午去交通大隊處理違章,下午去了安樂村小學(xué),劉校長說是學(xué)區(qū)中華...
    方圓_22cf閱讀 267評論 0 0

友情鏈接更多精彩內(nèi)容