优化系列之函数优化 - junruchen/junruchen.github.io GitHub Wiki

目录

  • 节流与防抖
  • 缓存函数
  • 惰性求值

函数节流与防抖

函数节流 throttle

在规定的时间内,只能触发一次函数,如果触发多次函数,则只有一次生效。

常见的应用场景:

  • 监听滚动事件时,如滚动到底部加载更多的需求。
  • 监听鼠标移动事件时或者鼠标不断进行点击时。

简单实现 throttle

主要借助闭包与setTimeout实现。

function throttle(fn, interval = 500) {
    let timeout
    return function () {
        if (timeout) return
        timeout = setTimeout(() => {
            fn.apply(this, arguments)
            timeout = null
        }, interval)
    }
}

应用场景下对比

一个简单的例子,页面上存在一个DOM元素<div id="count">0</div>,鼠标移动时,count值增加1。

未使用防抖时,代码如下:

const countNode = document.querySelector('#count')

window.addEventListener('mousemove', addCount)

function addCount() {
    countNode.innerText++
}

效果图1

如上述效果图所示,addCount函数被频繁的执行,页面数据变化迅速。下面让我们看一下使用节流后的效果。

window.addEventListener('mousemove', throttle(addCount))

效果图2

加上节流之后,函数每隔 500ms 才会去执行一次。

函数防抖 debounce

设定任务触发间隔,只有任务触发的时间超过了指定间隔的时候,任务才会执行一次。

常见的应用场景:

  • 监听resize事件时,如监听resize事件,判断图表是否需要重新绘制以实现页面自适应的效果。
  • 需要监听用户输入,并进行资源请求时,可使用防抖节约请求资源。

简单实现 debounce

原理很简单,就是在设定的间隔后执行。

function debounce(fn, interval = 500) {
    let timeout
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
            fn.apply(this, arguments)
        }, interval)
    }
}

应用场景下对比

页面上存在一个<input id="search" type="text" placeholder="输入后触发搜索">输入框,监听内容变化,实现搜索功能。

未使用防抖时,代码如下:

const searchNode = document.querySelector('#search')

searchNode.addEventListener('input', search)

function search() {
    console.log('ajax--->')
}

效果图1

如上述效果图所示,search函数被频繁的执行,在真实的业务场景中会频繁的触发网络请求,造成资源浪费。下面看一下使用防抖后的效果。

searchNode.addEventListener('input', debounce(search))

效果图2

使用防抖之后,每隔 500ms 才会请求一次网络资源,效果十分明显。

总结

巧妙地使用函数节流与函数防抖,可实现节约资源,提升性能的目的。在实际的开发过程中,可根据应用场景选择防抖或节流。另外在 lodash (文档) 等工具库中也实现了对防抖与节流的支持。

缓存函数

在实际的开发中,经常会遇到计算、属性处理等相关的操作,在未引入缓存的情况下,每一次调用函数都需要重新计算,当函数的开销比较大时,对性能的消耗也是不可小觑的。

缓存函数做的事情简单来说就是将上一次运算的结果放在一个数组或者对象中缓存起来,下次再遇到相同的处理时,直接取出缓存中对应的数据即可,不需要重新进行计算。

简单实现

原理很简单,传入一个原始函数,返回一个新函数,第一个参数作为缓存的key,如果缓存中已经存在直接获取即可。

function cached (fn) {
  const cache = Object.create(null)
  return function cachedFn (...args) {
    // 第一个参数作为key
    const id = args[0]
    const hit = cache[id]
    return hit || (cache[id] = fn.apply(this, args))
  }
}

应用

下面是一个比较常见的常见,将下划线格式的字符串转成小驼峰的格式。

const camelizeRE = /_(\w)/g
const camelize = memorize(str => {
   return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})

// 使用
camelize('test_a') // testA  cache: {'test_a': testA}
camelize('test_b') // testB  cache: {'test_a': testA, 'test_b': testB}
camelize('test_c') // testC  cache: {'test_a': testA, 'test_b': testB, 'test_c': testC}

异步缓存函数

和同步缓存函数类似,区别是函数结果是异步返回的,常见的异步返回方式一:callback, 二:promise

使用callback实现

直接看源码实现

function asyncCached (fn) {
  const cache = Object.create(null)
  return function cachedFn (...args) {
    // 函数的特点,第一个参数作为key, 最后一个是回调函数  
    const id = args[0]
    const cb = args.pop()
    if (cache[id]) {
      return cb(null, cache[id])
    }
    args.push((err, res) => {
      if (!err) cache[id] = res
      cb(err, res)
    })
    fn.apply(this, args)
  }
}

应用

获取图片的大小

