如何設(shè)計一個緩存函數(shù)

在項目中你有優(yōu)化過自己寫過的代碼嗎?或者在你的項目中,你有用過哪些技巧優(yōu)化你的代碼,比如常用的函數(shù)防抖、節(jié)流,或者異步懶加載、惰性加載等。

今天一起學(xué)習(xí)一下如何利用函數(shù)緩存優(yōu)化你的業(yè)務(wù)項目代碼。

正文開始...

初始化一個基礎(chǔ)項目
我們還是快速初始化一個項目

npm init -y
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
然后新建webpack.config.js并且配置對應(yīng)的內(nèi)容

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
  entry: {
    app: './src/index.js',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',
    })
  ],
}
然后新建index.html

<!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>緩存函數(shù)</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
對應(yīng)的src/index.js

const appDom = document.getElementById('app');
console.log('hello');
appDom.innerText = 'hello webpack';
對應(yīng)package.json配置執(zhí)行腳本命令

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start:dev": "webpack serve --mode development",
    "build": "webpack --config ./webpack.config.js --mode production"
  }
}
執(zhí)行npm run start:dev,瀏覽器打開http://localhost:8080

至此這個前端的簡單應(yīng)用已經(jīng)ok了
現(xiàn)在頁面我需要一個需求,我要在頁面中插入1000條數(shù)據(jù)

分時函數(shù)
在這之前我們使用過一個分時函數(shù)思想來優(yōu)化加載數(shù)據(jù)

現(xiàn)在我們把這個分時函數(shù)寫成一個工具函數(shù)

// utils/timerChunks.js
// 分時函數(shù)
module.exports = (sourceArr = [], callback, count = 1, wait = 200) => {
  let ret, timer = null;
  const renderData = () => {
    for (let i = 0; i < Math.min(count, sourceArr.length); i++) {
      // 取出數(shù)據(jù)
      ret = sourceArr.shift();
      callback(ret);
    }
  }
  return () => {
    if (!timer) {
      // 利用定時器每隔200ms取出數(shù)據(jù)
      timer = setInterval(() => {
        // 如果數(shù)據(jù)取完了,就清空定時器
        if (sourceArr.length === 0) {
          clearInterval(timer);
          ret = null;
          return;
        }
        renderData();
      }, wait)
    }
  }
}
由于代碼中使用了es6,因此還需要配置babel-loader將es6轉(zhuǎn)換成es5

npm i @babel/core @babel/cli @babel/preset-env babel-loader --save-dev
以上幾個通常是babel需要安裝的,修改下的webpack.config.js的module.rules

{
  ...
   module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/env'] // 設(shè)置預(yù)設(shè),這個會把es6轉(zhuǎn)換成es5
            }
          }
        ]
      }
    ]
  },
}
我們修改下index.js

const timerChunk = require('./utils/timerChunk');
class renderApp {
  constructor(dom) {
    this.dom = dom;
    this.sourceArr = [];
    this.appDom = new WeakMap().set(dom, dom);
  }
  init() {
    this.createData();
    // 頁面創(chuàng)建div,然后為div內(nèi)容賦值
    this.createElem('hello webpack');
    const curentRender = this.render();
    curentRender();
  }
  createData() {
    const arr = [], max = 100;
    for (let i = 0; i < max; i++) {
      arr.push(i)
    }
    this.sourceArr = arr;
  }
  createElem(res) {
    const divDom = document.createElement('div');
    divDom.innerText = res;
    this.appDom.get(this.dom).appendChild(divDom);
  }
  render() {
    const { sourceArr } = this;
    return timerChunk(sourceArr, (res) => {
      this.createElem(res);
    })
  }
}
new renderApp(document.getElementById('app')).init();
ok,我們看下頁面

好像以上代碼沒有什么可以優(yōu)化的了,并且渲染大數(shù)據(jù)做了分時函數(shù)處理。

并且我們可以測試一下代碼運(yùn)行的時間

console.time('start');
const timerChunk = require('./utils/timerChunk');
...
new renderApp(document.getElementById('app')).init();
console.timeEnd('start');
瀏覽器打印出來的大概是:start: 1.07177734375 ms

