按需加载的一些方法

2020-11-14codinglazy loading

​ 当一个产品服务的客户足够多时,就一定有一些功能是只服务部分用户的。因此对不使用此功能的用户而言,加载这部分的代码并执行是完全不必要的事情。按需加载是一种很优雅的解决方案。

​ 在此先不讨论图片等素材的按需加载,仅仅拿代码中的按需加载为例。

按需加载

​ 试想一个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。
// 以此来将异步的调用再次转换为同步的使用方法。

​ 如果在最底层的实现都是按需加载,能否在顶层实现一个按需加载的应用呢?

​ 当然加载动画也是需要的,每点击一个功能都要进行加载的话自然也很不方便,有没有什么方便进行预加载的手段呢?

The end