设计模式之代理模式在前端的应用

780 阅读5分钟

image.png

代理模式

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

在面向对象的编程中,代理模式的合理使用能够很好的体现下面两条原则:

  • 单一职责原则: 面向对象设计中鼓励将不同的职责分布到细粒度的对象中,Proxy 在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。
  • 开放-封闭原则:代理可以随时从程序中去掉,而不用对其他部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种原因不再需要代理,那么就可以容易的将代理对象换成原对象的调用

ES6中的代理模式

ES6所提供Proxy构造函数能够让我们轻松的使用代理模式。

var proxy = new Proxy(target, handler);

Proxy构造函数传入两个参数,第一个参数target表示所要代理的对象,第二个参数handler也是一个对象用来设置对所代理的对象的行为。

利用Proxy实现前端中3种代理模式的使用场景,分别是:缓存代理验证代理实现私有属性,事件代理,代理图片预加载

缓存代理

缓存代理可以将一些开销很大的方法的运算结果进行缓存,再次调用该函数时,若参数一致,则可以直接返回缓存中的结果,而不用再重新进行运算。

const getFib = (number) => {
  if (number <= 2) {
    return 1;
  } else {
    return getFib(number - 1) + getFib(number - 2);
  }
}

使用缓存代理的工厂函数:

const getFib = (number) => {
    if (number <= 2) {
      return 1;
    } else {
      return getFib(number - 1) + getFib(number - 2);
    }
  }

function getCacheProxy(fn, map){
    return new Proxy(fn, {
        apply: (target, context, args)=> {
            const argString = args.join(' ');

            if(map.has(argString)) {
                return map.get(argString);
            }

            let res = Reflect.apply(target, context, args);
            map.set(argString, res);

            return res;
        }
    });
}

// 使用
let map = new Map();
let proxyFib = getCacheProxy(getFib, map);
let res1 = proxyFib(10);
let re2 = proxyFib(10);

当我们第二次调用getFibProxy(40)时,getFib函数并没有被调用,而是直接从cache中返回了之前被缓存好的计算结果。通过加入缓存代理的方式,getFib只需要专注于自己计算斐波那契数列的职责,缓存的功能使由Proxy对象实现的。这实现了我们之前提到的单一职责原则

验证代理

Proxy构造函数第二个参数中的set方法,可以很方便的验证向一个对象的传值。我们以一个传统的登陆表单举例,该表单对象有两个属性,分别是account和password,每个属性值都有一个简单和其属性名对应的验证方法,验证规则如下:

// 表单对象
const userForm = {
    account: '',
    password: '',
};

// 验证方法
const validators = {
    account(value) {
        // account 只允许为中文    
        const re = /^[\u4e00-\u9fa5]+$/;
        return {
            valid: re.test(value),
            error: '"account" is only allowed to be Chinese'    
        }
    },
    password(value) {
        // password 的长度应该大于6个字符    
        return {
            valid: value.length >= 6,
            error: '"password" should more than 6 character'    
        };
    }
};

使用Proxy实现一个通用的表单验证器:

const getValidateProxy = (target, validators) => {
    return new Proxy(target, {
        set(target, key, value, receiver){
            if(!value) {
                console.error(`"${key}" is not allowed to be empty`);
                return target[key] = false;
            }

            const validResult = validators[key](value);
            if(validResult.valid) {
                const res = Reflect.set(target, key, value, receiver);
                return res;
            }
            else {
                console.error(validResult.error);
                return target[key] = false;
            }
        }
    });
}

let vp = getValidateProxy(userForm, validators);

vp.password = '123';
vp.account = 'david';

实现私有属性

代理模式还有一个很重要的应用是实现访问限制。总所周知,JavaScript是没有私有属性这一个概念的,通常私有属性的实现是通过函数作用域中变量实现的,虽然实现了私有属性,但对于可读性来说并不好。

私有属性一般是以_下划线开头,但ES6中可以使用#来实现私有属性。

function getPrivateProps(obj, isPrivate){
    return new Proxy(obj, {
        get(target, key, receiver){
            if(!isPrivate(key)) {
                let res = Reflect.get(target, key, receiver);

                if(typeof res === 'function') {
                    res = res.bind(obj);
                }

                return res;
            }
        },
        set(target, key, value, receiver){
            if(isPrivate(key)) {
                throw new TypeError('Cannot set ' + key);
            }

            return Reflect.set(target, key, value, receiver);
        },
        has(target, key, receiver){
            return isPrivate(prop) ? false : Reflect.has(target, key, receiver);
        },
        getOwnPropertyDescriptor(target, key, receiver){
            return isPrivate(prop) ? undefined : Reflect.getOwnPropertyDescriptor(target, key, receiver);
        }
    });
}

function isPrivate(key){
    return key.startsWith('_');
}

const myObj = {
    public: 'hello',
    _privat1: 'secret1',
    _privat2: 'secret2',
    output: function () {
      console.log(this._private1);
    },
    _print: function () {
        console.log(this._private2);
    }
};

const proxy = getPrivateProps(myObj, isPrivate);
console.log(proxy.public);
console.log(proxy._privat1);
proxy._privat1 = 'Hello';
proxy._print();

事件代理

HTML元 素事件代理。

<ul id="ul">
  <div>111</div>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

<script>
  let ul = document.querySelector('#ul');
	ul.addEventListener('click', event => {
 		console.log(event.target);
	});
</script>

react 的事件机制利用了事件委托机制。事件并没有绑定在真实的 dom 节点上,而是把事件都绑定在结构的最外层 document,使用一个统一的事件监听器。所有的事件都由这个监听器统一分发。这样的事件机制简单而又高效。

虚拟代理实现图片预加载

导致图片出来前会有一片空白。所以我们限用一张 loading 图片占位,在异步方式加载图片。

不用代理:

// 创建一个本体对象
var myImage = (function(){
  // 创建标签
  var imgNode = document.createElement( 'img' );
  // 添加到页面
  document.body.appendChild( imgNode );
  return {
    // 设置图片的src
    setSrc: function( src ){
      // 更改src
      imgNode.src = src;
    }
  }
})();

myImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

使用代理:

// 创建一个本体对象
var myImage = (function(){
  // 创建标签
  var imgNode = document.createElement( 'img' );
  // 添加到页面
  document.body.appendChild( imgNode );
  return {
    // 设置图片的src
    setSrc: function( src ){
      // 更改src
      imgNode.src = src;
    }
  }
})();

// 创建代理对象
var proxyImage = (function(){
  // 创建一个新的img标签
  var img = new Image;
  // img 加载完成事件
  img.onload = function(){
    // 调用 myImage 替换src方法
    myImage.setSrc( this.src );
  }
  return {
    // 代理设置地址
    setSrc: function( src ){
      // 预加载 loading
      myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
      // 赋值正常图片地址
      img.src = src;
    }
  }
})();

proxyImage.setSrc( 'http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );

优缺点

优点

  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

缺点

处理请求速度可能有差别,非直接访问存在开销。

总结

ES6提供的Proxy可以让JS开发者很方便的使用代理模式,虽然代理模式很方便,但是在业务开发时应该注意使用场景,不需要在编写对象时就去预先猜测是否需要使用代理模式,只有当对象的功能变得复杂或者我们需要进行一定的访问限制时,再考虑使用代理。

我的微信公众号

更多精彩文章请关注我的前端技术公众号哦!

wechat.jpg