数组扁平化,柯里化,防抖,节流

@sakila1012 2018-03-23 07:35:33发表于 sakila1012/blog

数组扁平化

数组扁平化:使用递归实现

function flattenDepth(array, depth=1) {
  let result = [];
  array.forEach (item => {
    let d = depth;
    if(Array.isArray(item) && d > 0){
      result.push(...(flattenDepth(item, --d)))
    } else {
      result.push(item);
    }
  })
  return result;
}
console.log(flattenDepth([1,[2,[3,[4]],5]]]))
console.log(flattenDepth([1,[2,[3,[4]],5]],2))
console.log(flattenDepth([1,[2,[3,[4]],5]],3))

将每一项遍历,如果某一项为数组,则让该项继续调用,这里指定了depth作为扁平化的深度,因为这个参数对数组的每一项都要起作用。

柯里化

参数够了就执行,参数不够就返回一个函数,之前的参数存起来,直到够了为止。

function curry(func) {
  var l = func.length;
  return function curried() {
    var args = [].slice.call(arguments);
    if(args.length < l) {
      return function() {
        var argsInner = [].slice.call(arguments)
        return curried.apply(this, args.concat(argsInner))
      }
    } else {
      return func.apply(this, args)
    }
  }
}

var f = function(a,b,c) {
  return console.log([a,b,c])
}
var curried = curry(f);
curried(1)(2)(3)

函数节流和函数防抖

在开发过程中会遇到频率很高的事件或者连续的事件,如果不进行性能的优化,就可能会出现页面卡顿的现象,比如:

  • 鼠标事件:mousemove(拖曳)/mouseover(划过)/mouseWheel(滚屏)
  • 键盘事件:keypress(基于ajax的用户名唯一性校验)/keyup(文本输入检验、自动完成)/keydown(游戏中的射击)
  • window的resize/scroll事件(DOM元素动态定位)
    为了解决这类问题,常常使用的方法就是throttle(节流)和debounce(去抖)。函数节流和函数防抖都是一种对频繁调用代码的优化。可以参考lodash

Debounce

翻译:[计] 防反跳;

在 Javascript 中,那些 DOM 频繁触发的事件,我们想在某个时间点上去执行我们的回调,而不是每次事件每次触发,我们就执行该回调。我们希望多次触发的相同事件的触发合并为一次触发(其实还是触发了好多次,只是我们只关注那一次)。简单地说,即在某段连续时间内,在事件触发后只执行一次

实际应用场景

  • 监听窗口大小重绘的操作(resize)
  • 搜索联想(keyup)
  • 发送一个 ajax 表单,给一个 button 绑定 click 事件,并且监听触发 ajax 请求。如果是 debounce,则用户不管点击多少次,都只会发送一个请求:如果是 throttle,不断点击的过程中会间隔发送请求。这个时候最好使用 debounce。

监听窗口大小重绘的操作。

在用户拖拽窗口时,一直在改变窗口的大小,如果我们在 resize 事件中进行一些操作,消耗将是巨大的。而且大多数可能是无意义的执行,因为用户还处于拖拽的过程中。
可以使用 函数防抖 来优化相关的处理。

// 普通方案
window.addEventListener('resize', () => {
  console.log('trigger');
})

//函数防抖方案
let debounceIdentify = 0;
window.addEventListener('resize', () => {
  debounceIdentify && clearTimeout(debounceIdentify)
  debounceIdentity = setTimeout(() => {
    console.log('trigger')
  }, 300)
})

在 resize 事件中,我们添加了一个 300 ms 的延迟执行逻辑。
并且在每次事件触发时,都会重新计时,这样可以确保函数的执行肯定是在距离上次 resize 事件被触发的 300 ms 后。
两次 resize 事件间隔小于 300 ms 的都被忽略了,这样就会节省很多无意义的事件触发。

搜索联想

function debounce(func, delay) {
  return function(args) {
    var _this = this;
    var _args = args;
    clearTimeout(func.id);
    func.id = setTimeout(function() {
      func.call(_this, _args)
    }, delay)
  }
}

function ajax(value) {
  console.log('ajax request' + value)
}

var debounceAjax = debounce(ajax, 1000);

var input = document.getElementById("search")

input.addEventListener('keyup', function(e) {
  debounceAjax(e.target.value)
})

可以看到当你输入的时候,并不会发送 ajax 请求,当停止并且指定间隔内没有输入的时候,才会执行相应的回调函数。

表单的提交

