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

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

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

正文開(kāi)始...

初始化一個(gè)基礎(chǔ)項(xiàng)目
我們還是快速初始化一個(gè)項(xiàng)目

npm init -y
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
然后新建webpack.config.js并且配置對(duì)應(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>
對(duì)應(yīng)的src/index.js

const appDom = document.getElementById('app');
console.log('hello');
appDom.innerText = 'hello webpack';
對(duì)應(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,瀏覽器打開(kāi)http://localhost:8080

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

分時(shí)函數(shù)
在這之前我們使用過(guò)一個(gè)分時(shí)函數(shù)思想來(lái)優(yōu)化加載數(shù)據(jù)

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

// utils/timerChunks.js
// 分時(shí)函數(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) {
      // 利用定時(shí)器每隔200ms取出數(shù)據(jù)
      timer = setInterval(() => {
        // 如果數(shù)據(jù)取完了,就清空定時(shí)器
        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
以上幾個(gè)通常是babel需要安裝的,修改下的webpack.config.js的module.rules

{
  ...
   module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/env'] // 設(shè)置預(yù)設(shè),這個(gè)會(huì)把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();
    // 頁(yè)面創(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è)面

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

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

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

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






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

// utils/memorize.js

/**
 * @desption 緩存函數(shù)
 * @param {*} callback
 * @returns
 */
export const memorize = callback => {
  let cache = false;
  let result = null;
  return () => {
    // 如果緩存標(biāo)識(shí)存在,則直接返回緩存的結(jié)果
    if (cache) {
      return result;
    } else {
      // 將執(zhí)行的回調(diào)函數(shù)賦值給結(jié)果
      result = callback();
      // 把緩存開(kāi)關(guān)打開(kāi)
      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;
}
我們?cè)趇ndex.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');
我們看下測(cè)試結(jié)果,控制臺(tái)上打印時(shí)間是start: 0.72607421875 ms



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

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

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

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

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

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

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

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

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

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

在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 深拷貝一個(gè)對(duì)象
 * @param {*} obj
 * @param {*} targets
 */
export const mergeDeep = (obj, targets) => {
  const descriptors = Object.getOwnPropertyDescriptors(targets);
  // todo 針對(duì)不同的數(shù)據(jù)類型做value處理
  const helpFn = val => {
    if (isType(val)('String')) {
      return val;
    }
    if (isType(val)('Array')) {
      const ret = [];
      // todo 輔助函數(shù),遞歸數(shù)組內(nèi)部, 這里遞歸可以考慮用分時(shí)函數(shù)來(lái)代替優(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取出對(duì)象屬性的每個(gè)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中引入這個(gè)merge.js,對(duì)于的source.js數(shù)據(jù)如下

// source.js
export const sourceObj = {
  name: 'Maic',
  public: '公眾號(hào):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ù)修改的變化


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

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

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

寫(xiě)了一個(gè)簡(jiǎn)單的深拷貝,主要是helpFn這個(gè)方法對(duì)不同數(shù)據(jù)類型的處理

本文示例code-example[2]

最后,看完覺(jué)得有收獲的,點(diǎn)個(gè)贊,在看,轉(zhuǎn)發(fā),收藏等于學(xué)會(huì),歡迎關(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)注微信公眾號(hào) :web技術(shù)學(xué)苑