原文連接javascript
事件委託,也叫事件委派,事件代理。css
當構建應用程序時,有時須要將事件監聽器綁定到頁面上的某些元素上,以便在用戶與元素交互時執行某些操做。html
假設咱們如今有一個無序列表:java
<ul id="todo-app"> <li class="item">Walk the dog</li> <li class="item">Pay bills</li> <li class="item">Make dinner</li> <li class="item">Code for one hour</li> </ul>
咱們須要在<li>
上綁定點擊事件,咱們可能會這樣操做:node
app = document.getElementById('todo-app'); let items = app.getElementsByClassName('item'); // 將事件偵聽器綁定到每一個列表項 for (let item of items) { item.addEventListener('click', function() { alert('you clicked on item: ' + item.innerHTML); }); }
雖然這樣能夠實現功能,但問題是要單獨將事件偵聽器綁定到每一個列表項。這是4個元素,沒什麼大問題,但若是列表中有10,000個事項,怎麼辦?這個函數將會建立10,000個獨立的事件監聽器,並將每一個事件監聽器綁定到 DOM 。這樣代碼執行的效率很是低下。面試
更高效的解決方案是將一個事件偵聽器實際綁定到父容器<ul>
上,而後在實際單擊時能夠訪問每一個確切元素。這被稱爲事件委託,而且它比每一個元素單獨綁定事件的處理程序更高效。瀏覽器
那麼上面的代碼能夠改變爲:閉包
let app = document.getElementById('todo-app'); // 事件偵聽器綁定到整個容器上 app.addEventListener('click', function(e) { if (e.target && e.target.nodeName === 'LI') { let item = e.target; alert('you clicked on item: ' + item.innerHTML); } });
閉包的本質是一個內部函數訪問其做用域以外的變量。閉包能夠用於實現諸如 私有變量 和 建立工廠函數之類的東西。app
在面試中咱們可能會見到一段這樣的代碼:函數
for (var i = 0; i < 4; i++) { setTimeout(function() { console.log(i); }, 1000); }
運行上面的代碼控制檯會在1秒後打印4個4,而不是0,1,2,3。
其緣由是由於setTimeout
函數建立了一個能夠訪問其外部做用域的函數(也就是咱們常常說的閉包),每一個循環都包含了索引i
。
1秒後,該函數被執行而且打印出i
的值,其在循環結束時爲4,由於它的循環週期經歷了0,1,2,3,4,而且循環最終在4時中止。
下面列舉兩種方案解決這個問題:
for (var i = 0; i < 4; i++) { // 經過傳遞變量 i // 在每一個函數中均可以獲取到正確的索引 setTimeout(function(j) { return function() { console.log(j); } }(i), 1000); }
for (let i = 0; i < 4; i++) { // 使用ES6的let語法,它會建立一個新的綁定 // 每一個方法都是被單獨調用的 setTimeout(function() { console.log(i); }, 1000); }
有一些瀏覽器事件能夠在很短的時間內快速啓動屢次,例如頁面滾動事件。若是將事件偵聽器綁定到窗口滾動事件上,而且用戶快速滾動頁面,事件極可能會在短期屢次觸發。這可能會致使一些嚴重的性能問題。
所以,在偵聽滾動,窗口調整大小,或鍵盤按下的事件時,請務必使用函數防抖動(Debouncing)或函數節流(Throttling)來提高頁面速度和性能。
函數防抖(Debouncing)是解決這個問題的一種方式,經過限制須要通過的時間,直到再次調用函數。一個實現函數防抖的方法是:把多個函數放在一個函數裏調用,隔必定時間執行一次。
這裏有一個使用原生JavaScript實現的例子,用到了做用域、閉包、this和定時事件:
function debounce(fn, delay) { // 持久化一個定時器 timer let timer = null; // 閉包函數能夠訪問 timer return function() { // 經過 'this' 和 'arguments' 得到函數的做用域和參數 let self = this; let args = arguments; // 若是事件被觸發,清除 timer 並從新開始計時 clearTimeout(timer); timer = setTimeout(function() { fn.apply(self, args); }, delay); } } // 當用戶滾動時調用函數foo() function foo() { console.log('You are scrolling!'); } // 在事件觸發的兩秒後,包裹在debounce()中的函數纔會被觸發 window.addEventListener('scroll', debounce(foo, 2000));
函數節流是另外一個相似函數防抖的技巧,除了使用等待一段時間再調用函數的方法,函數節流還限制固定時間內只能調用一次。因此,若是一個事件在100毫秒內發生10次,函數節流會每2秒調用一次函數,而不是100毫秒內所有調用。
(完)