备受“冷落”的享元模式(上)

前言

设计模式有很多:策略模式、代理模式、装饰器模式、迭代器模式、观察者模式、发布订阅模式等等,他们都是用来优化屎山代码的得力助手,并且广泛运用于日常开发中;但是有一个设计模式恰好相反,很少听说,而却使用不当甚至会增添屎山代码,那就是享元模式,享元模式的目的与其他设计模式不同,它不是为了优化代码,而是为了性能优化,因此常常备受冷落,日常业务中很少使用得到;

然而,在面试中总会被提一嘴,这就最让人苦恼了,用又用不到考又要考,那就让他变成八股文背诵吧;这种想法也没有错,但是本文想带大家实际地感受一下享元模式的存在;

大脑中先忘记所有的设计模式>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

来看一看下面具体的场景,看大家能不能够想到用的是哪种设计模式。

线程池

在大文件分片上传的需求里面,一般前端需要读取文件内容然后计算MD5戳,这个过程是非常耗费CPU的,因此它比较适合放在Worker中执行,执行完之后通知到主线程就行了;当上传的文件多起来了,就需要反复地创建和销毁Worker,如果一不小心某些Worker没有被销毁掉,它们就会永驻于内存,导致内存泄漏;为了避免这种情况一般就可以用线程池来管理线程,可以设定一个最大线程数,当某个线程执行完任务之后就将其回收到线程池中,如果需要启动一个新任务就从线程池拿出一个Worker来执行它,这样既能避免反复创建和销毁带来的性能损耗,也能避免销毁不当带来的意外的内存泄漏;

线程池常用的库:threads,它可以跨平台使用,在浏览器、Nodejs、Electron里面都可以使用,因为Worker本身也是可以在js和nodejs两种环境中运行的;

使用Pool方法创建线程池:

const pool = Pool(() => spawn(new Worker("./worker.js")), { size: WORKER_COUNT });

// 当需要执行任务时:
await pool.queue((worker) => worker.calculateHash());

// 页面卸载时卸载Worker
window.addEventListener("beforeunload", async () => { await pool.completed(); await pool.terminate(); });

对象池

对象池,一般用来复用某一个对象,例如DOM对象。比如说需要反复创建div元素,那么就可以把div元素放到对象池中,当需要append到页面上的时候修改其内容然后append到页面,最后放回到对象池,需要用的时候又拿出来;

并非所有对象都适合拿来池化――因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。但是对于生成时开销可观的对象,池化技术就是提高性能的有效策略了。

对象池一般用在创建dom对象上面,比方说下面这个例子中需要反复创建iframe,就比较适合使用对象池来缓存iframe对象,然后再添加到dom对象上:

var objectPoolFactory = function(createObjFn) {
    var objectPool = [];
 
    return {
        create: function() {
            var obj = objectPool.length === 0 ?
                createObjFn.apply(this, arguments) : objectPool.shift();
            return obj;
        },
        recover: function(obj) {
            objectPool.push(obj);
        }
    }
};
 
// test
var iframeFactory = objectPoolFactory(function() {
    var iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
 
    iframe.onload = function() {
        iframe.onload = null;   // 防止iframe重复加载
        iframeFactory.recover(iframe); // iframe加载完了回收节点
    }
    return iframe;
});
 
var iframe1 = iframeFactory.create();
iframe1.src = 'https://www.baidu.com';
 
var iframe2 = iframeFactory.create();
iframe2.src = 'http://www.qq.com/';
 
setTimeout(function() {
    var iframe3 = iframeFactory.create();
    iframe3.src = 'http://www.163.com';
}, 750);

对象池在实践层面较少出现,只需要了解其存在就可以了;

事件池

事件池出现于React源码中,React本身模拟了一套事件机制,所以不可避免地React需要创建一个新的事件对象,而前端应用中绑定的事件是非常多的,那么就涉及到事件对象的反复创建和销毁,会造成一些内存开销,于是React就实现了事件池来管理事件对象,当然在React17版本已经删除了事件池,但是这并不妨碍大家学习一下它;

备受“冷落”的享元模式(上)

这是我从React源码摘录出的一段代码,里面的instance就是需要被复用的事件对象,从代码中可以看到如果事件池中有事件对象那么就从事件池中取出,如果没有才会新建事件对象,这样的确能够优化那么一点点内存,但是它是有副作用的;

如果在事件中延时去打印事件对象,这个时候事件对象会是null,因为它已经被回收了;这是一个反面的案例,为了追求性能而造成的bug;

享元模式

前面的池化思想其实是一个设计模式的体现,这个设计模式就是享元模式;享元模式将状态划分为内部状态和外部状态,内部状态是可以共享的状态,而外部状态可以暴露一些方法以修改,是每个实例之间不同的一些属性;比如说上面的iframe复用的案例,它的外部状态是iframe的src属性,其他的状态都可以共享;

说到设计模式,都有一个通病,就像童话故事一样好像离大家都很遥远,理论大家都懂但是又接触不到,或者说实际上大家都用不上,这样的话就变成了八股文毫无意义;享元模式其实就是这样一个设计模式,在框架源代码中可能会依稀可见,但是在日常代码中一般不会见到;

享元模式的目的是性能优化,而性能优化一般需要指标来衡量,但是在业务开发的过程中也不可能先去测算一下指标再去做,所以要慎用享元模式,防止为了优化而优化;

结语

本文只提到了线程池、对象池、事件池,如果大家有其他的池化实践可以在下方评论区分享;

如果觉得文章不错,请关注、点赞、收藏来个一键三连;

如果有不同的意见也欢迎一起讨论;

原文链接:https://juejin.cn/post/7242911173723979833 作者:蚂小蚁

(0)
上一篇 2023年6月11日 上午10:46
下一篇 2023年6月11日 上午10:56

相关推荐

发表回复

登录后才能评论