Appearance
属性系统与观察者模式
上一章我们设计了 BaseObject 基类。但遇到一个问题:修改对象属性后,画布不会自动重绘。
javascript
const rect = new Rectangle({ left: 50, top: 50, fill: 'red' });
rect.draw(ctx);
// 修改属性
rect.fill = 'blue';
// 画布没有变化!需要手动重绘
ctx.clearRect(0, 0, canvas.width, canvas.height);
rect.draw(ctx);如何在属性变化时自动触发重绘?答案是:属性系统 + 观察者模式。
1. 问题分析
直接修改属性有两个问题:
- 无感知:对象不知道自己的属性被修改了
- 无联动:属性变化不会触发任何动作(如重绘)
我们需要一个机制:在属性被设置时,执行特定的逻辑。
2. 观察者模式简介
观察者模式:当一个对象的状态改变时,所有依赖它的对象都会收到通知。
核心角色:
- 被观察者(Subject):发布事件
- 观察者(Observer):订阅事件,响应通知
类比:订阅报纸。报社发布新内容,所有订阅者自动收到。
3. 事件系统实现
先实现一个简单的事件系统:
javascript
class EventEmitter {
constructor() {
this.events = {};
}
on(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}
off(event, handler) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(h => h !== handler);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(handler => handler(data));
}
}使用:
javascript
const emitter = new EventEmitter();
// 订阅
emitter.on('change', (data) => {
console.log('属性变化:', data);
});
// 发布
emitter.emit('change', { key: 'fill', value: 'blue' });
// 输出:属性变化: { key: 'fill', value: 'blue' }4. 属性拦截:使用 defineProperty
现在要拦截属性的设置操作。使用 Object.defineProperty:
javascript
function defineReactiveProperty(obj, key, defaultValue) {
let value = defaultValue;
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
if (value !== newValue) {
const oldValue = value;
value = newValue;
// 发布属性变化事件
obj.emit('property:changed', { key, oldValue, value: newValue });
obj.setDirty(); // 标记需要重绘
}
}
});
}使用:
javascript
class BaseObject extends EventEmitter {
constructor() {
super();
defineReactiveProperty(this, 'fill', '#000000');
defineReactiveProperty(this, 'left', 0);
// ... 其他属性
}
setDirty() {
console.log('对象被标记为脏');
// 通知画布重绘
}
}
const obj = new BaseObject();
obj.fill = 'red'; // 触发 setter,输出:对象被标记为脏5. 脏标记机制
频繁重绘会影响性能。使用 脏标记(Dirty Flag) 优化:
- 属性变化时,只标记对象为"脏"
- 统一在
requestAnimationFrame中批量重绘所有脏对象
javascript
class Canvas {
constructor(canvasElement) {
this.canvas = canvasElement;
this.ctx = canvasElement.getContext('2d');
this.objects = [];
this.renderRequested = false;
}
add(obj) {
obj.canvas = this; // 对象引用画布
this.objects.push(obj);
this.requestRender();
}
requestRender() {
if (this.renderRequested) return;
this.renderRequested = true;
requestAnimationFrame(() => {
this.render();
this.renderRequested = false;
});
}
render() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.objects.forEach(obj => {
if (obj._dirty) {
obj.draw(this.ctx);
obj._dirty = false;
} else {
obj.draw(this.ctx); // 简化版:每次都绘制
}
});
}
}
class BaseObject extends EventEmitter {
constructor() {
super();
this._dirty = true;
this.canvas = null;
}
setDirty() {
this._dirty = true;
this.canvas?.requestRender(); // 通知画布重绘
}
}6. 完整属性系统集成
将属性系统集成到 BaseObject:
javascript
class BaseObject extends EventEmitter {
constructor(options = {}) {
super();
this.canvas = null;
this._dirty = true;
// 定义响应式属性
this._defineReactiveProperties([
'left', 'top', 'width', 'height',
'rotation', 'scaleX', 'scaleY',
'fill', 'stroke', 'strokeWidth', 'opacity',
'visible', 'selectable'
]);
// 设置初始值
Object.assign(this, {
left: 0,
top: 0,
width: 100,
height: 100,
rotation: 0,
scaleX: 1,
scaleY: 1,
fill: '#000000',
stroke: null,
strokeWidth: 1,
opacity: 1,
visible: true,
selectable: true,
...options
});
}
_defineReactiveProperties(keys) {
keys.forEach(key => {
let value = this[key];
Object.defineProperty(this, key, {
get() {
return value;
},
set(newValue) {
if (value !== newValue) {
const oldValue = value;
value = newValue;
this.emit('property:changed', { key, oldValue, value: newValue });
this.setDirty();
}
}
});
});
}
setDirty() {
this._dirty = true;
this.canvas?.requestRender();
}
// 批量设置属性(避免多次重绘)
set(props) {
Object.keys(props).forEach(key => {
this['_' + key] = props[key]; // 直接设置内部值,不触发 setter
});
this.setDirty(); // 最后统一标记脏区
}
}7. 使用示例
javascript
const canvas = new Canvas(document.getElementById('canvas'));
const rect = new Rectangle({
left: 50,
top: 50,
width: 100,
height: 80,
fill: 'red'
});
canvas.add(rect);
// 修改属性,自动重绘
setTimeout(() => {
rect.fill = 'blue'; // 自动触发重绘
}, 1000);
setTimeout(() => {
rect.rotation = Math.PI / 4; // 自动触发重绘
}, 2000);
// 批量更新(只重绘一次)
setTimeout(() => {
rect.set({
fill: 'green',
left: 150,
scaleX: 1.5
});
}, 3000);8. 监听属性变化
外部可以监听对象的属性变化:
javascript
rect.on('property:changed', (e) => {
console.log(`属性 ${e.key} 从 ${e.oldValue} 变为 ${e.value}`);
});
rect.fill = 'yellow';
// 输出:属性 fill 从 blue 变为 yellow9. 使用 Proxy 的简洁实现
Proxy 提供更简洁的属性拦截方式:
javascript
class BaseObject extends EventEmitter {
constructor(options = {}) {
super();
this.canvas = null;
this._dirty = true;
// 初始化属性
Object.assign(this, {
left: 0,
top: 0,
fill: '#000000',
// ... 其他属性
...options
});
// 返回 Proxy 包装后的对象
return new Proxy(this, {
set(target, key, value) {
const oldValue = target[key];
if (oldValue !== value) {
target[key] = value;
target.emit('property:changed', { key, oldValue, value });
target.setDirty();
}
return true;
}
});
}
setDirty() {
this._dirty = true;
this.canvas?.requestRender();
}
}优点:代码简洁,无需逐个定义属性 缺点:兼容性稍差(IE 不支持)
10. 属性验证
增强属性系统,添加验证逻辑:
javascript
class BaseObject extends EventEmitter {
constructor() {
super();
// 属性验证规则
this._validators = {
opacity: (value) => value >= 0 && value <= 1,
scaleX: (value) => value > 0,
scaleY: (value) => value > 0
};
}
_defineReactiveProperties(keys) {
keys.forEach(key => {
let value = this[key];
Object.defineProperty(this, key, {
get() {
return value;
},
set(newValue) {
// 验证
const validator = this._validators[key];
if (validator && !validator(newValue)) {
console.warn(`Invalid value for ${key}: ${newValue}`);
return;
}
if (value !== newValue) {
const oldValue = value;
value = newValue;
this.emit('property:changed', { key, oldValue, value: newValue });
this.setDirty();
}
}
});
});
}
}
// 使用
rect.opacity = 1.5; // 警告:Invalid value for opacity: 1.511. 性能优化策略
防止无效更新
javascript
set(newValue) {
if (value === newValue) return; // 关键:避免无效更新
// ...
}批量更新
javascript
rect.set({
fill: 'red',
stroke: 'blue',
strokeWidth: 2
});
// 只触发一次重绘,而不是三次延迟重绘
使用 requestAnimationFrame 将多次重绘合并为一次:
javascript
requestRender() {
if (this.renderRequested) return;
this.renderRequested = true;
requestAnimationFrame(() => {
this.render();
this.renderRequested = false;
});
}12. 与 Vue.js 响应式系统的对比
Vue.js 2.x 也使用 Object.defineProperty 实现响应式:
javascript
// Vue.js 的核心思想
data() {
return {
message: 'Hello'
};
}
// 当 message 变化时,视图自动更新
// 我们的属性系统
rect.fill = 'red'; // 画布自动重绘两者原理相同:拦截属性设置,触发更新。
本章小结
属性系统通过 观察者模式 实现了属性变化的自动响应:
- 事件系统:发布订阅模式,解耦对象和画布
- 属性拦截:
defineProperty或Proxy,监听属性设置 - 脏标记:批量重绘,优化性能
- 批量更新:
set()方法,避免多次重绘
下一章,我们将实现对象的序列化与反序列化,让图形数据可以保存和加载。