Шаблон наблюдателя в шаблонах проектирования JavaScript

внешний интерфейс Шаблоны проектирования JavaScript Vue.js

хм~~~

Откройте дверь, на этот раз я не продаю Guan Zi, сегодня мы поговорим о режиме дизайна Javassript.Шаблон наблюдателя, прежде всего давайте узнаем, что такое паттерн Observer?

Что такое шаблон наблюдателя?

Шаблон наблюдателя (наблюдатель)

обычно называютшаблон публикации-подпискиилимеханизм сообщений, который определяет зависимость между объектами «один ко многим». Пока состояние объекта изменяется, все объекты, которые зависят от него, уведомляются и автоматически обновляются, что решает функцию между объектом-субъектом и наблюдателем. то есть проблема уведомления других объектов об изменении состояния объекта.

Просто глядя на определение, для фронтенд-друзей это понятие может быть относительно расплывчатым, и я все еще немного понимаю режим наблюдателя, хорошо, тогда я рассмотрю более подходящий пример из жизни, я думаю, вы поймете. сразу понял~

Модель наблюдателя в жизни

Каждый раз, когда Xiaomi выпускает новый мобильный телефон, он продается очень быстро.Мне понравился Xiaomi Mi 3, и я хотел пойти в Xiaomi Home, чтобы купить его, но когда я пришел в магазин, продавец сказал мне, что этот телефон очень популярен. .Есть в наличии, то я не могу приходить и спрашивать каждый день, это очень долго, поэтому я оставляю свой номер мобильного телефона продавщице, если у них есть запас в магазине, пусть она мне позвонит и сообщит , так что вам не нужно беспокоиться о том, что вы не знаете, когда он будет в наличии, и вам не нужно каждый день бегать спрашивать. Если вы успешно купили мобильный телефон, то продавщица не нужно уведомить вас~

Разве это не намного яснее~ Таких случаев еще много, поэтому я не буду их повторять.

Использование шаблона наблюдателя

По правде говоря, я могу гарантировать, что все, кто придет посмотреть, использовали режим наблюдателя~

что, не веришь?

Тогда взгляните на следующий код~

    document.querySelector('#btn').addEventListener('click',function () {
        alert('You click this btn');
    },false)

Что, разве это не выглядит знакомым!

Да, мы обычноDOMПривязка событий является очень типичнымшаблон публикации-подписки, здесь нам нужно отслеживать действие пользователя, нажимающего кнопку, но мы не можем знать, когда пользователь нажимает, поэтому мы подписываемся на кнопку наclickсобытие, пока кнопка нажата, кнопка опубликует сообщение для подписчика, и мы можем выполнить соответствующую операцию.

Помимо нашего обычногоDOMВ дополнение к привязке событий существует множество применений шаблона Observer~

Например, сравните текущие популярныеvueFramework, который использует шаблон наблюдателя во многих местах, например:

Двустороннее обязательство данных

использоватьObject.defineProperty()Захватите данные и настройте прослушивательObserver, используется для мониторинга всех атрибутов, при изменении атрибута нужно сообщить подписчикуWatcherобновить данные и, наконец, дать указание парсеруCompileРазбирает соответствующую инструкцию, а затем выполняет соответствующую функцию обновления, тем самым обновляя представление и реализуя двустороннюю привязку~

Дочерний компонент взаимодействует с родительским компонентом

Vueв котором мы проходимpropsПередача данных родительского компонента завершена, родительские компонентные подсказки и подбаллии нас для связи через пользовательское событие, которое$on,$emitдобиться, по сути, через$emitпубликовать сообщения и подписчикам$onСделайте унифицированную обработку ~

Хорошо, сказав так много, пора нам показать свои руки.Далее, давайте создадим простой наблюдатель сами~

создать наблюдателя

Во-первых, нам нужно создать объект-наблюдатель, который содержит контейнер сообщений и три метода, а именно метод сообщения о подписке.on, метод сообщения отпискиoff, отправить сообщение о подпискеsubscribe.

    const Observe = (function () {
    	//防止消息队列暴露而被篡改,将消息容器设置为私有变量
    	let __message = {};
    	return {
        	//注册消息接口
            on : function () {},
            //发布消息接口
    		subscribe : function () {},
            //移除消息接口
            off : function () {}
        }
    })();

Ок, наш прототип наблюдателя вышел, осталось улучшить три метода внутри~

метод регистрации сообщения

Функция метода зарегистрированного сообщения состоит в том, чтобы отправить сообщение, зарегистрированное подписчиком, в очередь сообщений, поэтому необходимо передать два параметра: тип сообщения и соответствующую функцию обработки.Если он существует, создайте тип сообщения и поместите сообщение в очередь сообщений. Если сообщение уже существует, поместите соответствующий метод в очередь методов выполнения.

    //注册消息接口
    on: function (type, fn) {
        //如果此消息不存在,创建一个该消息类型
        if( typeof __message[type] === 'undefined' ){
        	// 将执行方法推入该消息对应的执行队列中
            __message[type] = [fn];
        }else{
        	//如果此消息存在,直接将执行方法推入该消息对应的执行队列中
            __message[type].push(fn);
        }
    }

