防抖
input的輸入自動關鍵字匹配提示,通過監聽每一次輸入來出發 AJAX 請求,獲取匹配數據,但是每一次的輸入並不一定都要發起AJAX請求,此時,我們就需要通過使用 setTimeout() 函數來讓函數延遲執行,達到添加防抖功能。
- 普通寫法
// 代碼1
const ipt = document.querySelector('input')
let timeout = null
ipt.addEventListener('input', e => {
if(timeout) {
clearTimeout(timeout)
timeout = null
}
timeout = setTimeout(() => {
search(e.target.value).then(resp => {
// ...
}, e => {
// ...
})
}, 500)
})
- 優化寫法
爲了達到複用,可以抽取成公共函數。
// 代碼2
const debounce = (func, wait = 0) => {
let timeout = null
let args
function debounced(...arg) {
args = arg
if(timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函數執行結果
return new Promise((res, rej) => {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch(e) {
rej(e)
}
}, wait)
})
}
// 允許取消
function cancel() {
clearTimeout(timeout)
timeout = null
}
// 允許立即執行
function flush() {
cancel()
return func.apply(this, args)
}
debounced.cancel = cancel
debounced.flush = flush
return debounced
}
代碼說明:首先將原函數作爲參數傳入 debounce() 函數中,同時指定延遲等待時間,返回一個新的函數,這個函數包含 cancel 屬性,用來取消原函數執行。flush 屬性用來立即調用原函數,同時將原函數的執行結果以 Promise 的形式返回。
節流
場景
實現文章閱讀大綱節點高亮顯示。具體就是,當用戶滾動閱讀右側文章內容時,左側大綱相對應部分高亮顯示,提示用戶當前閱讀位置。
現在來考慮另外一個場景,一個左右兩列布局的查看文章頁面,左側爲文章大綱結構,右側爲文章內容。現在需要添加一個功能,就是當用戶滾動閱讀右側文章內容時,左側大綱相對應部分高亮顯示,提示用戶當前閱讀位置。
這個功能的實現思路比較簡單,滾動前先記錄大綱中各個章節的垂直距離,然後監聽 scroll 事件的滾動距離,根據距離的比較來判斷需要高亮的章節。
思路
- 滾動前先記錄大綱中各個章節的垂直距離;
- 監聽 scroll 事件的滾動距離,根據距離的比較來判斷需要高亮的章節。
//爲代碼
// 監聽scroll事件
wrap.addEventListener('scroll', e => {
let highlightId = ''
// 遍歷大綱章節位置,與滾動距離比較,得到當前高亮章節id
for (let id in offsetMap) {
if (e.target.scrollTop <= offsetMap[id].offsetTop) {
highlightId = id
break
}
}
const lastDom = document.querySelector('.highlight')
const currentElem = document.querySelector(`a[href="#${highlightId}"]`)
// 修改高亮樣式
if (lastDom && lastDom.id !== highlightId) {
lastDom.classList.remove('highlight')
currentElem.classList.add('highlight')
} else {
currentElem.classList.add('highlight')
}
})
以上代碼可以實現大綱高亮的功能,但是由於滾動時間出發頻率很高,持續調用會影響渲染性能,因此,我們可以指定一段時間內只調用一次函數,從而降低函數調用頻率,這種方式我們稱之爲“節流”。
節流函數的兩種執行方式:
- 在調用函數時執行最先一次調用;
- 在調用函數時執行最近一次調用。
通過節流方式實現如下:
//僞代碼
const throttle = (func, wait = 0, execFirstCall) => {
let timeout = null
let args
let firstCallTimestamp
function throttled(...arg) {
if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
if (!execFirstCall || !args) {
console.log('set args:', arg)
args = arg
}
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 以Promise的形式返回函數執行結果
return new Promise(async(res, rej) => {
if (new Date().getTime() - firstCallTimestamp >= wait) {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
} else {
timeout = setTimeout(async () => {
try {
const result = await func.apply(this, args)
res(result)
} catch (e) {
rej(e)
} finally {
cancel()
}
}, firstCallTimestamp + wait - new Date().getTime())
}
})
}
// 允許取消
function cancel() {
clearTimeout(timeout)
args = null
timeout = null
firstCallTimestamp = null
}
// 允許立即執行
function flush() {
cancel()
return func.apply(this, args)
}
throttled.cancel = cancel
throttled.flush = flush
return throttle
}
tips:
節流與防抖都是通過延遲執行,減少調用次數,來優化頻繁調用函數時的性能。不同的是,對於一段時間內的頻繁調用,防抖是延遲執行後一次調用,節流是延遲定時多次調用。
代理
以下實例是通過事件代理來優化列表中控件的點擊事件監聽。
初始實例
通過點擊每個項目的時候調用 getInfo() 函數,當點擊“編輯”時,調用一個 edit() 函數,當點擊“刪除”時,調用一個 del() 函數。
<ul class="list">
<li class="item" id="item1">項目1<span class="edit">編輯</span><span class="delete">刪除</span></li>
<li class="item" id="item2">項目2<span class="edit">編輯</span><span class="delete" >刪除</span></li>
<li class="item" id="item3">項目3<span class="edit">編輯</span><span class="delete">刪除</span></li>
...
</ul>
問題所在
功能的實現並不難,但如果數據量一旦增大,事件綁定佔用的內存以及執行時間將會成線性增加,而其實這些事件監聽函數邏輯一致,只是參數不同而已。此時我們可以以事件代理或事件委託來進行優化。
首先,看看事件觸發流程的三個階段:
- 捕獲,事件對象 Window 傳播到目標的父對象,圖 1 的紅色過程;
- 目標,事件對象到達事件對象的事件目標,圖 1 的藍色過程;
- 冒泡,事件對象從目標的父節點開始傳播到 Window,圖 1 的綠色過程。
監聽事件執行順序實例:
<body>
<button>click</button>
</body>
<script>
document.querySelector('button').addEventListener('click', function () {
console.log('bubble')
})
document.querySelector('button').addEventListener('click', function () {
console.log('capture')
}, true)
// 執行結果
// buble
// capture
</script>
結合上述對DOM事件的觸發流程,我們得到事件代理的原理就是利用 DOM 事件的觸發流程來對一類事件進行統一處理。以下是事件代理實例代碼:
const ul = document.querySelector('.list')
ul.addEventListener('click', e => {
const t = e.target || e.srcElement
if (t.classList.contains('item')) {
getInfo(t.id)
} else {
id = t.parentElement.id
if (t.classList.contains('edit')) {
edit(id)
} else if (t.classList.contains('delete')) {
del(id)
}
}
})
補充
區別以下三種事件監聽方式:
// 方式1
<input type="text" onclick="click()"/>
// 方式2
document.querySelector('input').onClick = function(e) {
// ...
}
// 方式3
document.querySelector('input').addEventListener('click', function(e) {
//...
})
方式 1 和方式 2 同屬於 DOM0 標準,通過這種方式進行事件監會覆蓋之前的事件監聽函數。
方式 3 屬於 DOM2 標準,推薦使用這種方式。同一元素上的事件監聽函數互不影響,而且可以獨立取消,調用順序和監聽順序一致。
筆記內容來自拉勾教育朱德龍講師講解的 前端高手進階 第三講