zoukankan      html  css  js  c++  java
  • 个税小游戏要点之节流函数(提高动画流畅性)

    什么是throttle和debounce

    throttle(又称节流)和debounce(又称防抖)其实都是函数调用频率的控制器,这里只做简单的介绍,如果想了解更多关于这两个定义的细节可以看下后文给出的一张图片,或者阅读一下lodash的文档

    throttle:将一个函数的调用频率限制在一定阈值内,例如 1s 内一个函数不能被调用两次。

    debounce:当调用函数n秒后,才会执行该动作,若在这n秒内又调用该函数则将取消前一次并重新计算执行时间,举个简单的例子,我们要根据用户输入做suggest,每当用户按下键盘的时候都可以取消前一次,并且只关心最后一次输入的时间就行了。

    lodash 对这两个函数又增加了一些参数,主要是以下三个:

    • leading,函数在每个等待时延的开始被调用

    • trailing,函数在每个等待时延的结束被调用

    • maxwait(debounce才有的配置),最大的等待时间,因为如果 debounce 的函数调用时间不满足条件,可能永远都无法触发,因此增加了这个配置,保证大于一段时间后一定能执行一次函数

    其实 throttle 就是设置了 maxwait 的 debounce。

    lodash 的实现解读

    下面,我就会带着这几个问题去看看 lodasah 的代码。

    官方代码的实现也不是很复杂,这里我贴出一些核心部分代码和我阅读后的注释,后面会讲一下 lodash 的大概流程:

    function debounce(func, wait, options) {
        let lastArgs,
            lastThis,
            maxWait,
            result,
            timerId,
            lastCallTime
    
        // 参数初始化
        let lastInvokeTime = 0 // func 上一次执行的时间
        let leading = false
        let maxing = false
        let trailing = true
    
        // 基本的类型判断和处理
        if (typeof func != 'function') {
            throw new TypeError('Expected a function')
        }
        wait = +wait || 0
        if (isObject(options)) {
            // 对配置的一些初始化
        }
    
        function invokeFunc(time) {
            const args = lastArgs
            const thisArg = lastThis
    
            lastArgs = lastThis = undefined
            lastInvokeTime = time
            result = func.apply(thisArg, args)
            return result
        }
    
        function leadingEdge(time) {
            // Reset any `maxWait` timer.
            lastInvokeTime = time
            // 为 trailing edge 触发函数调用设定定时器
            timerId = setTimeout(timerExpired, wait)
            // leading = true 执行函数
            return leading ? invokeFunc(time) : result
        }
    
       function remainingWait(time) {
            const timeSinceLastCall = time - lastCallTime // 距离上次debounced函数被调用的时间
            const timeSinceLastInvoke = time - lastInvokeTime // 距离上次函数被执行的时间
            const timeWaiting = wait - timeSinceLastCall // 用 wait 减去 timeSinceLastCall 计算出下一次trailing的位置
    
            // 两种情况
            // 有maxing:比较出下一次maxing和下一次trailing的最小值,作为下一次函数要执行的时间
            // 无maxing:在下一次trailing时执行 timerExpired
            return maxing
                ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
                : timeWaiting
        }
    
        // 根据时间判断 func 能否被执行
        function shouldInvoke(time) {
            const timeSinceLastCall = time - lastCallTime
            const timeSinceLastInvoke = time - lastInvokeTime
    
            // 几种满足条件的情况
            return (lastCallTime === undefined //首次
                || (timeSinceLastCall >= wait) // 距离上次被调用已经超过 wait
                || (timeSinceLastCall < 0) //系统时间倒退
                || (maxing && timeSinceLastInvoke >= maxWait)) //超过最大等待时间
        }
    
        function timerExpired() {
            const time = Date.now()
            // 在 trailing edge 且时间符合条件时,调用 trailingEdge函数,否则重启定时器
            if (shouldInvoke(time)) {
                return trailingEdge(time)
            }
            // 重启定时器,保证下一次时延的末尾触发
            timerId = setTimeout(timerExpired, remainingWait(time))
        }
    
        function trailingEdge(time) {
            timerId = undefined
    
            // 有lastArgs才执行,意味着只有 func 已经被 debounced 过一次以后才会在 trailing edge 执行
            if (trailing && lastArgs) {
                return invokeFunc(time)
            }
            // 每次 trailingEdge 都会清除 lastArgs 和 lastThis,目的是避免最后一次函数被执行了两次
            // 举个例子:最后一次函数执行的时候,可能恰巧是前一次的 trailing edge,函数被调用,而这个函数又需要在自己时延的 trailing edge 触发,导致触发多次
            lastArgs = lastThis = undefined
            return result
        }
    
        function cancel() {}
    
        function flush() {}
    
        function pending() {}
    
        function debounced(...args) {
            const time = Date.now()
            const isInvoking = shouldInvoke(time) //是否满足时间条件
    
            lastArgs = args
            lastThis = this
            lastCallTime = time  //函数被调用的时间
    
            if (isInvoking) {
                if (timerId === undefined) { // 无timerId的情况有两种:1.首次调用 2.trailingEdge执行过函数
                    return leadingEdge(lastCallTime)
                }
                if (maxing) {
                    // Handle invocations in a tight loop.
                    timerId = setTimeout(timerExpired, wait)
                    return invokeFunc(lastCallTime)
                }
            }
            // 负责一种case:trailing 为 true 的情况下,在前一个 wait 的 trailingEdge 已经执行了函数;
            // 而这次函数被调用时 shouldInvoke 不满足条件,因此要设置定时器,在本次的 trailingEdge 保证函数被执行
            if (timerId === undefined) {
                timerId = setTimeout(timerExpired, wait)
            }
            return result
        }
        debounced.cancel = cancel
        debounced.flush = flush
        debounced.pending = pending
        return debounced
    }

    这里我用文字来简单描述一下流程:

    首次进入函数时因为 lastCallTime === undefined 并且 timerId === undefined,所以会执行 leadingEdge,如果此时 leading 为 true 的话,就会执行 func。同时,这里会设置一个定时器,在等待 wait(s) 后会执行 timerExpired,timerExpired 的主要作用就是触发 trailing。

    如果在还未到 wait 的时候就再次调用了函数的话,会更新 lastCallTime,并且因为此时 isInvoking 不满足条件,所以这次什么也不会执行。

    时间到达 wait 时,就会执行我们一开始设定的定时器timerExpired,此时因为time-lastCallTime < wait,所以不会执行 trailingEdge。

    这时又会新增一个定时器,下一次执行的时间是 remainingWait,这里会根据是否有 maxwait 来作区分:

    • 如果没有 maxwait,定时器的时间是 wait - timeSinceLastCall,保证下一次 trailing 的执行。

    • 如果有 maxing,会比较出下一次 maxing 和下一次 trailing 的最小值,作为下一次函数要执行的时间。

    最后,如果不再有函数调用,就会在定时器结束时执行 trailingEdge。

  • 相关阅读:
    动态规划-矩阵链乘法
    钢条切割问题
    代码着色
    Sublime配置C和C++编译运行环境
    Guava中集合类的简单实用
    Junit单元测试入门
    Sublime Text 快捷键
    Editplus 的配色方案
    利用Wireshark任意获取QQ好友IP实施精准定位
    linux下实现定时执行php脚本
  • 原文地址:https://www.cnblogs.com/Joe-and-Joan/p/10826471.html
Copyright © 2011-2022 走看看