memorize 緩存函數(shù)
緩存函數(shù)其實(shí)就是當(dāng)我們第二次加載的時,我們會從緩存對象中獲取函數(shù),這是一個常用的優(yōu)化手段,在webpack源碼中也有大量的這樣的緩存函數(shù)處理






首先我們創(chuàng)建一個memorize工具函數(shù)

// utils/memorize.js

/**
 * @desption 緩存函數(shù)
 * @param {*} callback
 * @returns
 */
export const memorize = callback => {
  let cache = false;
  let result = null;
  return () => {
    // 如果緩存標(biāo)識存在,則直接返回緩存的結(jié)果
    if (cache) {
      return result;
    } else {
      // 將執(zhí)行的回調(diào)函數(shù)賦值給結(jié)果
      result = callback();
      // 把緩存開關(guān)打開
      cache = true;
      // 清除傳入的回調(diào)函數(shù)
      callback = null;
      return result;
    }
  }
}
/**
 * 懶加載可執(zhí)行函數(shù)
 * @param {*} factory
 * @returns
 */
export const lazyFunction = (factory) => {
  const fac = memorize(factory);
  const f = (...args) => fac()(...args);
  return f;
}
我們在index.js中修改下代碼

console.time('start');
const { lazyFunction } = require('./utils/memorize.js');
// const timerChunk = require('./utils/timerChunk.js')
const timerChunk = lazyFunction(() => require('./utils/timerChunk.js'));
...
new renderApp(document.getElementById('app')).init();
console.timeEnd('start');
我們看下測試結(jié)果,控制臺上打印時間是start: 0.72607421875 ms



因此時間上確實(shí)是要小了不少。

那為什么memorize這個工具函數(shù)可以優(yōu)化程序的性能

當(dāng)我們看到這段代碼是不是感覺很熟悉

export const memorize = callback => {
  let cache = false;
  let result = null;
  return () => {
    // 如果緩存標(biāo)識存在,則直接返回緩存的結(jié)果
    if (cache) {
      return result;
    } else {
      // 將執(zhí)行的回調(diào)函數(shù)賦值給結(jié)果
      result = callback();
      // 把緩存開關(guān)打開
      cache = true;
      // 清除傳入的回調(diào)函數(shù)
      callback = null;
      return result;
    }
  }
}
沒錯,本質(zhì)上就是利用閉包緩存了回調(diào)函數(shù)的結(jié)果,當(dāng)?shù)诙卧俅螆?zhí)行時,我們用了一個cache開關(guān)的標(biāo)識直接返回上次緩存的結(jié)果。并且我們手動執(zhí)行回調(diào)函數(shù)后,我們手動釋放了callback。

并且我們使用了一個lazyFunction的方法,實(shí)際上是進(jìn)一步包了一層,我們將同步引入的代碼,通過可執(zhí)行回調(diào)函數(shù)去處理。

所以你看到的這行代碼,lazyFunction傳入了一個函數(shù)

const { lazyFunction } = require('./utils/memorize.js');
// const timerChunk = require('./utils/timerChunk.js')
const timerChunk = lazyFunction(() => require('./utils/timerChunk.js'));
實(shí)際上你也可以不需要這么做,因為timerChunk.js本身就是一個函數(shù),memorize只要保證傳入的形參是一個函數(shù)就行

所以以下也是等價的,你也可以像下面這樣使用

console.time('start');
const { lazyFunction, memorize } = require('./utils/memorize.js');
const timerChunk = memorize(() => require('./utils/timerChunk.js'))();
...
為此這樣的一個memorize的函數(shù)就可以當(dāng)成業(yè)務(wù)代碼的一個通用的工具來使用了

深拷貝對象
我們再來看另外一個例子,深拷貝對象,這是一個業(yè)務(wù)代碼經(jīng)常有用的一個函數(shù),我們可以用memorize來優(yōu)化,在webpack源碼中合并內(nèi)部plugins、chunks處理啊,參考webpack.js[1],等等都有用這個memorize,具體我們寫個簡單的例子感受一下

在utils目錄下新建merge.js

// utils/merge.js
const { memorize } = require('./memorize');
/**
 * @desption 判斷基礎(chǔ)數(shù)據(jù)類型以及引用數(shù)據(jù)類型,替代typeof
 * @param {*} val
 * @returns
 */
