如何設(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é)苑