在项目中你有优化过自己写过的代码吗?或者在你的项目中,你有用过哪些技巧优化你的代码,比如常用的函数防抖、节流,或者异步懒加载等。
今天一起学习一下如何利用函数缓存优化你的业务项目代码。
正文开始...
我们还是快速初始化一个项目
npm init -y
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
然后新建webpack.config.js并且配置对应的内容
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>缓存函数</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
对应的src/index.js
const appdom = document.getElementById('app');
console.log('hello');
appDom.innerText = 'hello webpack';
对应package.json配置执行脚本命令
{
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"start:dev": "webpack serve --mode development",
"build": "webpack --config ./webpack.config.js --mode production"
}
}
执行npm run start:dev,浏览器打开http://localhost:8080,至此这个前端的简单应用已经ok了。
现在页面我需要一个需求,我要在页面中插入1000条数据
在这之前我们使用过一个分时函数思想来优化加载数据
现在我们把这个分时函数写成一个工具函数
// utils/timerChunks.js
// 分时函数
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++) {
// 取出数据
ret = sourceArr.shift();
callback(ret);
}
}
return () => {
if (!timer) {
// 利用定时器每隔200ms取出数据
timer = setInterval(() => {
// 如果数据取完了,就清空定时器
if (sourceArr.length === 0) {
clearInterval(timer);
ret = null;
return;
}
renderData();
}, wait)
}
}
}
由于代码中使用了es6,因此还需要配置babel-loader将es6转换成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'] // 设置预设,这个会把es6转换成es5
}
}
]
}
]
},
}
我们修改下index.js
// 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();
// 页面创建div,然后为div内容赋值
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,我们看下页面
好像以上代码没有什么可以优化的了,并且渲染大数据做了分时函数处理。
并且我们可以测试一下代码运行的时间
console.time('start');
const timerChunk = require('./utils/timerChunk');
...
new renderApp(document.getElementById('app')).init();
console.timeEnd('start');
浏览器打印出来的大概是:start: 1.07177734375 ms
缓存函数其实就是当我们第二次加载的时,我们会从缓存对象中获取函数,这是一个常用的优化手段,在webpack源码中也有大量的这样的缓存函数处理
首先我们创建一个memorize工具函数
// utils/memorize.js
/**
* @desption 缓存函数
* @param {*} callback
* @returns
*/
export const memorize = callback => {
let cache = false;
let result = null;
return () => {
// 如果缓存标识存在,则直接返回缓存的结果
if (cache) {
return result;
} else {
// 将执行的回调函数赋值给结果
result = callback();
// 把缓存开关打开
cache = true;
// 清除传入的回调函数
callback = undefined;
return result;
}
}
}
/**
* 懒加载可执行函数
* @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');
我们看下测试结果,控制台上打印时间是start: 0.72607421875 ms
因此时间上确实是要小了不少。
那为什么memorize这个工具函数可以优化程序的性能
当我们看到这段代码是不是感觉很熟悉
export const memorize = callback => {
let cache = false;
let result = null;
return () => {
// 如果缓存标识存在,则直接返回缓存的结果
if (cache) {
return result;
} else {
// 将执行的回调函数赋值给结果
result = callback();
// 把缓存开关打开
cache = true;
// 清除传入的回调函数
callback = null;
return result;
}
}
}
没错,本质上就是利用闭包缓存了回调函数的结果,当第二次再次执行时,我们用了一个cache开关的标识直接返回上次缓存的结果。并且我们手动执行回调函数后,我们手动释放了callback。
并且我们使用了一个lazyFunction的方法,实际上是进一步包了一层,我们将同步引入的代码,通过可执行回调函数去处理。
所以你看到的这行代码,lazyFunction传入了一个函数
const { lazyFunction } = require('./utils/memorize.js');
// const timerChunk = require('./utils/timerChunk.js')
const timerChunk = lazyFunction(() => require('./utils/timerChunk.js'));
实际上你也可以不需要这么做,因为timerChunk.js本身就是一个函数,memorize只要保证传入的形参是一个函数就行
所以以下也是等价的,你也可以像下面这样使用
console.time('start');
const { lazyFunction, memorize } = require('./utils/memorize.js');
const timerChunk = memorize(() => require('./utils/timerChunk.js'))();
...
为此这样的一个memorize的函数就可以当成业务代码的一个通用的工具来使用了
我们再来看另外一个例子,深拷贝对象,这是一个业务代码经常有用的一个函数,我们可以用memorize来优化,在webpack源码中合并内部plugins、chunks处理啊,参考webpack.js,等等都有用这个memorize,具体我们写个简单的例子感受一下
在utils目录下新建merge.js
// utils/merge.js
const { memorize } = require('./memorize');
/**
* @desption 判断基础数据类型以及引用数据类型,替代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 针对不同的数据类型做value处理
const helpFn = val => {
if (isType(val)('String')) {
return val;
}
if (isType(val)('Array')) {
const ret = [];
// todo 辅助函数,递归数组内部, 这里递归可以考虑用分时函数来代替优化
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 根据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数据如下
// source.js
export const sourceObj = {
name: 'Maic',
public: '公众号:Web技术学苑',
children: [
{
title: 'web技术',
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技术2'
console.log(cacheSource, 'end--cacheSource')
我们可以观察出前后数据修改的变化
因此一个简单的深拷贝就已经完成了
来源:Web技术学苑
浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存),强缓存是利用http头中的Expires和Cache-Control两个字段来控制的,用来表示资源的缓存时间。协商缓存就是由服务器来确定缓存资源是否可用.
一个缓存就是一个组件,它可以透明地存储数据,以便未来可以更快地服务于请求。缓存能够服务的请求越多,整体系统性能就提升得越多。
浏览器缓存就是把一个已经请求过的Web资源(如html页面,图片,js,数据等)拷贝一份副本储存在浏览器中,为什么使用缓存:减少网络带宽消耗,降低服务器压力,减少网络延迟,加快页面打开速度
一个H5页面在APP端,如果勾选已读状态,则下次打开该链接,会跳过此页面。用到了HTML5 的本地存储 API 中的 localStorage作为解决方案,回顾了下Web缓存的知识
在描述CDN的实现原理之前,让我们先看传统的未加缓存服务的访问过程,以便了解CDN缓存访问方式与未加缓存访问方式的差别,用户访问未使用CDN缓存网站的过程为:用户向浏览器提供要访问的域名;
页面打开时,由于缓存的存在,刚刚更新的数据有时无法在页面得到刷新,当这个页面作为模式窗口被打开时问题更为明显, 如何将缓存清掉?
通过在Response Header设置Cache-Control head 信息可以控制浏览器的缓存行为。我们先来看一下Cache-Control可以设置哪些值:缓存头Cache-Control只能在服务端设置,在客户端是由浏览器设置的,自己不能修改它的值。
增量更新是目前大部分团队采用的缓存更新方案,能让用户在无感知的情况获取最新内容。具体实现方式通常是(一般我们通过构建工具来实现,比如webpack):
浏览器会默认缓存网站的静态资源文件,如:js文件、css文件、图片等。缓存带来网站性能提升的同时也带来了一些困扰,最常见的问题就是不能及时更新静态资源,造成新版本发布时用户无法及时看到新版本的变化,严重影响了用户体验。
一个后台管理系统,一个列表页A路由配置需要缓存,另一个页面B里面有多个跳转到A路由的链接。问题描述:首先访问/A?id=1页面,然后到B页面再点击访问A?id=2的页面,发现由于页面A设置了缓存,数据还是id=1的数据,并没有更新。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!