一. 代理模式的定义

代理模式的定义:为其他对象提供一种代理,以控制对着这个对象的访问。

在代理模式中,一个对象充当另一个对象的接口。

这种模式看起来像是额外的开销,但是出于性能因素的考虑却是非常有用的。代理充当了本体对象的守护对象,并且试图使本体对象做尽可能少的工作。

二. 代理模式的适用场景

代理模式的适用场景有:

  • 延迟一个大对象的实例化
  • 访问远程对象
  • 访问控制
  • … …

三. 代理模式的实现

在代理模式中,一个对象充当另一个对象的接口,使得本体对象做尽可能少的工作。

/* =============== 本体类 =============== */
var Client = function() {};
Client.prototype = {
    add: function() {
        // 添加功能... ...
    },
    delete: function() {
        // 删除功能... ...
    },
    update: function() {
        // 修改功能... ...
    }
};

/* =============== 代理类 =============== */
var Proxy = function() {
    this.client = new Client();
};
Proxy.prototype = {
    add: function() {
        return this.client.add();
    },
    delete: function() {
        return this.client.delete();
    },
    update: function() {
        return this.client.update();
    }
};

3.1 虚拟代理

假如Client类有很多方法,并且大多数都庞大且复杂,为了实例它会占用很多很多CPU。那当我们需要使用这个对象时才去实例化它不是更好吗?虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建

我们把上面的代码用虚拟代理重构一下:

/* =============== 本体类 =============== */
var Client = function() {};
Client.prototype = {
    add: function() {
        // 添加功能... ...
    },
    delete: function() {
        // 删除功能... ...
    },
    update: function() {
        // 修改功能... ...
    }
};


/* =============== 代理类 =============== */
var Proxy = function() {
    this.client = null;
};
Proxy.prototype = {
    // 在必要的时候才创建实例对象
    _init: function() {
        if (!this.client) {
            this.client = new Client();
        }
    },
    add: function() {
        this._init();
        return this.client.add();
    },
    delete: function() {
        this._init();
        return this.client.delete();
    },
    update: function() {
        this._init();
        return this.client.update();
    }
};

3.2 缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储。在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

例如:

/* =============== 开销大的本体类 =============== */
var calculate = function() {
    var result;

    // 复杂且庞大的计算 .... ...

    return result;
};

/* =============== 缓存代理类 =============== */
var calculateProxy = (function() {
    var cache = {}; // 缓存计算结果

    return function() {
        var args = Array.prototype.join.call(arguments, ",");
        if(args in cache) {
            return cache[args];
        }
        return cache[args] = calculate.apply(this, arguments);
    }
})();

/* =============== 客户端实现 =============== */
calculateProxy(1, 2, 3, 4, 5); // 本体calculate函数被计算,并写入缓存结果
calculateProxy(1, 2, 3, 4, 5); // 本体calculate函数并没有被计算,而是直接返回之前缓存好的计算结果
calculateProxy(1, 2, 3); // 本体calculate函数被计算,并写入缓存结果

通过增加缓存代理的方式,本体calculate函数可以专注于自身的计算职能,而缓存的额功能则由代理对象来实现。

3.3 用高阶函数动态创建代理

通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。这些方法被当作参数传入一个专门用于创建缓存代理的工厂中。这样,我们就可以为加减乘除等创建缓存代理,代码如下:

/********** 计算乘积 **********/
var mult = function() {
    var result = 1;
    for(var i = 0, l = arguments.length; i < l; i++) {
        result = result * arguments[i];
    }
    return result;
};
/********** 计算加和 **********/
var plus = function() {
    var result = 0;
    for(var i = 0, l = arguments.length; i < l; i++) {
        result = result + arguments[i];
    }
    return result;
};

/********** 创建缓存代理的工厂 **********/
var createProxyFactory = function(fn) {
    var cache = {};

    return function() {
       var args = Array.prototype.join.call(arguments, ",");
        if(args in cache) {
            return cache[args];
        }
        return cache[args] = fn.apply(this, arguments); 
    };
};

var multProxy = createProxyFactory(mult);
var plusProxy = createProxyFactory(plus);

/********** 客户端实现 **********/
multProxy(1, 2, 3, 4, 5); // 120
plusProxy(1, 2, 3, 4, 5); // 15

3.4 其他代理模式

代理模式的变种很多,主要有:

  • 远程代理:为一个对象在不同的地址空间提供局部代表。
  • 保护代理:用于控制不同权限的对象对目标对象的访问。
  • 智能引用代理:取代了简单的指针,它在访问对象时执行了一些附加操作,比如计算一个对象被引用的次数。
  • … …

代理模式包括许多小分类,在JavaScript开发中最常用的是虚拟代理和缓存代理。

四. 代理模式的实际应用

4.1 虚拟代理实现图片预加载

在Web开发中,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张图片作为loading图占位,然后用异步的方式加载图片,等图片加载完成后再将其插入img节点中。这种延迟初始化的场景就很适合使用虚拟代理。