在一些与用户的交互上,比如提交表单后,一般都会显示一个loading框来提示用户,用户提交的表单正在处理中。
但是发送表单请求后就显示loading是一件很不友好的事情,因为请求可能在几十毫秒内就会得到响应。
这样在用户看来就是页面中闪过一团黑色,所以可以在提交表单后添加一个延迟函数,在XXX秒后再显示loading框。
这样在快速响应的场景下,用户是不会看到一闪而过的loading框,当然,一定要记得在接收到数据后去clearTimeout

let identify = setTimeout(showLoadingModal, 500)
anxios('url').then(res => {
  // doing something

  // clear timer
  clearTimeout(identify);
})

基本版的:

debounce(func, delay) {
     return function(args) {
          var _this = this
          var _args = args
          clearTimeout(func.id)
          func.id = setTimeout(function() {
          func.call(_this, _args)
           }, delay)
    }
}
function debounce(func, wait, leading, trailing) {
  var timer, lastCall = 0, flag = true
  return function() {
    var context = this
    var args = arguments
    var now = + new Date()
    if (now - lastCall < wait) {
      flag = false
      lastCall = now
    } else {
      flag = true
    }
    if (leading && flag) {
      lastCall = now
      return func.apply(context, args)
    }
    if (trailing) {
      clearTimeout(timer)
      timer = setTimeout(function() {
        flag = true
        func.apply(context, args)
      }, wait)
    }
  }
}

Throttle

翻译:
-- n. | 节流阀

throttle就是设置固定的函数执行速率,从而降低频繁事件回调的执行次数。

无论怎么触发,均按照指定的时间间隔来执行。简单地说,就是限制函数在一定时间内调用的次数。
在代码中,可以通过限制函数的调用频率,来抑制资源的消耗。

实际应用场景

  • DOM 元素的拖拽功能的实现(mousemove)
  • 计算鼠标移动的距离(mousemove)
  • canvas 模拟画板功能(mousemove)
  • 监听滚动事件判断是否页面底部自动加载更多,如果是 debounce,则只有在用户停止滚动的时候才会判断是否到了底部,如果是 throttle ,则页面滚动的过程中会间隔判断是否到达底部,此时最好使用 throttle。

1. 需要实现一个元素拖拽的效果,可以在每次 move 事件中进行重绘 DOM,但是这样做,程序的开销是非常大的。

所以这里用到函数节流的方法,来减少重绘的次数。

//普通方案
$dragable.addEventListener('mousemove', () => {
  console.log('trigger')
})

// 函数节流的实现方案

/**
*
* @param fn {Function}   实际要执行的函数
* @param delay {Number}  执行间隔,单位是毫秒(ms)
*
* @return {Function}     返回一个“节流”函数
*/
function throttle(fn, threshold) {
  // 记录上次执行的时间
  var last
  // 定时器
  var timer
  // 默认间隔为 250ms
  threshold || (threshold = 250)
  // 返回的函数,每过 threshold 毫秒就执行一次 fn 函数
  return function () {
    // 保存函数调用时的上下文和参数,传递给 fn
    var context = this
    var args = arguments
    var now = +new Date()
    // 如果距离上次执行 fn 函数的时间小于 threshold,那么就放弃
    // 执行 fn,并重新计时
    if (last && now < last + threshold) {
      clearTimeout(timer)
      // 保证在当前时间区间结束后,再执行一次 fn
      timer = setTimeout(function () {
        last = now
        fn.apply(context, args)
      }, threshold)
    // 在时间区间的最开始和到达指定间隔的时候执行一次 fn
    } else {
      last = now
      fn.apply(context, args)
    }
  }
}

代码中,比较关键的部分是最后部分的if .. else ..,每次回调执行以后,需要保存执行的函数的时间戳,为了计算以后的事件触发回调时与之前执行回调函数的时间戳的间隔,从而根据间隔判断要不要执行回调。

$dragable.addEventListener('mousemove', throttle(function(e) {
	// 代码
}, 500))

这样做的结果是,在拖拽的过程中,可以确保在 500 ms 内,只能重绘一次 DOM。
同时监听了 mousemove,两者最终的结果是一致的,但是在拖拽的过程中,函数节流版触发的事件次数会相对减少很多,相应地资源消耗会更少。

2. 通用的函数节流实现

// ES6 版
function throttle (func, interval) {
  let identify = 0;
  return (...args) => {
    if (identify) return;
    identify = setTimeout(() => identify = 0, interval);
    func.apply(this, args)
  }
}

可视化解释

如果还是对防抖和节流不太明白,可以在下面看到 debounce 和 throttle 可视化区别

image

总结

debounce 强制函数在某段时间内只执行一次,throttle 强制函数以固定的速率执行。在处理一些高频率触发的 DOM 事件的时候,它们都能极大提高用户体验。

参考资料

  1. Javascript debounce vs throttle function
  2. Javascript function debounce and throttle
  3. 函数节流与函数防抖