防抖
防抖, 僅僅從字面去理解,就是防止抖動(dòng),關(guān)鍵點(diǎn)是等待,等待300ms,如果沒(méi)有新的action,就執(zhí)行。
這里舉一個(gè)更形象的例子,也是使用此場(chǎng)景最多的例子。
有一個(gè)搜索輸入框,為了提升用戶體驗(yàn),希望在用戶輸入后可以立即展現(xiàn)搜索結(jié)果,而不是每次輸入完后還要點(diǎn)擊搜索按鈕。最基本的實(shí)現(xiàn)方式應(yīng)該很容易想到,那就是綁定 input 元素的鍵盤事件,每次輸入的時(shí)候,觸發(fā)input,向后臺(tái)發(fā)起請(qǐng)求。
類似于以下偽代碼:
const inputEle = document.querySelector('input')
async function search (e) {
await getUser({name: e.targe.value})
}
inputEle.addEventListener('input', search)
但是這個(gè)時(shí)候,后端提出了一個(gè)問(wèn)題,不希望用戶每輸入一個(gè)字符,就發(fā)起一次后端請(qǐng)求,這樣會(huì)造成資源的浪費(fèi)。例如每當(dāng)用戶輸入一個(gè)字符,都會(huì)觸發(fā)搜索,而實(shí)際上,只有最后一次搜索結(jié)果是用戶想要的,前面進(jìn)行了 2 次無(wú)效查詢,浪費(fèi)了網(wǎng)絡(luò)帶寬和服務(wù)器資源。
- 1: 'l'
- 2: 'li'
- 3: 'liu'
對(duì)于這類連續(xù)觸發(fā)的事件,需要添加一個(gè)“防抖”功能,為函數(shù)的執(zhí)行設(shè)置一個(gè)合理的時(shí)間間隔,避免事件在時(shí)間間隔內(nèi)頻繁觸發(fā),同時(shí)又保證用戶輸入后能即時(shí)看到搜索結(jié)果。
接下來(lái),我們分析下如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單版的的防抖
function debounce (fn, wait = 0) {
let timeout = null
return function () {
// 如果已經(jīng)存在定時(shí)器,說(shuō)明是在wait間隔之內(nèi)觸發(fā)的,需要重新計(jì)時(shí)
if (timeout) {
clearTimeout(timeout)
timeout = null
}
let self = this
let args = Array.prototype.slice(arguments)
timeout = setTimeout(() => {
fn.call(self, ...args)
}, wait)
}
}
如果通過(guò)箭頭函數(shù),也可以這么寫
function debounce = (fn, wait = 0) => {
let timeout = null
return (...args) => {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
timeout = setTimeout(async () => {
// 箭頭函數(shù)里的this,指向定義時(shí)的上一層,沒(méi)有自己的this
await fn.apply(this, args)
}, wait)
}
}
通過(guò)這段代碼,我們做測(cè)試和調(diào)試
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>截流與防抖</title></title>
<script src="./debounce.js"></script>
</head>
<body>
<div>
<input type="text"> 請(qǐng)輸入
</div>
<script>
const inputEle = document.querySelector('input')
async function search (e) {
console.log(this)
console.log('input==>', e.target.value)
}
inputEle.addEventListener('input', debounce(search, 200))
</script>
</body>
</html>
接下來(lái),我們實(shí)現(xiàn)一個(gè)豪華版的防抖
const debounce = (func, await = 0) => {
let timeout = null
let args
function dobounced(...arg) {
args = arg
if (timeout) {
clearTimeout(timeouts)
timeout = null
}
return new Promise((resolve, reject) => {
timeout = setTimeout(async() => {
try {
const res = await func.apply(this, args)
resolve(res)
} catch (e) {
reject(e)
}
}, wait)
})
}
// 允許取消
function cancel() {
clearTimeout(timeout)
timeout = null
}
// 允許立即執(zhí)行
function flush() {
cancel()
return func.apply(this, args)
}
debounced.cancel = cancel
debounced.flush = flush
return debounced
}
節(jié)流
我們知道有個(gè)詞語(yǔ),叫做開源節(jié)流。這里的節(jié)流是省著消費(fèi)。而我們JS里的節(jié)流,也用相同的意義,減少?zèng)]有必要的dom操作,因?yàn)閐om操作是昂貴的。節(jié)流的關(guān)鍵詞是丟棄, 500ms內(nèi)只執(zhí)行一次,其余的action 被丟棄。打一個(gè)生活中的比喻就是:我腦海里無(wú)時(shí)無(wú)刻都有購(gòu)物的想法,但是要省錢,就必須節(jié)制購(gòu)物,給自己定下規(guī)則,3個(gè)月內(nèi)只能買一次物品。當(dāng)然現(xiàn)實(shí)生活中,小伙伴們不會(huì)對(duì)自己這么嚴(yán)苛啊,這里的例子只是方便大家理解。
我們舉一個(gè)項(xiàng)目上的應(yīng)用,加深對(duì)節(jié)流的理解。
例子來(lái)源于拉勾教育-前端高手進(jìn)階, 有興趣可以到應(yīng)用里學(xué)習(xí)
一個(gè)左右兩列布局的查看文章頁(yè)面,左側(cè)為文章大綱結(jié)構(gòu),右側(cè)為文章內(nèi)容?,F(xiàn)在需要添加一個(gè)功能,就是當(dāng)用戶滾動(dòng)閱讀右側(cè)文章內(nèi)容時(shí),左側(cè)大綱相對(duì)應(yīng)部分高亮顯示,提示用戶當(dāng)前閱讀位置。這個(gè)功能的實(shí)現(xiàn)思路比較簡(jiǎn)單,滾動(dòng)前先記錄大綱中各個(gè)章節(jié)的垂直距離,然后監(jiān)聽 scroll 事件的滾動(dòng)距離,根據(jù)距離的比較來(lái)判斷需要高亮的章節(jié)。偽代碼如下:
// 監(jiān)聽scroll事件
wrap.addEventListener('scroll', e => {
let highlightId = ''
// 遍歷大綱章節(jié)位置,與滾動(dòng)距離比較,得到當(dāng)前高亮章節(jié)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')
}
})
功能是實(shí)現(xiàn)了,但這并不是最優(yōu)方法,因?yàn)闈L動(dòng)事件的觸發(fā)頻率是很高的,持續(xù)調(diào)用判斷函數(shù)很可能會(huì)影響渲染性能。實(shí)際上也不需要過(guò)于頻繁地調(diào)用,因?yàn)楫?dāng)鼠標(biāo)滾動(dòng) 1 像素的時(shí)候,很有可能當(dāng)前章節(jié)的閱讀并沒(méi)有發(fā)生變化。所以我們可以設(shè)置在指定一段時(shí)間內(nèi)只調(diào)用一次函數(shù),從而降低函數(shù)調(diào)用頻率,這種方式我們稱之為“節(jié)流”。
方法一:
通過(guò)一個(gè)標(biāo)識(shí)位,來(lái)實(shí)現(xiàn)第一個(gè)截流,可以滿足一般場(chǎng)景
function throttle (fn, interval = 0) {
let isExecute = true
return function (...args) {
if (isExecute) {
fn.apply(this, args)
isExecute = false
setTimeout(() => {
isExecute = true
}, interval)
}
}
}
方法二:
通過(guò)時(shí)間來(lái)控制
第一次就會(huì)觸發(fā), 因?yàn)榈谝淮蝜ast ===0 ,導(dǎo)致 now 是一定大于 delay的,所以第一次必須觸發(fā)
function throttle(fn, interval) {
let last = 0
return function () {
let now = Date.now()
let context = this
let args = arguments
if (now - last > interval) {
last = Date.now()
fn.apply(context, args)
}
}
}
方法三,思想類似與方法一
第一次也是延遲執(zhí)行,但是用戶最后一次操作,也會(huì)延遲執(zhí)行
function throttle(fn, interval = 0) {
let timer = null
return function () {
let args = arguments
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
clearTimeout(timer)
timer = null
}, interval)
}
}
}
方法四:更精確的時(shí)間控制防抖
function throttle5(fn, interval) {
let timer = null
let startTime = Date.now()
return function () {
let curTime = Date.now()
let remainning = interval - (curTime - startTime)
let context = this
let args = arguments
clearTimeout(timer)
// 操作已過(guò)剩余時(shí)間,立即執(zhí)行
if (remainning <=0) {
// 重新計(jì)時(shí)
startTime = Date.now()
fn.apply(context, args)
} else {
timer = setTimeout(() => {
fn.apply(context, args)
}, remainning)
}
}
}
可以通過(guò)下面這點(diǎn)代碼來(lái)驗(yàn)證調(diào)試throttle
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>截流與防抖</title></title>
<script src="./debounce.js"></script>
<script src="./throttle.js"></script>
<style>
.test {
width: 200px;
height: 200px;
overflow: auto;
background-color: burlywood;
}
</style>
</head>
<body>
<div class="test">
一個(gè)左右兩列布局的查看文章頁(yè)面,左側(cè)為文章大綱結(jié)構(gòu),右側(cè)為文章內(nèi)容?,F(xiàn)在需要添加一個(gè)功能,就是當(dāng)用戶滾動(dòng)閱讀右側(cè)文章內(nèi)容時(shí),左側(cè)大綱相對(duì)應(yīng)部分高亮顯示,提示用戶當(dāng)前閱讀位置。這個(gè)功能的實(shí)現(xiàn)思路比較簡(jiǎn)單,滾動(dòng)前先記錄大綱中各個(gè)章節(jié)的垂直距離,然后監(jiān)聽 scroll 事件的滾動(dòng)距離,根據(jù)距離的比較來(lái)判斷需要高亮的章節(jié)。
</div>
<script>
const ele = document.querySelector('.test')
function scrollFn (e) {
console.log('this==>', this)
console.log('top', e.target.scrollTop)
}
ele.addEventListener('scroll', throttle5(scrollFn, 500))
</script>
</body>
</html>