引入代理对象proxyImage,通过这个代理对象,在图片被真正加载完成之前,将出现一张占位的菊花图loading.gif,来提示用户图片正在加载。如下:

var myImage = (function() {
    var imgNode = document.createElement("img");
    document.body.appendChild(imgNode);

    return {
        setSrc: function(src) {
            imgNode.src = src;
        }
    };
})();

var proxyImage = (function() {
    var img = new Image;
    img.onload = function() {
        myImage.setSrc(this.src);
    };
    return {
        setSrc: function(src) {
            myImage.setSrc("loading.gif");
            img.src = src;
        }
    };
})();

proxyImage.setSrc("http://127.0.0.1/1.jpg");

备注:该代码摘抄自《JavaScript设计模式与开发实践》第6章P92。

4.2 虚拟代理实现合并HTTP请求

假设在做一个标签管理的功能时,当点击标签删除按钮,该对应的标签就会对服务器进行标签删除的网络请求。当在短时间内点击多次标签删除按钮,可以预见,如此频繁的网络请求将会带来相当大的开销。

解决方案是:我们可以收集一段时间内的请求,最后一次性发送给服务器。比如等待2秒钟之后,才把这2秒之内需要删除的标签打包发送给服务器。

/* ============== 删除标签的本体类 ============== */
var deleteTag = function(tagName) {
    // 删除标签的网络请求与功能实现
    // ... ...
}

/* ============== 删除标签的代理类 ============== */
var deleteTagProxy = (function() {
    var cache = [], // 保存一段时间需要删除的标签名
        timer; // 定时器

    return function(tagName) {
        cache.push(tagName);
        if(timer) { // 保证不会覆盖已经启动的定时器
            return;
        }

        // 2s后向本体发送需要同步的标签名集合
        timer = setTimeout(function() {
            deleteTag(cache.join(","));

            // 清空定时器
            clearTimeout(timer);
            timer = null;

            // 清空标签名集合
            cache = [];
        }, 2000);
    };
})();

/* ============== 删除标签的交互实现 ============== */
/* 
 * 标签删除按钮的DOM结构为:<div class="btn-delete-tag" data-tagName="my-tag"></div>
 */
var deleteTagBtn = document.getElementByClassName("btn-delete-tag");

deleteTagBtn.forEach(function(element, index) {
    element.addEventListener("click", function() {

        deleteTagProxy(this.dataSet.tagName);

    }, false);
});

4.3 缓存代理用于ajax异步请求数据

在项目中常常会遇到分页的需求。同一页的数据理论上只需要去后台拉去一次。这些已经拉取好的数据在某个地方被缓存之后,下次再请求同一页时,便可以直接从缓存中读取数据。

这里适合使用缓存代理模式。

/* =============== ajax工具函数 =============== */
function ajax(options) {
    options = options || {};
    options.type = (options.type || "GET").toUpperCase();
    options.dataType = options.dataType || "json";
    var params = formatParams(options.data);

    //创建XMLHttpRequest
    if (window.XMLHttpRequest) { // IE6+及现代浏览器
        var xhr = new XMLHttpRequest();
    } else { //IE6及其以下版本浏览器
        var xhr = new ActiveXObject('Microsoft.XMLHTTP');
    }

    // 接收数据
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4) {
            var status = xhr.status;
            if (status >= 200 && status < 300) {
                options.success && options.success(xhr.responseText, xhr.responseXML);
            } else {
                options.fail && options.fail(status);
            }
        }
    }

    // 连接和发送数据
    if (options.type == "GET") {
        xhr.open("GET", options.url + "?" + params, true);
        xhr.send(null);
    } else if (options.type == "POST") {
        xhr.open("POST", options.url, true);
        xhr.send(params);
    }
}

//格式化参数
function formatParams(data) {
    var arr = [];
    for (var name in data) {
        arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));
    }
    arr.push(("v=" + Math.random()).replace(".",""));
    return arr.join("&");
}

/* =============== ajax异步请求分页数据 =============== */
var getPageContext = function(pageId) {
    var result;
    ajax({
        url: "/test/index.php",
        method: "get",
        dataType: "json",
        data: {
            id: pageId
        },
        success: function(response) {
             result = response;
        }
    });
    return result;
};

/* =============== 缓存代理类 =============== */
var getPageContextProxy = (function() {
    var cache = {}; // 缓存计算结果

    return function() {
        var args = Array.prototype.join.call(arguments, ",");
        if(args in cache) {
            return cache[args];
        }
        return cache[args] = getPageContext.apply(this, arguments);
    }
})();

/* =============== 客户端实现 =============== */
getPageContextProxy(1); // 向服务器请求第1页数据
getPageContextProxy(2); // 向服务器请求第2页数据
getPageContextProxy(1); // 从缓存中读取第1页数据

五. 总结

在JavaScript开发中最常用的是虚拟代理和缓存代理。

虽然代理模式很有用,但是在实际业务开发中,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象时,再编写代理也不迟。

本文作者:子匠_Zijor,转载请注明出处:http://www.dengzhr.com/js/1132