Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

阅读 lodash 源码学防抖 #5

Open
huitoutunao opened this issue Jun 20, 2021 · 0 comments
Open

阅读 lodash 源码学防抖 #5

huitoutunao opened this issue Jun 20, 2021 · 0 comments
Labels

Comments

@huitoutunao
Copy link
Owner

huitoutunao commented Jun 20, 2021

阅读 lodash 源码学防抖

前言

在介绍防抖之前,我们先看下面这个例子:鼠标滑过黑布触发 onmousemove 事件。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
    <title>debounce</title>
    <style>
        #wrapper {
            width: 100%;
            height: 200px;
            line-height: 200px;
            text-align: center;
            font-size: 30px;
            color: #fff;
            background-color: #666;
        }
    </style>
</head>
<body>
    <div id="wrapper"></div>
    <script src="debounce.js"></script>
</body>
</html>
// debounce.js

var count = 1
var wrapperDom = document.getElementById('wrapper')

function doEvent (e) {
    // 这两个是为了验证防抖前后的变化,后面会提到。
    // console.log(this)
    // console.log(e)
    wrapperDom.innerHTML = count++
}
wrapperDom.onmousemove = doEvent

运行效果图如下:

debounce

从效果图可以看出,浏览器处理数字累加还是蛮流畅的,因为这个例子的事件函数相对简单。假设 1s 触发 1000 次事件,且事件是发送 ajax 请求,那么浏览器处理时就会出现卡顿。

为了解决这类问题,通常使用防抖(debounce)和节流(throttle)的方案。

介绍

防抖指的是触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数,延长执行时间。

实现

根据上面的介绍,我们现在可以简单实现下:

function debounce (func, wait) {
    let timeout
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait)
    }
}

wrapperDom.onmousemove = debounce(doEvent, 1000)

运行效果图如下:

debounce2

现在不管你在 1s 内鼠标移动多少次,它都只在移动完 1s 后再触发事件。

细心的你应该已经发现了,上面的实现过程中,this 的指向和 MouseEvent 对象参数发生了改变。那么现在我们来修复这两个已知问题,如下:

function debounce (func, wait) {
    let timeout
    return function (...args) {
        const lastThis = this
        const lastArgs = args

        clearTimeout(timeout)
        timeout = setTimeout(function () {
            func.apply(lastThis, lastArgs)
        }, wait)
    }
}

立即调用

虽然上面的防抖函数已经基本完成了,但是产品经理有这么个需求:添加一个控制立即调用函数 func 的开关。

// leading 默认不开启立即调用
function debounce (func, wait, leading = false) {
    let timerId
    function debounced (...args) {
        const lastThis = this
        const lastArgs = args

        timerId && clearTimeout(timerId)
        if (leading) {
            const invokeNow = !timerId
            timerId = setTimeout(function () {
                timerId = null
            }, wait)
            invokeNow && func.apply(lastThis, lastArgs)
        } else {
            timerId = setTimeout(function () {
                func.apply(lastThis, lastArgs)
            }, wait)
        }
    }

    return debounced
}

wrapperDom.onmousemove = debounce(doEvent, 1000, true)

运行效果图如下:

debounce3

返回值

如果调用的 func 函数有返回值怎么办?我们得添加它的返回值。代码实现如下:

function debounce (func, wait, leading = false) {
    let timerId,
        result

    function debounced (...args) {
        const lastThis = this
        const lastArgs = args

        timerId && clearTimeout(timerId)
        if (leading) {
            const invokeNow = !timerId
            timerId = setTimeout(function () {
                timerId = null
            }, wait)
            result = invokeNow && func.apply(lastThis, lastArgs)
        } else {
            timerId = setTimeout(function () {
                func.apply(lastThis, lastArgs)
            }, wait)
        }

        return result
    }

    return debounced
}

注意:当 leading 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 leading 为 true 的时候返回函数的执行结果。

虽然实际开发中,这个返回值几乎用不上,但是作为工具库的 Lodash 考虑情况比较全面。

取消

如果我不想等待防抖函数执行了,是否可以取消呢?答案是可以的哈~,下面我们为防抖函数添加一个取消属性即可。

function debounce (func, wait, leading = false) {
    let timerId,
        result

    function debounced (...args) {
        const lastThis = this
        const lastArgs = args

        timerId && clearTimeout(timerId)
        if (leading) {
            const invokeNow = !timerId
            timerId = setTimeout(function () {
                timerId = null
            }, wait)
            result = invokeNow && func.apply(lastThis, lastArgs)
        } else {
            timerId = setTimeout(function () {
                func.apply(lastThis, lastArgs)
            }, wait)
        }

        return result
    }

    function cancel () {
        clearTimeout(timerId)
        timerId = null
    }

    debounced.cancel = cancel

    return debounced
}

// 给页面添加一个取消防抖函数按钮
var activity = debounce(doEvent, 20000, false) // 设置 20s 后才执行函数
$('#wrapper').on('mousemove', activity)
$('#btn').on('click', function () {
    activity.cancel()
})

题外话

或许有小伙伴会问,如果我想要调试 Lodash 源码怎么办?有没有方案推荐呢?

有的哈。我已经把它总结到这篇文章了。戳这里学习

结语

本文到这里就结束了,通过文章我们了解到什么是防抖以及它的实现原理,使用防抖函数可以解决项目中,搜索框输入关键字后间隔一段时间,才会请求获取建议列表......

在前端面试中,防抖函数还是一道高频考题,希望小伙伴们看完本文后能够顺利拿下。

参考文献

Lodash 源码
冴羽的博客

@huitoutunao huitoutunao added the 专题系列 JS专题 label Jun 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant