задний план
В бизнесе компании есть сценарий: когда пользователь нажимает на метку, содержимое метки нужно обработать в форму, аналогичную темам Weibo, и вставить вtextareaсередина.textareaМетка и это два компонента страницы.Обычно я могу щелкнуть по метке и вызвать событие, страница слушает, а затем передает данные вtextarea, но такая обработка доставляет неудобства, поэтому я хочу сымитировать EventBus Vue, чтобы реализовать передачу значений родственных компонентов апплета.
Первый взгляд на демо:
Часть метки и раздел поля ввода являются двумя компонентами страницы.Когда мы хотим сделать, добавьте содержимое метки в поле ввода при нажатии на метку.
Реализовать идеи
Способ реализации 1: когда вы нажимаете на метку, получаете текстовое содержимое метки, а затем вызываете WeChat'striggerEventAPI выдает событие; затем слушайте это событие на странице, получайте текст метки, выдаваемый компонентом метки, вставляйте его в данные страницы, а затем передайте его компоненту поля ввода и компоненту поля ввода. проходитobserversОтслеживайте входящие текстовые данные и, наконец, расшифровывайте их и присваивайте значение в поле ввода.
Метод 1 — это решение, которое легко придумать, но недостатки очевидны, а процесс реализации слишком громоздкий, поэтому метод напрямую передается, и мы сосредоточимся на другом решении реализации — EventBus.
Метод реализации 2: в методе 2 мы имитируем EventBus Vue для реализации передачи значений одноуровневых компонентов, по сути, используется режим публикации-подписки.
Во-первых, когда мы нажимаем на метку, нам нужно инициировать событие наружу, а затем нести содержимое метки, которая является издателем сообщения; затем нам нужно прослушать событие, вызванное компонентом метки в компонент textarea, а затем получить содержимое метки, он является подписчиком сообщения. Что нам нужно реализовать, так это мост между компонентом метки и связью с текстовой областью.
Я уже написал компонент, адрес демки на гитхабе прикреплен в конце статьи, ниже я показываю только код ключа, базовый код компонента демонстрировать не буду.
написать подписчику
Во-первых, нам нужен объект, который дает намonметод, который позволяет нам прослушивать события, запускаемые другим компонентом. так этоonМетод должен получить два параметра: один — это имя события, которое мы хотим прослушать, а другой параметр — это функция, которая уведомит нас, когда событие сработает.
Нам нужно написать подписку в жизненном цикле компонента, чтобы гарантировать, что когда другой компонент выдает событие, мы подписаны.
// textArea组件的JS
import bus from '../../utils/eventbus'; // 这里的代码还没写,我们先假定提供订阅API的对象是这个js模块提供的
Component({
data: {},
lifetimes: {
ready() {
// 当组件的ready生命周期执行的时候,我们通过bus对象提供的on方法去订阅了sendTag事件
// 当sendTag事件被触发的时候,我们给它提供了一个函数,这个函数接收个tagText参数,这个就是标签组件被点击要传递的内容
// 我们这里订阅的是sendTag事件,那么也就是要求标签被点击的时候也必须向外抛出sendTag事件
bus.on('sendTag', tagText => {
console.log(tagText);
});
},
},
methods: {}
})
Затем мы пишем модуль шины событий.Во-первых, этот модуль должен, наконец, выставить объект, и этот объект должен иметь метод on.
// eventbus.js
class Bus {
on(Event, cb) {
}
}
export default new Bus();
Продолжаем анализировать метод on Нам нужно хранить Event в качестве ключа и cb в качестве значения, чтобы когда издатель публикует сообщение (запуск события извне), мы находили соответствующее событие и выполняли соответствующий метод.
В общем, у одного и того же события может быть несколько подписчиков. Например, ниже есть три компонента textarea, каждый из которых должен получать содержимое метки при нажатии на метку. Поэтому нам нужен массив для размещения нескольких подписчиков.
class Bus {
constructor() {
// events 是一个容器,里面放的是,各个事件和它的订阅者,数据格式如:
/**
this.events = {
sendTag: [cb1,cb2,cb3],
sendMsg: [cb4,cb5,cb6]
};
*/
// 当然也可以直接this.sendTag = [cb1,cb2,cb3];个人习惯不同,我更喜欢放的容器内
this.events = {};
}
on(Event, cb) {
if(this.events[Event]) {
// 如果这个事件存在,那说明之前已经有订阅者了,此时只需要将这个订阅者再push进去即可
// this.events[Event] 是ES6的写法,可以百度搜索 ES6熟悉名表达式了解
// 如果传进来的 Event 是 ‘sendTag’,this.events[Event] 就是 this.events.sendTag,也就在 events 对象上加了一个 sendTag 属性
this.events[Event].push(cb);
} else {
// 如果这个事件不存在,那我们就需要对其初始化,将我们作为第一个订阅者,放到数组中赋给这个事件
this.events[Event] = [cb];
}
}
}
export default new Bus();
написать издатель
Выше мы реализовали базовую функцию подписки, а затем нам нужно реализовать функцию публикации.
Во-первых, нам нужноbusОбъекты дают намemitметод, этот метод также получает два параметра, первый параметр — это имя события, когда мы публикуем сообщение (имя события, когда событие инициируется извне), второй параметр — это параметр, который будет передан при публикации события, конечно, вы также может не передаваться аргументов.
Нам нужно вызвать, когда метка нажатаemitметод запускает событие
<!-- 标签对应的组件 -->
<view class="container">
<view class="label">Tags</view>
<view class="tags">
<!-- bindtap="clickTag" 标签的点击事件-->
<!-- data-tag="{{item}}" 标签被点击的时候,在事件对象内加一个 tag 属性,值就是标签的文本内容,小程序文档:https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html -->
<view
class="tag"
wx:for="{{tags}}"
wx:key="index"
bindtap="clickTag"
data-tag="{{item}}">{{item}}</view>
</view>
</view>
// 标签组件对应的 js 逻辑
// 引入 bus 对象
import bus from '../../utils/eventbus';
Component({
data: {
tags: ["被骂哭的导盲犬主", "全国65亿网民月", "鲍毓明养女发声", "大学没有谈过恋爱", "多名学生曝被班主", "林奕含去世三周年", "李国庆发人事调整", "肖战发声", "和对象一起长胖是", "奔跑吧", "最春天的照片", "张杰爱人啊", "拔牙后千万不要嗜睡", "中国第四个新冠疫", "蒋凡被除名合伙人"]
},
methods: {
clickTag({target: {dataset: {tag}}}) {
// 当标签被点击的时候,调用 bus 的 emit 方法,然后向外抛一个 sendTag 事件,同时把标签内容传出去
// 抛出的事件一定要和监听事件对应起来,呃。。。。。
bus.emit('sendTag', `#${tag}#`);
}
}
})
следующий вbusдобавить один к объектуemitметод.
class Bus {
constructor() {
// events 是一个容器,里面放的是,各个事件和它的订阅者,数据格式如:
/**
this.events = {
sendTag: [cb1,cb2,cb3],
sendMsg: [cb4,cb5,cb6]
};
*/
// 当然也可以直接this.sendTag = [cb1,cb2,cb3];个人习惯不同,我更喜欢放的容器内
this.events = {};
}
// 新加的 emit 方法
emit(Event, obj) {
}
on(Event, cb) {
if(this.events[Event]) {
// 如果这个事件存在,那说明之前已经有订阅者了,此时只需要将这个订阅者再push进去即可
// this.events[Event] 是ES6的写法,可以百度搜索 ES6熟悉名表达式了解
// 如果传进来的 Event 是 ‘sendTag’,this.events[Event] 就是 this.events.sendTag,也就在 events 对象上加了一个 sendTag 属性
this.events[Event].push(cb);
} else {
// 如果这个事件不存在,那我们就需要对其初始化,将我们作为第一个订阅者,放到数组中赋给这个事件
this.events[Event] = [cb];
}
}
}
export default new Bus();
Далее продолжайте анализироватьemitЧто должен делать метод
когдаemitКогда метод вызывается, это означает, что кому-то нужно опубликовать сообщение, в это время нам нужно запуститьeventsНайдите соответствующее событие на объекте (events[event]), а затем уведомить всех подписчиков, подписавшихся на это событие (events[event]Значение представляет собой массив, а элементы массива — это функции, предоставленные подписчиком для их уведомления, поэтому необходимо пройти по массиву, вызвать эти функции одну за другой, а затем передать содержимое, переданное издателем, в эти функции. функции)
class Bus {
constructor() {
// events 是一个容器,里面放的是,各个事件和它的订阅者,数据格式如:
/**
this.events = {
sendTag: [cb1,cb2,cb3],
sendMsg: [cb4,cb5,cb6]
};
*/
// 当然也可以直接this.sendTag = [cb1,cb2,cb3];个人习惯不同,我更喜欢放的容器内
this.events = {};
}
// 新加的 emit 方法
emit(Event, obj) {
// 如果要发布的这个事件不存在,说明这个事件没有订阅者,直接 return ,什么都不需要做
if(!this.events[Event]) return
// 这个事件存在,说明它是有订阅者的,我们去遍历它,然后再把发布者要传递的内容传入这些订阅者提供的函数内
this.events[Event].forEach(cb => {
cb(obj)
})
}
on(Event, cb) {
if(this.events[Event]) {
// 如果这个事件存在,那说明之前已经有订阅者了,此时只需要将这个订阅者再push进去即可
// this.events[Event] 是ES6的写法,可以百度搜索 ES6熟悉名表达式了解
// 如果传进来的 Event 是 ‘sendTag’,this.events[Event] 就是 this.events.sendTag,也就在 events 对象上加了一个 sendTag 属性
this.events[Event].push(cb);
} else {
// 如果这个事件不存在,那我们就需要对其初始化,将我们作为第一个订阅者,放到数组中赋给这个事件
this.events[Event] = [cb];
}
}
}
export default new Bus();
После написания этой части мы можем сначала посмотреть на эффект
написать бизнес
Видно, что с помощью приведенного выше кода мы в основном реализовали связь между двумя компонентами.Далее мы продолжим улучшатьtextareaкод, мы слушаемsendTagКогда событие срабатывает, вам нужно добавить значение, переданное из другого компонента, в поле ввода.
<!-- textarea 组件模板 -->
<view class="container">
<view class="label">内容</view>
<!-- value="{{value}}" 控制输入框的内容 -->
<textarea
class="textarea"
placeholder="请输入内容。。。"
value="{{value}}"
show-confirm-bar="{{false}}"
bindinput="handleInput"></textarea>
</view>
// textArea组件的JS
import bus from '../../utils/eventbus'; // 这里的代码还没写,我们先假定提供订阅API的对象是这个js模块提供的
Component({
data: {
value: ''
},
lifetimes: {
ready() {
// 当组件的ready生命周期执行的时候,我们通过bus对象提供的on方法去订阅了sendTag事件
// 当sendTag事件被触发的时候,我们给它提供了一个函数,这个函数接收个tagText参数,这个就是标签组件被点击要传递的内容
// 我们这里订阅的是sendTag事件,那么也就是要求标签被点击的时候也必须向外抛出sendTag事件
bus.on('sendTag', tagText => {
const {value} = this.data;
this.setData({
value: `${value} ${tagText} `
});
});
},
},
methods: {
handleInput({detail: {value}}) {
this.setData({value});
},
}
})
Давайте посмотрим на эффект в это время
исправить ошибку
Через приведенную выше демонстрацию кажется, что наши потребности были выполнены, но на самом деле в коде подписки все еще скрыта ошибка, мы печатаем переданные параметры и текущий this
bus.on('sendTag', tagText => {
console.log(tagText,this);
const {value} = this.data;
this.setData({
value: `${value} ${tagText} `
})
});
Тогда давайте посмотрим, что за ошибка
Мы видим, что в первый раз, чтобы войти на страницу, нажмите на вкладку, когда консоль печатает бумажную запись; во второй раз, чтобы войти на страницу, нажмите на метку, консоль напечатает дважды; нажмите, чтобы войти в третью метку, печатающую три записи но мы только щелкаем каждый раз, когда вы вводите ярлык, зачем вам печатать несколько записей.
В то же время, взглянув на три записи, выведенные на консоль при третьем нажатии на метку, мы можем обнаружить, что третья запись — это содержимое, отображаемое в нашем поле ввода, а вторая запись — это содержимое поля ввода. второе поле ввода плюс содержимое метки, на которую нажали в третий раз, чтобы войти на страницу; первая запись — это содержимое первого поля ввода, а также содержимое метки, на которую нажали второй и третий раз, чтобы войти на страницу.
Таким образом, мы можем быть уверены, что причина этой ошибки в том, что страница уничтожается, когда мы возвращаемся, но событие, на которое подписано, на этот раз не является отпиской, и в функции с подпиской есть ссылка на текущий компонент this, поэтому есть нажмите на метку , соответствующую случаю, когда событие запускается несколько раз.
Чтобы решить эту проблему, нам нужно отменить подписку на соответствующее время, когда текущая страница будет уничтожена, а компонент будет удален со страницы.
Отменить подписку на мероприятие
Суть отмены подписки на событие заключается в изменении функции подписки сevents[event]Это требует, чтобы функция, предоставляемая при подписке, и функция, предоставляемая при отмене подписки, были одинаковыми.Как убедиться, что он один и тот же, адрес памяти функции, указанный дважды, одинаков.
Метод отписки называетсяoff, также получает два параметра: первое имя события при подписке, а второй параметр — функция, предоставляемая при подписке.
(Чтобы код выглядел понятнее, я показываю только код ключа)
import bus from '../../utils/eventbus';
Component({
data: {},
lifetimes: {
ready() {
// 组件 ready 生命周期执行时进行订阅,订阅的方法是 this.handleTag
bus.on('sendTag', this.handleTag);
},
detached() {
// 组件销毁时进行取消订阅,方法同样是 this.handleTag
bus.off('sendTag', this.handleTag)
}
},
methods: {
handleTag(tagText) {
// 这里专门把 this 打印出来,是因为这里存在一个 this 指向的问题
console.log(tagText, this)
}
}
})
Давайте снова напишем шину (я показываю только код клавиши, чтобы код выглядел понятнее)
class Bus {
constructor() {
// events 是一个容器,里面放的是,各个事件和它的订阅者,数据格式如:
/**
this.events = {
sendTag: [cb1,cb2,cb3],
sendMsg: [cb4,cb5,cb6]
};
*/
// 当然也可以直接this.sendTag = [cb1,cb2,cb3];个人习惯不同,我更喜欢放的容器内
this.events = {};
}
// 取消订阅
off(Event,cb) {
// 如果要取消的这个事件不存在,说明这个事件一直都没有订阅者,直接 return ,什么都不需要做
if(!this.events[Event]) return
// 根据你提供的 你在订阅时提供的订阅函数 到所有的订阅函数中查找它所在索引
const index = this.events[Event].findIndex(item => item === cb);
// 如果要取消订阅的这个事件存在,但是根据你提供的函数,并没有在数组内查找到,说明你提供的函数并不是订阅者
// 那就给用户一个报错信息,让用户去检查下他的代码
if (index === -1) {
console.error(new Error('该 handle 没有订阅者,取消订阅失败'));
return;
}
// 如果找到了,直接根据索引删除
this.events[Event].splice(index, 1);
}
}
export default new Bus();
Давайте посмотрим на печать консоли в это время
измените это, чтобы указать на
Согласно печати консоли, при каждом нажатии на метку печатается только одна запись, свидетельствующая о том, что нам удалось отписаться; но печатьthisНо это не определено, это не определено, что означает, что мы не можем установить содержимое поля ввода, поэтому нам нужно изменить этот пункт функции подписки (чтобы код выглядел понятнее, я показываю только ключевой код)
import bus from '../../utils/eventbus';
Component({
data: {},
lifetimes: {
ready() {
// 通过 bind 修改 this 指向
bus.on('sendTag', this.handleTag.bind(this));
},
detached() {
// 通过 bind 修改 this 指向
bus.off('sendTag', this.handleTag.bind(this))
}
},
})
я используюbindизменить суть этого,Но вызов метода связывания вернет новую функцию. Я использую связывание для подписки, так и для отказа от подписки, поэтому новая функция возвращается каждый раз, что заставляет адреса памяти двух функций быть непоследовательными, и отказаться от подписки.Поэтому необходимоdataв определении свойства для полученияbindНовая возвращаемая функция (я показываю только код клавиши, чтобы код выглядел понятнее)
import bus from '../../utils/eventbus';
Component({
data: {
value: '',
_handle: undefined, // 接收bind返回的新函数
},
lifetimes: {
ready() {
this.setData({
// 将bind返回的新函数赋给 _handle
_handle: this.handleTag.bind(this)
})
// 将 _handle 提供给 on
bus.on('sendTag', this.data._handle);
},
detached() {
// 将 _handle 提供给 off ,订阅和取消订阅提供的同一个 _handle 内存地址一致,所以可以成功取消订阅
bus.off('sendTag', this.data._handle)
}
},
methods: {
handleInput({detail: {value}}) {
this.setData({value});
},
handleTag(tagText) {
console.log(tagText, this)
const {value} = this.data;
this.setData({
value: `${value} ${tagText} `
})
}
}
})
Наконец, давайте посмотрим на эффект
Оптимизация шины событий
Наши требования в основном выполнены, но чтобы сделать EventBus более удобным для пользователя, мы можем провести некоторые оптимизации.
-
Добавьте оценку анонимной функции при подписке на сообщение, потому что анонимные функции не могут быть отменены, поэтому, если пользователь предоставляет анонимную функцию, мы должны дать пользователю подсказку
class Bus { constructor() { this.events = {} } on(Event, cb) { if(this.events[Event]) { this.events[Event].push(cb); } else { this.events[Event] = [cb]; } // 如果是匿名函数就给用户个警告 if(!cb.name) { console.warn('on 接口的 handler 参数推荐使用具名函数。具名函数可以使用 off 接口取消订阅,匿名函数无法取消订阅。') } } emit(Event, obj) { if(!this.events[Event]) return this.events[Event].forEach(cb => { cb(obj) }) } off(Event,cb) { if(!this.events[Event]) return const index = this.events[Event].findIndex(item => item === cb); if (index === -1) { console.error(new Error('该 handle 没有订阅者,取消订阅失败')); return; } this.events[Event].splice(index, 1); } } export default new Bus; -
Поскольку временная подписка и отмена должны быть одним и тем же объектом, лучше добавить ограничение: класс Bus допускает только один объект-экземпляр.
class Bus { constructor() { this.events = {} } on(Event, cb) { if(this.events[Event]) { this.events[Event].push(cb); } else { this.events[Event] = [cb]; } // 如果是匿名函数就给用户个警告 if(!cb.name) { console.warn('on 接口的 handler 参数推荐使用具名函数。具名函数可以使用 off 接口取消订阅,匿名函数无法取消订阅。') } } emit(Event, obj) { if(!this.events[Event]) return this.events[Event].forEach(cb => { cb(obj) }) } off(Event,cb) { if(!this.events[Event]) return const index = this.events[Event].findIndex(item => item === cb); if (index === -1) { console.error(new Error('该 handle 没有订阅者,取消订阅失败')); return; } this.events[Event].splice(index, 1); } // 给这个类加一个静态方法,用来判断这个类之前有没有生成过对象 static getInstance() { if (!Bus.instance) { Bus.instance = new Bus() } return Bus.instance; } } // 导出这里返回 Bus 类静态 getInstance 的执行结果 export default Bus.getInstance();
Суммировать
Модуль EventBus в этой статье представляет собой типичный сценарий использования модели публикации-подписки, но он не ограничивается обменом данными между компонентами небольших программ, его можно использовать и в других подобных сценариях. модель публикации-подписки При разработке проекта мы можем гибко использовать шаблоны проектирования для решения проблем, с которыми мы сталкиваемся.
GitHub-адрес этой статьи:GitHub.com/Ло Кайбин/Я…
серия статей
руководство по деревне для начинающих на eggjs
Настройте набор шаблонов проектов Vue-Cli для собственной разработки
iframe архитектура микро интерфейс боевой
Проектирование структуры крупномасштабного фронтенд-проекта
Решение для управления Git для крупных интерфейсных проектов
Установка Linux и настройка nginx (1)
Как реализовать компонент подсказки сообщения (Vue), используемый вызовом js