当一个产品服务的客户足够多时,就一定有一些功能是只服务部分用户的。因此对不使用此功能的用户而言,加载这部分的代码并执行是完全不必要的事情。按需加载是一种很优雅的解决方案。
在此先不讨论图片等素材的按需加载,仅仅拿代码中的按需加载为例。
按需加载
试想一个Loader类型,开发者可以自行生成一个示例。如果想要内置就生成一个实例以方便使用,如何实现更好呢?
使用getter
// 以下代码参考自[pixi.js](https://pixijs.download/dev/docs/PIXI.Loader.html)
class Loader {
constructor() {
}
static get shared() {
let shared = Loader._shared;
if (!shared) {
shared = new Loader();
Loader._shared = shared;
}
return shared;
}
}
Loader.shared // Loader {}
这样可以实现在访问shared的初次,才生成对应的实例,后续访问直接返回该实例。
使用Object.defineProperty
在访问时会生成实例,但getter的访问效率不及键值对象。有没有办法在getter生成后直接把对应的键值赋值到此对象上呢?Object.defineProperty能很好地实现。
/**
* {obj} - 对象
* {key} - 对象的键值
* {initValue} - 生成键值的函数,返回值为作为键key对应的value
*/
function lazy(obj, key, initValue) {
let getValue = function() {
let v = initValue.apply(this);
Object.defineProperty(obj, key, {
writable: true,
enumerable: true,
configurable: true,
value: v,
}); // 把value直接更新,取消掉get
getValue = null;
return v;
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: getValue,
set(v) {
throw new Error('lazy value cannot be assigned');
}
})
};
class Loader {
constructor() {
}
}
lazy(Loader, 'shared', () => new Loader());
Loader.shared // Loader {}
重定义
Object.defineProperty就是一种对值的重定义。对值进行重定义这种思想有没有可能在其他方面应用呢?
比如说当一个函数执行返回的结果是异步的时候,有没有可能直接将这个函数给按需加载。
可不可以先把这个函数抽象成一个delay函数,第一次的调用都会让其开始加载这个文件,在没有加载成功前的调用都会按顺序记录调用参数。在加载成功后再执行之前保存的调用参数,并在后续调用时直接执行加载成功后的函数。
当然,此函数也不能有其返回值。因为未加载前不能确定函数是否存在返回值。
示例如下。
// hello.js
window.getValueAsync = function(arg, cb) {
cb('hello');
}
// index.js
const getValueAsync = delay((resolve) => {
loadScript('./hello.js').then(() => {
resolve(window.getValueAsync);
delete window.getValueAsync
});
});
getValueAsync('xxx', (value) => {
console.log(value);
})
getValueAsync('xxx2', (value) => {
console.log(value);
})
function delay(wrapperFunc) {
let delayed = [];
let isResolved = false;
let resolvedFunc = null;
function resolve(resultFunc) { // 加载成功
if (typeof resultFunc !== 'function') throw new Error('resolve function please');
if (isResolved) return;
delayed.forEach(([_this, args]) => {
resultFunc.apply(_this, args);
});
delayed = null;
resolvedFunc = resultFunc;
isResolved = true;
}
return function delayedFunc() {
//在没有resolve时存储参数
// resolve之后做一个转发,保证可以通过同一个之前的变量调用如 getValueAsync
if (isResolved) return resolvedFunc.apply(this, arguments);
// 还没调用过就开始初始化
if (delayed.length === 0) wrapperFunc(resolve);
delayed.push([this, arguments]);
return 'delaying';
}
}
// 也可以把异步callback来改为promise
因此为了按需加载,也可以把先加载再同步执行的代码改写为异步执行,在执行时才加载的方案。
粒度的控制
按需加载就注定需要把加载的各个部分分离成不同的文件,bundleless工具在开发模式下就是最细粒度的按需加载。但在用户使用过程中,受限于网速,不能以最细粒度完成按需加载。
可以将较细粒度的文件分别打包。打包的大小可以自己设置一个定值最大值(如500KB)。
总结
在访问某些网页游戏时,总需要过多的文件加载才能正式进入游戏。因此有一个颇为玩具性质的想法,在使用的时候才开始加载。比如下面拿pixi.js举例:
getSprite('ui/atk.png');
// 在没有加载时 先返回一个Loading Sprite, 加载成功后再去更新Loading Sprite。
// 以此来将异步的调用再次转换为同步的使用方法。
如果在最底层的实现都是按需加载,能否在顶层实现一个按需加载的应用呢?
当然加载动画也是需要的,每点击一个功能都要进行加载的话自然也很不方便,有没有什么方便进行预加载的手段呢?