Опубликовать метод сообщения

Объявил, что его функция - опубликовать сообщение, когда наблюдатель состоит в том, чтобы все подписчики подписывайтесь, что последовательно выполнено подписчики, необходимо пройти два параметра, а именно соответствующие типы сообщений и параметры, необходимые для выполнения функции, в которой требуется тип сообщения.

    //发布消息接口
    subscribe: function (type, args) {
    	//如果该消息没有注册,直接返回
    	if ( !__message[type] )  return;
    	//定义消息信息
    	let events = {
        	type: type,           //消息类型
        	args: args || {}       //参数
        },
        i = 0,                         // 循环变量
        len = __message[type].length;   // 执行队列长度
    	//遍历执行函数
    	for ( ; i < len; i++ ) {
    		//依次执行注册消息对应的方法
            __message[type][i].call(this,events)
    	}
    }

удалить метод сообщения

Функция удаления метода сообщения состоит в том, чтобы очистить сообщение о том, что подписчик вышел из очереди сообщений, а также передать два параметра, тип сообщения и функцию в очереди выполнения. Здесь, чтобы избежать ситуации, когда сообщение не существует при удалении, необходимо проверить существование сообщения.

    //移除消息接口
    off: function (type, fn) {
    	//如果消息执行队列存在
    	if ( __message[type] instanceof Array ) {
    		// 从最后一条依次遍历
    		let i = __message[type].length - 1;
    		for ( ; i >= 0; i-- ) {
    			//如果存在改执行函数则移除相应的动作
    			__message[type][i] === fn && __message[type].splice(i, 1);
    		}
    	}
    }

хорошо, на данный момент мы реализовали базовую модель наблюдателя, и теперь пришло время показать наши навыки~ Возьмите его и протестируйте!

Покажи свои таланты

Прежде всего, давайте проведем простой тест, чтобы увидеть, насколько хорошо работает шаблон наблюдателя, который мы создали сами?

   //订阅消息
    Observe.on('say', function (data) {
    	console.log(data.args.text);
    })
    Observe.on('success',function () {
        console.log('success')
    });
    
    //发布消息
    Observe.subscribe('say', { text : 'hello world' } )
    Observe.subscribe('success');  

Мы находимся в типе сообщения какsayВ сообщениях, зарегистрированных в сообщении два метода, один из которых принимает параметры, другой не требует параметров, а затем проходитsubscribeвыпускатьsayа такжеsuccessсообщение, результат такой, как мы и ожидали, вывод консолиhello worldтак же какsuccess ~

Смотреть! Мы успешно реализовали наш наблюдатель ~ поставьте себе лайк!

Двухстороннее связывание пользовательских данных

Как указано выше,vueДвусторонняя привязка реализована путем перехвата данных и публикации-подписки.Теперь мы используем эту идею, чтобы реализовать простую двустороннюю привязку данных самостоятельно~

В первую очередь, конечно, должна быть структура страницы, мне здесь все равно, я просто закодирую~

<div id="app">
    <h3>数据的双向绑定</h3>
    <div class="cell">
        <div class="text" v-text="myText"></div>
        <input class="input" type="text" v-model="myText" >
    </div>
</div>

Я полагаю, вы уже знаете, что мы должны сделать этоinputввод этикетки, черезv-textпривязать к классу с именемtextизdivна этикетке~

Для начала нам нужно создать класс, здесь он называетсяmyVueБар.

class myVue{
    constructor (options){
        // 传入的配置参数
        this.options = options;
        // 根元素
        this.$el = document.querySelector(options.el);
        // 数据域
        this.$data = options.data;
        
        // 保存数据model与view相关的指令,当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
        this._directives = {};
        // 数据劫持,重新定义数据的 set 和 get 方法
        this._obverse(this.$data);
        // 解析器,解析模板指令,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
        this._compile(this.$el);
    }
}

Здесь мы определяемmyVueконструктор, а некоторые операции инициализации выполняются в методе построения, который аннотирован выше, я не буду их повторять здесь, в основном, чтобы посмотреть на два ключевых метода в нем_obverseа также_compile.

прежде всего_observeметод, его роль заключается в обработке входящихdata, и переопределитьdataизsetа такжеgetспособ убедиться, что мы находимся вdataКогда происходят изменения, их можно отслеживать и уведомлять, в основном используетсяObject.defineProperty()Этот метод, друзья, которые не знакомы с этим методом, пожалуйстакликните сюда~

_observe

    //_obverse 函数,对data进行处理,重写data的set和get函数
    _obverse(data){
    	let val ;
    	//遍历数据
        for( let key in data ){
            // 判断是不是属于自己本身的属性
            if( data.hasOwnProperty(key) ){
            	this._directives[key] = [];
            }
        
            val = data[key];        
            //递归遍历
            if ( typeof val === 'object' ) {
            	//递归遍历
            	this._obverse(val);
            }
            
            // 初始当前数据的执行队列
            let _dir = this._directives[key];
        
            //重新定义数据的 get 和 set 方法
            Object.defineProperty(this.$data,key,{
            	enumerable: true,
            	configurable: true,
            	get: function () {
            		return val;
            	},
            	set: function (newVal) {
            		if ( val !== newVal ) {
            			val = newVal;
            			// 当 myText 改变时,触发 _directives 中的绑定的Watcher类的更新
            			_dir.forEach(function (item) {
            			    //调用自身指令的更新操作
            				item._update();
            			})
            		}
            	}
            })
        }
    }

Приведенный выше код также очень прост, и комментарии очень понятны, но есть проблема, заключающаяся в том, что при рекурсивном обходе данных я украл немного лени — здесь я задействую только некоторые простые структуры данных, такие как сложные циклы, которые я не делал. не рассматривайте возможность ввода такого рода ссылки, вы можете добавить ее самостоятельно~

Давайте взглянем_compileЭтот метод, который на самом деле является синтаксическим анализатором, функция которого заключается в разборе инструкций шаблона, привязке узла, соответствующего каждой инструкции, к функции обновления, добавлении подписчиков для мониторинга данных, получении уведомления при изменении данных, а затем переходе к обновлению. просмотреть изменения, конкретная реализация выглядит следующим образом:

_compile

_compile(el){
    //子元素
    let nodes = el.children;
    for( let i = 0 ;  i < nodes.length ; i++ ){
    	let node = nodes[i];
    	// 递归对所有元素进行遍历,并进行处理
    	if( node.children.length ){
    		this._compile(node);
    	}
    
        //如果有 v-text 指令 , 监控 node的值 并及时更新
        if( node.hasAttribute('v-text')){
            let attrValue = node.getAttribute('v-text');
            //将指令对应的执行方法放入指令集
            this._directives[attrValue].push(new Watcher('text',node,this,attrValue,'innerHTML'))
        }
    
    	//如果有 v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
        if( node.hasAttribute('v-model') && ( node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){
            let _this = this;
            //添加input时间
            node.addEventListener('input',(function(){
            	let attrValue = node.getAttribute('v-model');
            	//初始化赋值
            	_this._directives[attrValue].push(new Watcher('input',node,_this,attrValue,'value'));
                return function () {
                    //后面每次都会更新
                    _this.$data[attrValue] = node.value;
            	}
            })())
        }
    }
}

Приведенный выше код тоже очень понятен, мы начинаем с корневого элемента#appНачните рекурсивно проходить каждый узел и определите, есть ли у каждого узла соответствующая инструкция, здесь мы сосредоточимся только наv-textа такжеv-model,Мыv-textвыполнено один разnew Watcher(), и вставьте егоmyTextВ инструкции даv-modelтакже был проанализирован, гдеinputграницаinputсобытие и передать его черезnew Watcher()а такжеmyTextсвязаны, то мы должны посмотреть на этоWatcherчто именно?

WatcherЭто на самом деле подписчик, да_observerа также_compileМост связи используется для привязки функции обновления для реализацииDOMобновление элемента

Warcher

class Watcher{
    /*
    * name  指令名称,例如文本节点,该值设为"text"
    * el    指令对应的DOM元素
    * vm    指令所属myVue实例
    * exp   指令对应的值,本例如"myText"
    * attr  绑定的属性值,本例为"innerHTML"
    * */
    constructor (name, el, vm, exp, attr){
        this.name = name;
        this.el = el;
        this.vm = vm;
        this.exp = exp;
        this.attr = attr;
    
        //更新操作
        this._update();
    }
    
    _update(){
    	this.el[this.attr] = this.vm.$data[this.exp];
    }
}

каждый раз, когда вы создаетеWatcher, будут переданы соответствующие параметры, и он также будет выполнен один раз._updateоперация, выше_compile, мы создаем дваWatcherпример, но два соответствующих_updateОперация отличается, т.div.textОперация фактически эквивалентнаdiv.innerHTML=h3.innerHTML = this.data.myText, дляinputэквивалентноinput.value=this.data.myText, чтобы каждый раз данныеset, мы вызовем два_updateработа, обновление отдельноdivа такжеinputв содержании~

Без дальнейших ADO давайте проверим это ~

Сначала инициализируйте его~

    //创建vue实例
    const app = new myVue({
        el : '#app' ,
        data : {
            myText : 'hello world'
        }
    })

Далее, картинка выше~

Мы успешно реализовали простую двустороннюю привязку, отлично~

Эпилог

Теперь у вас есть более глубокое понимание шаблона наблюдателя? На самом деле, я так много сказал здесь, но это играет роль только в привлечении новых идей.Важным моментом является дизайнерская идея.Чтобы научиться разумно применять эту дизайнерскую идею к нашему фактическому процессу разработки, процесс может быть трудным, но на бумаге я стал чувствовать себя мелким, и я знаю, что это дело должно быть сделано, все, давайте~

Да, кстати, сегодня 1024 год, всех с праздником~