export const isType = (val) => {
  return (type) => {
    return Object.prototype.toString.call(val) === `[object ${type}]`
  }
}
/**
 * @desption 深拷貝一個對象
 * @param {*} obj
 * @param {*} targets
 */
export const mergeDeep = (obj, targets) => {
  const descriptors = Object.getOwnPropertyDescriptors(targets);
  // todo 針對不同的數(shù)據(jù)類型做value處理
  const helpFn = val => {
    if (isType(val)('String')) {
      return val;
    }
    if (isType(val)('Array')) {
      const ret = [];
      // todo 輔助函數(shù),遞歸數(shù)組內(nèi)部, 這里遞歸可以考慮用分時函數(shù)來代替優(yōu)化
      const loopFn = (val) => {
        val.forEach(item => {
          if (isType(item)('Object')) {
            ret.push(auxiFn(item))
          } else if (isType(item)('Array')) {
            loopFn(item)
          } else {
            ret.push(item)
          }
        });
      }
      loopFn(val);
      return ret;
    }
    if (isType(val)('Object')) {
      return Object.assign(Object.create({}), val)
    }
  }
  for (const name of Object.keys(descriptors)) {
    // todo 根據(jù)name取出對象屬性的每個descriptor
    let descriptor = descriptors[name];
    if (descriptor.get) {
      const fn = descriptor.get;
      Object.defineProperty(obj, name, {
        configurable: false,
        enumerable: true,
        writable: true,
        get: memorize(fn), // 參考https://github.com/webpack/webpack/blob/main/lib/index.js
      })
    } else {
      Object.defineProperty(obj, name, {
        value: helpFn(descriptor.value),
        writable: true,
      })
    }

  }
  return obj
}
在index.js中引入這個merge.js,對于的source.js數(shù)據(jù)如下

// source.js
export const sourceObj = {
  name: 'Maic',
  public: '公眾號:Web技術(shù)學(xué)苑',
  children: [
    {
      title: 'web技術(shù)',
      children: [
        {
          title: 'js'
        },
        {
          title: '框架'
        },
        {
          title: '算法'
        },
        {
          title: 'TS'
        },
      ]
    },
    {
      title: '工程化',
      children: [
        {
          title: 'webpack'
        }
      ]
    },
  ],
}
index.js

const { mergeDeep } = require('./utils/merge.js');
import { sourceObj } from './utils/source.js'
...
console.log(sourceObj, 'start--sourceObj')
const cacheSource = mergeDeep({}, sourceObj);
cacheSource.public = '122';
cacheSource.children[0].title = 'web技術(shù)2'
console.log(cacheSource, 'end--cacheSource')
我們可以觀察出前后數(shù)據(jù)修改的變化


因此一個簡單的深拷貝就已經(jīng)完成了

總結(jié)
使用memorize緩存函數(shù)優(yōu)化代碼,本質(zhì)緩存函數(shù)就是巧用閉包特性,當(dāng)我們首次加載回調(diào)函數(shù)時,我們會緩存其回調(diào)函數(shù)并會設(shè)置一個開關(guān)記錄已經(jīng)緩存,當(dāng)再次使用時,我們會直接從緩存中獲取函數(shù)。在業(yè)務(wù)代碼中可以考慮緩存函數(shù)思想優(yōu)化以往寫過的代碼

利用緩存函數(shù)在對象攔截中使用memorize優(yōu)化,主要參考webpack源碼合并多個對象

寫了一個簡單的深拷貝,主要是helpFn這個方法對不同數(shù)據(jù)類型的處理

本文示例code-example[2]

最后,看完覺得有收獲的,點(diǎn)個贊,在看,轉(zhuǎn)發(fā),收藏等于學(xué)會,歡迎關(guān)注Web技術(shù)學(xué)苑,好好學(xué)習(xí),天天向上!

參考資料
[1]
webpack.js: https://github.com/webpack/webpack/blob/main/lib/index.js

[2]
code-example: https://github.com/maicFir/lessonNote/tree/master/javascript/13-緩存函數(shù)






作者:Maic

歡迎關(guān)注微信公眾號 :web技術(shù)學(xué)苑