const getImgSize = asyncCached((url, cb) => {
  var image = new Image()
  image.onload = function (e) {
    cb(null, {
      width: e.target.width,
      height: e.target.height
    })
  }
  image.onerror = function (e) {
    cb(e)
  }
  image.src = url
})

// 使用
getImgSize('abc.png', (err, res) => {})

使用promise实现

直接看源码实现

function promiseCached (fn) {
  const cache = Object.create(null)
  return async function cachedFn (...args) {
    const id = args[0]
    const hit = cache[id]
    return hit || (cache[id] = await fn.apply(this, args))
  }
}

应用

获取图片的大小

const getImgSize = promiseCached((url) => new Promise((resolve, reject) => {
  var image = new Image()
  image.onload = function (e) {
    resolve({
      width: e.target.width,
      height: e.target.height
    })
  }
  image.onerror = reject
  image.src = url
}))

// 使用
getImgSize('abc.png').then(({width, height}) => {})

惰性求值

惰性求值(Lazy evaluation)是在需要时才进行求值的计算方式。 惰性求值在工作中还是比较常见的,如Vue的computed,只有依赖的状态变化时才会重新计算;ES6 的 generator 通过next天然支持惰性求值。 今天要讲的是如何在数组中使用惰性求值来优化代码。

举例:从一个用户数组中找到前5个以wang开头的用户名列表 list = [{name: 'wanglei'}, {name: 'lisi'}, {name: 'wangjialei'}, ...]

常规命令式

命令式代码不够直观,容易出错

var res = []
for (let i = 0; i < list.length; i++) {
  var name = list[i].name;
  if (name.startsWith('wang')) {
    res.push(name)
    if (res.length === 5){
      break;
    }
  }
}

函数式

函数式代码直观清楚,但重复循环导致性能下降

list.map(item => item.name)
    .filter(name => name.startsWith('wang'))
    .slice(0, 5)

函数惰性求值

函数式且避免重复循环,这里使用的是一些开源库

// https://github.com/dtao/lazy.js
var result = Lazy(list)
  .pluck('name')
  .filter(name => name.startsWith('wang'))
  .take(5);

// https://github.com/ReactiveX/rxjs
Rx
  .Observable
  .from(list)
  .pluck('name')
  .filter(name => name.startsWith('wang'))
  .take(5)
  .subscribe(value => console.log(value));

pluck,filter 这些函数只有在take取值时才会执行,从而达到优化的目的

惰性求值是怎么实现的

下面是简单实现,不能用在生产环境上,生产环境建议使用一些开源库

// 过滤掉数据的标志
const INVALID_SYMBOL = Symbol()
// 结束的标志
const END_SYMBOL = Symbol()
class Sequence {
  constructor (prev, handle, data) {
    // 上一个操作符
    this.prev = prev
    // 操作数据的函数
    this.handle = handle
    // 原始数据源
    this.data = data
  }

  pluck (name) {
    return this.map(item => item[name])
  }

  filter (cb) {
    return new FilterSequence(this, cb)
  }

  map (cb) {
    return new MapSequence(this, cb)
  }

  take (num) {
    return new TakeSequence(this)._take(num)
  }

  /**
   * 获取数据
   * @param {*} item
   * @private
   */
  _value (item) {
    const sequence = this.prev
    // 原始数据源取值,item是下标
    if (!sequence) {
      if (item < this.data.length) {
        return this.data[item]
      }
      return END_SYMBOL
    }

    let newValue = sequence._value(item)
    if (newValue === INVALID_SYMBOL || newValue === END_SYMBOL) {
      return newValue
    }
    if (sequence instanceof FilterSequence) {
      return sequence._filter(newValue)
    } else if (sequence instanceof MapSequence) {
      return sequence._map(newValue)
    }
    return newValue
  }
}

class FilterSequence extends Sequence {
  _filter (value) {
    if (this.handle(value)) {
      return value
    } else {
      return INVALID_SYMBOL
    }
  }
}
class MapSequence extends Sequence {
  _map (value) {
    return this.handle(value)
  }
}
class TakeSequence extends Sequence {
  _take (num) {
    let res = []
    let i = 0
    while (res.length < num) {
      const val = this._value(i++)
      if (val === END_SYMBOL) {
        break
      } else if (val !== INVALID_SYMBOL) {
        res.push(val)
      }
    }
    return res
  }
}

function Lazy (list) {
  return new Sequence(null, null, list)
}


// 使用
let list = [{ name: 'wanglei' }, { name: 'lisi' }, { name: 'wangjialei' }]

let res = Lazy(list)
  .pluck('name')
  .filter(name => name.startsWith('wang'))
  .map(name => name + '@')
  .take(3)

console.log(res)
⚠️ **GitHub.com Fallback** ⚠️