业务开发中最常见的四种代理类型:事件代理、虚拟代理、缓存代理和保护代理
事件代理
基于事件的冒泡特性,在子元素上的点击事件会向父级冒泡,所以我们只需要在父元素上绑定一次事件,根据event.target来判断实际触发事件的元素,节省了很多绑定事件的开销
虚拟代理
// 常见案例为图片的预加载
// 图片的预加载指,避免用户网络慢时或者图片太大时,页面长时间给用户留白的尴尬
// 图片URL先指向占位图url,
// 在后台新建一个图片实例,该图片实例 的URL指向真实的图片地址,
// 当该图片实例加载完毕时再把页面图片的地址指向真实的图片地址,
// 这样页面图片就可以直接使用缓存展示,
// 因预加载使用的图片实例的生命周期全程在后台从未在渲染层面抛头露面。
// 因此这种模式被称为“虚拟代理”模式
class LoadImage {
constructor(imgNode: Element) {
// 获取真实的DOM节点
this.imgNode = imgNode
}
// 操作img节点的src属性
setSrc(imgUrl) {
this.imgNode.src = imgUrl
}
}
class PreLoadProxy {
// 占位图的url地址
static LOADING_URL = 'xxxxxx'
private targetImage: LoadImage;
constructor(targetImage: LoadImage) {
this.targetImage = targetImage
}
// 该方法主要操作虚拟Image,完成加载
setSrc(targetUrl): Promise<boolean> {
// 真实img节点初始化时展示的是一个占位图
this.targetImage.setSrc(ProxyImage.LOADING_URL)
// 创建一个帮我们加载图片的虚拟Image实例
const virtualImage = new Image()
return new Promise((resolve, reject) => {
// 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage.onload = () => {
this.targetImage.setSrc(targetUrl);
resolve(true);
}
virtualImage.onerror = () => {
reject(false);
}
// 设置src属性,虚拟Image实例开始加载图片
virtualImage.src = targetUrl;
});
}
}
const imageList: Element[] = Array.from(document.getElementByClassName('preload-image')));
Promise.all(imageList.map(image => new PreLoadProxy(new LoadImage(image)).setSrc('realUrl')))
.then()
.catch()
缓存代理
interface Cal {
addAll: (...args: number[]) => : number
}
// 缓存上一次的结果
// 比如一个累加器
class Calculator implements Cal {
addAll(...args: number[]): number {
if (!args) {
return 0;
}
return args.reduce((pre, next) => pre + next, 0)
}
}
const calculator = new Calculator()
// 连续执行两次相同的累加函数会遍历两次
calculator.addAll(1, 2, 3, 5);
calculator.addAll(1, 2, 3, 5);
class CacheProxy implements Cal {
private caches: {[key: string]: number} = {}
private target: Cal;
constructor(cal: Cal) {
this.target = cal;
}
addAll(...args: number[]): number {
const key = args.join();
if (this.caches[key] === undefined) {
this.caches[key] = this.target.addAll();
}
return this.caches[key];
}
}
const calculator = new Calculator();
const calculatorProxy = new CacheProxy(calculator);
// 连续执行两次相同的累加函数第二次会使用缓存
calculatorProxy.addAll(1, 2, 3, 5);
calculatorProxy.addAll(1, 2, 3, 5);
// 写到这里,我想这不就是给原来的累加器加了一个缓存功能吗?
// 加额外的功能又不改变原来的结构这不就符合装饰器模式的定义吗
// 看看是否可以改造成一个缓存装饰器
// 没印象的可以看上一小节
// 装饰器使用闭包保存caches对象
function cacheDecorator(caches = {}) {
return function decorator(target, name, descriptor) {
// 保存装饰的方法
let originalMethod = descriptor.value
// 在这里扩展装饰的方法
descriptor.value = function(...args) {
const key = args.join();
if (caches[key] === undefined) {
caches[key] = originalMethod.apply(this, args);
}
return caches[key];
}
return descriptor
}
}
// 使用缓存装饰器
class Calculator implements Cal {
@cacheDecorator()
addAll(...args: number[]): number {
if (!args) {
return 0;
}
return args.reduce((pre, next) => pre + next, 0)
}
}
const calculator = new Calculator()
// 连续执行两次相同的累加函数同样会缓存
calculator.addAll(1, 2, 3, 5);
calculator.addAll(1, 2, 3, 5);
保护代理
就是ES6的Proxy, 劫持对象的属性,VUE3.0的双向绑定实现原理
行为型
策略模式
消灭if else , 定义一系列策略,把它们封装起来,并使它们可替换
// 比如你出去旅游
// 可以选择以下交通工具:步行,自行车,火车,飞机
// 对应需要花的时间
// 步行 48h
// 自行车 30h
// 火车 8h
// 飞机 1h
enum Tool {
WALK = 'walk',
BIKE = 'bike',
TRAIN = 'train',
PLANE = 'plane'
}
/**
* 计算花费的时间
* if else 一把梭
* @param tool
*/
function timeSpend(tool: Tool): number {
if (tool === Tool.WALK) {
return 48;
} else if (tool === Tool.BIKE) {
return 30;
} else if (tool === Tool.TRAIN) {
return 8;
} else if (tool === Tool.PLANE) {
return 1;
} else {
return NaN
}
}
// 此时新增了一种交通工具 motoBike : 18h
// 你就必须去改timeSpend函数,在里面加else if ,
// 然后你和测试同学说帮忙回归一下整套旅游时间花费逻辑
// 测试同学嘴上说好的,心里说了一句草泥马
// 策略模式重构
// 把策略抽出来并封装成一个个函数
// 使用映射代替if else
// 此时新增一种策略,只需要新增一个策略函数并把它放入映射中
// 这样你就可以自信的和测试同学说,我增加了一种旅行方式,
// 你只要测新增的方式,老逻辑不需要回归
// 于是你从人人喊打的if else 侠摇身一变成了测试之友
const timeMap = {
walk,
bike,
train,
plane
}
function timeSpend(tool: Tool): number {
return timeMap[tool]() || NaN;
}
function walk() {
return 48;
}
function bike() {
return 30;
}
function train() {
return 8;
}
function plane() {
return 1;
}
状态模式
一个对象有多种状态,每种状态做不同的事情,状态的改变是在状态内部发生的, 对象不需要清楚状态的改变,它只用调用状态的方法就行,可以看看这个例子加深理解
观察者模式
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
有两个关键角色: 发布者,订阅者
- 发布者添加订阅者
- 发布者发生变化通知订阅者
- 订阅者执行相关函数
// 以vue的响应式更新视图原理为例
// 数据发生变化,更新视图
// observe方法遍历并包装对象属性
function observe(target, cb) {
// 若target是一个对象,则遍历它
if(target && typeof target === 'object') {
Object.keys(target).forEach((key)=> {
// defineReactive方法会给目标属性装上“监听器”
defineReactive(target, key, target[key], cb)
})
}
}
// 定义defineReactive方法
function defineReactive(target, key, val, cb) {
// 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
observe(val)
// 为当前属性安装监听器
Object.defineProperty(target, key, {
// 可枚举
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 监听器函数
set: function (value) {
// 执行render函数
render();
val = value;
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
//代理传入的对象,数据发生变化,执行render函数
observe(this._data, options.render)
}
}
let app = new Vue({
el: '#app',
data: {
text: 'text',
text2: 'text2'
},
render(){
console.log("render");
}
})
观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者,
angular的ngrx, react的redux 和 vue的vuex,event-bus都是典型的发布订阅模式