- 重要: 代理模式、装饰器模式、适配器模式
- 不重要: 桥接模式、外观模式、组合模式、享元模式
装饰器模式
装饰器模式,又名装饰者模式。它的定义是“在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求”。
// 定义打开按钮
class OpenButton {
// 点击后展示弹框(旧逻辑)
onClick() {
const modal = new Modal()
modal.style.display = 'block'
}
}
// 定义按钮对应的装饰器
class Decorator {
// 将按钮实例传入
constructor(open_button) {
this.open_button = open_button
}
onClick() {
this.open_button.onClick()
// “包装”了一层新逻辑
this.changeButtonStatus()
}
changeButtonStatus() {
this.changeButtonText()
this.disableButton()
}
disableButton() {
const btn = document.getElementById('open')
btn.setAttribute("disabled", true)
}
changeButtonText() {
const btn = document.getElementById('open')
btn.innerText = '快去登录'
}
}
const openButton = new OpenButton()
const decorator = new Decorator(openButton)
document.getElementById('open').addEventListener('click', function() {
// openButton.onClick()
// 此处可以分别尝试两个实例的onClick方法,验证装饰器是否生效
decorator.onClick()
})
ES7,需要babel转换
// 具体的参数意义,在下个小节,这里大家先感知一下操作
function funcDecorator(target, name, descriptor) {
let originalMethod = descriptor.value
descriptor.value = function() {
console.log('我是Func的装饰器逻辑')
return originalMethod.apply(this, arguments)
}
return descriptor
}
class Button {
@funcDecorator
onClick() {
console.log('我是Func的原有逻辑')
}
}
// 验证装饰器是否生效
const button = new Button()
button.onClick()
适配器模式
适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题```
// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
const type = type.toUpperCase()
let result
try {
// 实际的请求全部由新接口发起
if(type === 'GET') {
result = await HttpUtils.get(url) || {}
} else if(type === 'POST') {
result = await HttpUtils.post(url, data) || {}
}
// 假设请求成功对应的状态码是1
result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
} catch(error) {
// 捕捉网络错误
if(failed){
failed(error.statusCode);
}
}
}
// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
await AjaxAdapter(type, url, data, success, failed)
}
⭐代理模式
在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。
应用
事件委托代理
- 事件捕获指的是从document到触发事件的那个节点,即自上而下的去触发事件
- 事件冒泡是自下而上的去触发事件
- 绑定事件方法的第三个参数,就是控制事件触发顺序是否为事件捕获。true为事件捕获;false为事件冒泡,默认false。
<body>
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let list = document.querySelector('#list');
list.addEventListener('click',event=>{
alert(event.target.innerHTML);
});
</script>
</body>
图片预加载
预加载主要是为了避免网络不好、或者图片太大时,页面长时间给用户留白的尴尬。
常见的操作是先让这个 img 标签展示一个占位图,然后创建一个 Image 实例,让这个 Image 实例的 src 指向真实的目标图片地址、观察该 Image 实例的加载情况 —— 当其对应的真实图片加载完毕后,即已经有了该图片的缓存内容,再将 DOM 上的 img 元素的 src 指向真实的目标图片地址。
此时我们直接去取了目标图片的缓存,所以展示速度会非常快,从占位图到目标图片的时间差会非常小、小到用户注意不到,这样体验就会非常好了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<style>
.bg-container {
width: 600px;
height: 400px;
margin: 100px auto;
}
.bg-container #bg-image {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="background">
<button
data-src="https://images.pexels.com/photos/258112/pexels-photo-258112.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260"
>
背景1
</button>
<button
data-src="https://images.pexels.com/photos/1025469/pexels-photo-1025469.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260"
>
背景2
</button>
</div>
<div class="bg-container">
<img id="bg-image" src="/img1.jpg" />
</div>
<script>
class PreLoadImage {
constructor() {
this.bgImage = document.querySelector("#bg-image");
}
setSrc(src) {
this.bgImage.src = src;
}
}
class ProxyImage {
static LOADING_URL = `loading.gif`;
constructor() {
this.targetImage = new PreLoadImage();
}
setSrc(src) {
this.targetImage.setSrc(ProxyImage.LOADING_URL);
let virtualImage = new Image();
// 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage .onload = () => {
this.targetImage.setSrc(src);
};
virtualImage .src = src;
}
}
let proxyImage = new ProxyImage();
let container = document.querySelector("#background");
container.addEventListener("click", function (event) {
let src = event.target.dataset.src;
proxyImage.setSrc(src);
});
</script>
</body>
</html>
缓存代理
缓存代理比较好理解,它应用于一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。
一个比较典型的例子,是对传入的参数进行求和:
// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
console.log('进行了一次新计算')
let result = 0
const len = arguments.length
for(let i = 0; i < len; i++) {
result += arguments[i]
}
return result
}
// 为求和方法创建代理
const proxyAddAll = (function(){
// 求和结果的缓存池
const resultCache = {}
return function() {
// 将入参转化为一个唯一的入参字符串
const args = Array.prototype.join.call(arguments, ',')
// 检查本次入参是否有对应的计算结果
if(args in resultCache) {
// 如果有,则返回缓存池里现成的结果
return resultCache[args]
}
return resultCache[args] = addAll(...arguments)
}
})()
Proxy
- Proxy 可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
- Proxy 这个词的原意是代理,用在这里表示由它来代理某些操作,可以译为代理器
let wang={
name: 'wanglaoshi',
age: 29,
height:165
}
let wangMama=new Proxy(wang,{
get(target,key) {
if (key == 'age') {
return wang.age-1;
} else if (key == 'height') {
return wang.height-5;
}
return target[key];
},
set(target,key,val) {
if (key == 'boyfriend') {
let boyfriend=val;
if (boyfriend.age>40) {
throw new Error('太老');
} else if (boyfriend.salary<20000) {
throw new Error('太穷');
} else {
target[key]=val;
return true;
}
}
}
});
console.log(wangMama.age);
console.log(wangMama.height);
wangMama.boyfriend={
age: 41,
salary:3000
}
- Vue2 中的变化侦测实现对 Object 及 Array 分别进行了不同的处理,Object 使用了 Object.defineProperty API,Array使用了拦截器对 Array 原型上的能够改变数据的方法进行拦截。虽然也实现了数据的变化侦测,但存在很多局限 ,比如对象新增属性无法被侦测,以及通过数组下边修改数组内容,也因此在 Vue2 中经常会使用到 $set 这个方法对数据修改,以保证依赖更新。
- Vue3 中使用了 es6 的 Proxy API对数据代理,没有像 Vue2 中对原数据进行修改,只是加了代理包装,因此首先性能上会有所改善。其次解决了 Vue2 中变化侦测的局限性,可以不使用 $set 新增的对象属性及通过下标修改数组都能被侦测到。