likes
comments
collection
share

JavaScript设计模式之代理模式

作者站长头像
站长
· 阅读数 80

概念

在《JavaScript设计模式与开发实践》 中对代理模式的定义为 为一个对象提供一个代用品或占位符,以便控制对它的访问代理模式的关键是当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问。客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象

保护代理和虚拟代理

在《JavaScript设计模式与开发实践》中是这么介绍保护代理虚拟代理的:

  • 代理B可以帮助A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B处被拒绝掉,这种代理叫做保护代理
  • 假设现实中花的价格不菲,导致在程序世界里,new Flower也是一个代价昂贵的操作,那么我们可以把new Flower的操作交给代理B去执行,代理B会选择在A心情好时在执行new Flower,这种代理叫做虚拟代理 虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。保护代理用于控制不同权限的对象对目标对象的访问。保护代理用于控制不同权限的对象对目标对象的访问,而虚拟代理是最常用的一种代理模式。

虚拟代理合并HTTP请求

假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外一台服务器上面,此时,我们的思路就是当checkbox被选中时,把选中的文件传到另一台服务器。代码如下:

var synchronousFile = function (id) {
    console.log('开始同步文件', id)
}

var checkbox = document.getElementsByTagName('input')

for (var i = 0, c; c = checkbox[i++]) {
    c.onclick = function() {
        if (this.checked === true) {
            synchronousFile(this.id);
        }
    }
}

虽然功能实现了,但是会存在很多问题:当我们连续快速点击时,会发送很多个请求,这会带来相当大的开销。我们的解决方案是,通过一个代理函数proxySynchronousFile来收集一段时间之内的请求,最后一次性发给服务器,如果不是实时性要求很高的系统,有一点延迟并不会带来太大的副作用,却能大大减轻服务器的压力。代码如下:

var synchronousFile = function (id) {
    console.log('开始同步文件', id)
}

var proxySynchronousFile = function() {
    var cache = [],
        timer;
    
    return function(id) {
        cache.push(id);
        if (timer) {
            return;
        }
        
        timer = settimeout(() => {
            synchronousFile(cache.join(','));
            clearTimeout(timer);
            timer = null;
            cache.length = 0; // 清空id集合
        }, 2000)
    }
}()

var checkbox = document.getElementsByTagName('input')

for (var i = 0, c; c = checkbox[i++]) {
    c.onclick = function() {
        if (this.checked === true) {
            proxySynchronousFile(this.id);
        }
    }
}

总结,每次只需要通过代理方法proxySynchronousFile去同步文件,而至于如何同步,只需要在代理函数中处理就好,而同步文件的方法synchronousFile也满足单一职责原则

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面的存储的运算结果。 这里引用书中缓存代理的例子-计算乘积来理解一下缓存代理的功能:

var mult = function() {
    var a = 1;
    for (var i = 0, l = argumments.length; i < l; i++) {
        a = a * arguments[i];
    }
    return a;
}

mult(2, 3) // 6
mult(2, 3, 4) // 24

加入缓存代理函数

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

proxyMult(1, 2, 3, 4) // 24
proxyMult(1, 2, 3, 4) // 24

当第二次调用proxyMult(1, 2, 3, 4)时,本体mult函数并没有被计算,proxyMult直接返回了之前计算好的结果。通过增加缓存代理的方式,mult函数可以继续专注于自身的职责——计算乘积,缓存功能是由代理对象实现的。

源码中的代理模式

Vue中的代理模式

使用过vue的同学都知道,当我们使用组件中的data、props和methods时,只需要调用this.xxx即可,那么这个是怎么实现的呢?我们拿initData的部分看下源码中是怎么处理的:

// src/core/instance/state.js

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

通过这段代码,可以看到首先我们设置了vm._data,后面又执行了 proxy(vm, _data, key)vm._data.xxx代理到vm.xxx上,最后通过observe(data, true) 监听data的变化,将data变为是相应式的。

所以,要知道为什么可以直接使用this.xxx调用到组件中的data,只需要了解proxy的实现即可:


const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

可以看到,通过修改get 和 set方法后,当我们获取vm.xxx时,实际则会取到this[sourceKey][key],也就是vm._data.xxx

代理模式的应用

代理模式在平时的开发中也会经常遇到,举个典型的例子:本地开发设置代理,在vue-cli中,我们通过设置devServer下的proxy将接口代理到对应的域名下,这应该算是最常见的代理模式的应用了,下面再列举几个不同场景的应用:

实现私有属性

js中本身是不支持私有属性的,我们也可以通过代理模式来模拟一个私有属性:

const obj = {
    name: 'myName',
    _age: 18
}

const handler = {
    get(target, propKey) {
        if (propKey.startsWith('_')) {
            throw new Error(`${propKey}为私有属性`)
        }
        return target[propKey];
    },

    set(target, propKey, value) {
        if (propKey.startsWith('_')) {
             throw new Error(私有属性`${propKey}不可设置`)
        }
        target[propKey] = value;
        return true;
    }
}

const proxyObj = new Proxy(obj, handler);

通过Proxy的get和方法,将有_开头的属性做了一个校验:如果是私有属性时,则不可直接获取和修改。

缓存分页数据

分页场景在平时的开发中是非常常见的了,通常我们的做法是,每次切换分页时,都重新请求一次接口,如果对于一些不会经常变动的列表来说,每次重新请求就没有必要了,此时我们可以类比前面的缓存代理-计算乘积的例子,缓存一下分页数据:

const getList = function(page) {
  return axios.get('/api/list', { page })
},

const proxyGetList = function() {
  const cache = {}

  return async function(page) {
    if (cache[page]) {
      return cache[page]
    }
    const list = await getList(page)
    cache[page] = list
    return list
  }
}

这种方式虽然可以减少不少的接口请求,但由于使用缓存数据,容易导致数据不能及时的更新,所以在实际的开发中,我并没有使用这种方式优化。而对于一些项目配置信息,例如用户信息等,我们完全可以通过vuex进行保存数据,当然,如果明确列表数据不会发生变化也可以考虑使用代理模式缓存下不同页数的数据,还是根据具体的场景来决定了。

总结

前面介绍了代理模式的基本概念,也举了虚拟代理缓存代理的例子来让大家有一个更深刻的影响,这两种代理模式应该是在JavaScript中最常用的两种模式,实现私有属性我认为属于是保护代理。在书中还有几种其他的代理模式,一并列出来供大家了解:

其他的代理模式

  • 防火墙代理:控制网络资源的访问,保护”主题“不让坏人接近
  • 远程代理:为一个对象在不同的地址空间提供局部代表,在Java中,远程代理可以是另一个虚拟机中的对象
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
  • 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是典型的应用场景。

最后,借用书中的一句话:虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟感谢阅读 🙏

转载自:https://juejin.cn/post/7160215752681717767
评论
请登录