消息订阅与发布
先直接手搓:
class Event {constructor() {this.events = {};}on(event, callback) {if (!this.events[event]) {this.events[event] = [];}this.events[event].push(callback);}emit(event, data) {if (this.events[event]) {this.events[event].forEach(callback => {callback(...data);});}}off(event, callback) {if (this.events[event]) {const index = this.events[event].indexOf(callback);if (index !== -1) {this.events[event].splice(index, 1);}}}once(event, callback) {const wrapper = (...args) => {callback(...args);this.off(event, wrapper);}this.on(event, wrapper);}
}export default new Event()
但是使用这玩意的时候要注意处理监听和触发的顺序问题,即保证在emit之前你的eventName已经正确的被on监听到。这个坑在异步操作的时候可能会出现,其他倒没啥难的。只能说哥们之前实习的时候干过这蠢事
补充:还有一种情况。想起来了,哥们全都想起来了,下面这个demo才是我在实习那会干的蠢事。 假设在我们的应用里面存在下面这样的订阅发布关系:
// PageA.vue
<template>Your View</template>
<script setup>mounted(() => { eventBus.on('eventA', () => {console.log("This is PageA")}) })
</script>// PageB.vue
<template>Your View</template>
<script>mounted(() => {eventBus.emit('eventA', "msg from PageB")})
</script>// App.vue
import PageB from './PageB.vue';
import PageA from './PageA.vue';
// TODO: 生成你的应用
这样的初衷是我们希望在PageB挂载的时候能发送消息到PageA那里,但这样写的话消息发送出去了可是PageA接受不到,这是由于模块引入的顺序导致的,应该先引入模块PageA。
defineProperty VS Proxy
还是那个老生常谈的问题:为什么Vue3用Proxy来实现响应式而不是defineProperty?Vue3现在哥们用的也不多
- 响应式系统的简化:使用 Object.defineProperty 需要对每个属性进行设置,而 Proxy 可以直接拦截对整个对象或者数组的操作,处理起来更为简便。
- 性能相关:Proxy 可以一次性拦截整个对象的操作,而defineProperty 则需要为每个属性进行递归处理,性能较低。
感觉很牵强,几行js能扯上啥性能 - 支持数组和对象的整个操作:Proxy 可以处理多种操作比如读取、写入、删除等,而 Object.defineProperty 主要用于属性的定义和获取。
感觉最重要的还是第三点,以数组为例,Proxy不止比defineProperty方便,而且似乎可以说很多情况根本无法用后者来处理数组:
const data = [1, 2, 3];Object.defineProperty(data, '0', {get: function () {console.log('Getting index 0');return data[0];},set: function (value) {console.log(`Setting index 0 to ${value}`);data[0] = value;}
});
console.log(data[0]); // 直接死循环。因为get里面访问data[0]的时候又调用了get
data.push(5); // 触发不了set,但是数组变为[1,2,3,5]
console.log(data[1]); // 触发不了get,但不会死循环
但是用Proxy来改写就不会出现这种情况:
const data = [1, 2, 3];
const handler = {get: function (target, prop) {if (target.hasOwnProperty(prop)) {console.log(`Getting index ${prop}`);}return target[prop];},set: function (target, prop, value) {console.log(`Setting index ${prop} to ${value}`);target[prop] = value;return true;}
};const proxyData = new Proxy(data, handler);
// 使用 Proxy 可以拦截所有索引的读取和赋值操作
console.log(proxyData[0]); // 输出: Getting index 0 1
proxyData[0] = 4; // 输出: Setting index 0 to 4
console.log(proxyData[0]); // 输出: Getting index 0 4
// 使用 Proxy 可以拦截数组长度的变化
proxyData.push(5); // 输出: Setting index 3 to 5
console.log(proxyData.length); // 输出: Getting index length 4
// 使用 Proxy 可以拦截其他索引的读取和赋值操作
console.log(proxyData[1]); // 输出: Getting index 1 2
proxyData[1] = 6; // 输出: Setting index 1 to 6
console.log(proxyData[1]); // 输出: Getting index 1 6
不过要注意的是用Proxy来代理的对象,原型上的方法或属性也会被get拦截(比如数组的push,当然length也会,即使它是实例属性)。而且要注意:Proxy监听的只是第一层的属性。换句话说他无法深层代理:
const data = {name: "John",profile: {age: 25,country: "USA"}
};const handler = {get: function (target, prop) {console.log(`Getting property: ${prop}`, target[prop]);return target[prop];},set: function (target, prop, value) {console.log(`Setting property: ${prop} to ${value}`);target[prop] = value;return true;}
};const proxy = new Proxy(data, handler);console.log(proxy.profile.age);
// 先输出:Getting property: profile { age: 25, country: 'USA' },此时访问的是 proxy.profile。这会返回 data.profile 对象,
// 再输出:25,这一次是在 console.log 内部访问 profile.age,这会直接从 profile 对象中获取值,而不是通过代理。proxy.profile.age = 26; // 不会触发set
console.log(proxy.profile.age);
// 先输出:Getting property: profile { age: 25, country: 'USA' },因为在赋值语句里访问了profile
// 再输出:Getting property: profile { age: 26, country: 'USA' }
// 最后输出:26
在某些场景下把Proxy改写成深度监听的Proxy会很有用,mobx和Vue3都是这样做的:
const deepProxy = (val: any, handler: Function) => {if (typeof val === "object" && val !== null) {Object.keys(val).forEach((key) => {val[key] = deepProxy(val[key], handler);});return new Proxy(val, handler());} else {return val;}};