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