1.先来看一个简单的例子
// 观察者classObserver{update(data){// 观察者收到数据变化,自行处理要做的事情console.log('接收到了数据:--',data);}}// 目标classSubject{constructor(){// 维护所有的观察者列表this.observers=[];}add(ob){// 添加观察者this.observers.push(ob);}notify(data){// 通知观察者数据发生变化for(constobofthis.observers){ob.update(data);}}}// 应用// 创建目标对象constsubject=newSubject();// 创建观察者constob1=newObserver();constob2=newObserver();// 给目标对象提那家观察者ob1,ob2subject.add(ob1);subject.add(ob2);// 通知所有观察者数据有变化啦subject.notify('hello,world,我来了')2.写一个ts版本的
interfaceIObserver{update(data:string):void;}// Log观察者classLogNotificationListenerimplementsIObserver{update(data:string):void{console.log('LogNotificationListener---',data);}}// Email观察者classEmailNotificationListenerimplementsIObserver{update(data:string):void{console.log('EmailNotificationListener---',data);}}classSubject{privateobservers:IObserver[]=[];// 添加观察者publicadd(ob:IObserver){this.observers.push(ob);}publicnotify(data:string):void{for(constobofthis.observers){// 调用观察者自身的更新方法ob.update(data);}}}// 创建一个发布者(商店)constsubject=newSubject();// 再创建两个观察者实例(有需求的顾客)constemailListener=newEmailNotificationListener();constlogListener=newLogNotificationListener();// 接下来发布者需要添加观察者// 这一步就相当于顾客订阅了商店的消息subject.add(emailListener);subject.add(logListener);// 商店发布消息,会向所有订阅了商店消息的顾客发送消息subject.notify("新来了华为Mate99 pro,欢迎大家前来订阅");3.说一下他的前端的实际应用吧
3.1dom事件的注册,这其实就是一种观察者模式,一个dom元素(发布者)可以有多个事件监听器(观察者)
<body><buttonid="mybtn">按钮</button><script>// 获取 DOM 元素(发布者)constbutton=document.getElementById('mybtn');// 第一个事件处理器,充当观察者的身份functionfirstObserver(){console.log('First observer responded to button click');}// 第二个事件处理器,同样也是充当观察者的身份functionsecondObserver(){console.log('Second observer responded to button click');}// 注册事件实际上就可以看作是发布者对观察者进行登记,或者说添加观察者的行为button.addEventListener('click',firstObserver);button.addEventListener('click',secondObserver);// 后期当用户真实的触发点击事件的时候,对应类型的所有的事件处理器都会被触发// 相当于就是发布者通知所有的观察者,观察者进行自身的一些行为</script></body>3.2MutationObserver,这是一个WebApi,它允许开发者监听DOM树的变化,包括元素的添加,删除,属性变化之类的,监听到变化之后,可以做出一些响应的行为
<body><ulid="myList"><li>Item1</li><li>Item2</li></ul><buttonid="addBtn">添加Item</button><buttonid="removeBtn">移除最后一个Item</button><buttonid="modifyBtn">修改最后一个Item</button><script>// 获取相应的 DOM 元素constmyList=document.getElementById("myList");constaddBtn=document.getElementById("addBtn");constremoveBtn=document.getElementById("removeBtn");constmodifyBtn=document.getElementById("modifyBtn");// 创建一个 MutationObserver 实例// mutationsList 是一个 MutationRecord 对象的数组// 每一个 MutationRecord 对象代表一个被观察到的 DOM 对象constobserver=newMutationObserver((mutationsList)=>{// 遍历这些被观察的 DOM 对象for(letmutationofmutationsList){if(mutation.type==="childList"){// 这里是 DOM 节点发生了改变console.log("A child node has been added or removed.");}elseif(mutation.type==="attributes"){// DOM 属性发生了改变console.log("The "+mutation.attributeName+" attribute was modified.");}}})// 接下来调用observer来进行观察// 该方法接收两个参数,第一个是要观察的DOM元素,第二个是一个配置对象observer.observe(myList,{attributes:true,// 会观察 DOM 元素的属性变化childList:true,// 会观察 DOM 元素的直接子节点变化subtree:true,// 会观察 DOM 元素的所有后代节点})// 后面就是对 DOM 元素进行操作addBtn.onclick=function(){constli=document.createElement("li");li.textContent="Item"+(myList.children.length+1);myList.appendChild(li);};removeBtn.onclick=function(){myList.removeChild(myList.lastElementChild);};modifyBtn.onclick=function(){myList.lastElementChild.setAttribute("style","color: red;");};</script></body>4.实现vue的迷你简单版的响应式系统
// 定义options的类型接口interfaceVueOptions{el:string;data:Record<string,any>;}// 观察者classWatcher{vm:Vue;// 表示Vue的实例对象el:Node// 代表一个DOM节点vmKey:string;// 存储data中的keyconstructor(vm:Vue,el:Node,vmKey:string){this.vm=vm;this.el=el;this.vmKey=vmKey;// 在第一次进行Watcher初始化的时候,将当前的Watcher对象保存到Dep.target上// 之所以要存储,是为了依赖收集Dep.target=this;// 先初始化更新一遍this.update();// 避免重复依赖收集,收集完依赖后,将Dep.target置空Dep.target=null;}// 更新方法update():void{//根据节点的类型来进行更新//这个例子是做了简化,只有两种类型if(this.el.nodeType===Node.TEXT_NODE){// 说明是一个文本类型节点,直接更新该节点的nodeValue// this.vm[this.vmKey]相当于是访问Vue实例对象的data中的属性// 后面我们会对data中的属性进行劫持,将data里面的所有数据存储到Vue实例对象上this.el.nodeValue=this.vm[this.vmKey];}elseif(this.el.nodeType==Node.ELEMENT_NODE){// 说明是一个元素节点,这里简化了,直接更新innerhtml(this.elasHTMLElement).innerHTML=this.vm[this.vmKey]}}}classDep{// 该静态属性用于暂时保存当前的Watcher对象,主要用于进行依赖的收集statictarget:Watcher|null=null;// 维护一个观察者的列表subs:Watcher[];constructor(){this.subs=[];}// 添加观察者到观察者列表addSub(sub:Watcher):void{this.subs.push(sub);}//通知所有观察者更新notify():void{this.subs.forEach(sub=>{sub.update();})}}// 该方法主要就是做数据劫持,将传递过来的data数据绑定到VUe实例对象上面,并且添加getter/setterfunctionobserver(vm:Vue,obj:Record<string,any>):void{constdep=newDep();// 实例化一个发布者// 遍历数据属性Object.keys(obj).forEach(key=>{// 首先,将原来的值先保存下来letinternalVal=obj[key];Object.defineProperty(vm,key,{get():any{// 如果有观察者,应该将观察者添加到发布者的观察者列表里面if(Dep.target){dep.addSub(Dep.target);}returninternalVal;},set(newVal:any):void{internalVal=newVal;// 数据发生了变化以后,我们就需要通知所有夫人观察者// 告诉观察者,数据发生变化,你们需要更新一下dep.notify();}});})}functioncompile(vm:Vue):void{// 首先拿到Vue实例对象上面的el属性,这个属性是一个选择器// 这一步其实就是拿到最外层的DOM节点 <div id="app"></div>constel:HTMLElement|null=document.querySelector(vm.$el);if(!el){thrownewError("Element with selector can not be found.");}// 接下来创建一个文档碎片constdocumentFragment:DocumentFragment=document.createDocumentFragment();//对节点进行处理,使用正则匹配{{ }},因为猫须字符串会成为一个观察者constreg:RegExp=/\{\{(.*)\}\}/;while(el.firstChild){//拿到第一个子节点,然后我们会进行各种分析处理constchild:ChildNode=el.firstChild;// 接下来对子节点进行分析操作if(child.nodeType===Node.ELEMENT_NODE){// 说明是一个元素节点constelement=childasHTMLElement;if(reg.test(element.innerHTML)){// 说明里面是带猫须的,需要将其变成一个观察者constvmKey:string=RegExp.$1.trim();// $1 是正则表达式匹配到的第一个值,这里其实就是 msgnewWatcher(vm,child,vmKey);}else{//说明里面没有猫须,我们还需要判断这个元素节点的属性是否有v-model// 如果有v-model 也需要进行处理Array.from(element.attributes).forEach(attr=>{if(attr.name==='v-model'){//说明如果进入此分支,说明该元素节点的属性包含v-modelconstvmKey:string=attr.value;// 这里其实就是msgelement.addEventListener('input',(ev:Event)=>{consttarget=ev.targetasHTMLInputElement;// 这里其实就是将文本框所输入的值赋值给vm实例对象上面的msg属性vm[vmKey]=target.value;})}})}}elseif(child.nodeType===Node.TEXT_NODE&®.test(child.nodeValue||'')){// 说明这是一个文本节点,并且这个文本节点也是带猫须的// 那么我们需要将这个文本节点转换为一个观察者constvmKey:string=RegExp.$1.trim();// $1 是正则表达式匹配到的第一个值,这里其实就是 msgnewWatcher(vm,child,vmKey);}//处理完成之后,我们就会将其添加到文档碎片中//当我们讲一个已有节点添加到另一个节点下面的时候做的是一个移动的操作documentFragment.appendChild(child);}// 因此当退出上面的循环时,el应该是一个空节点// 所有子节点都放进了文档碎片中// 我们再见文档中的所有子节点重新添加回到el中el.appendChild(documentFragment);}classVue{$el:string;[key:string]:any;constructor(options:VueOptions){this.$el=options.el;observer(this,options.data);// 做数据劫持,将data上面的数据存储到vue的实例对象上面compile(this);// 对模版进行编译}}// 实例化Vue时,传入一个配置对象constoptions={el:'#app',data:{msg:'hello Vue!'}}newVue(options);4.1上面是一个index.ts文件,把它使用tsc index.ts编译成index.js,然后在html文件中引入该文件,自行验证
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><title>Document</title></head><body><divid="app"><div>{{msg}}</div><inputtype="text"v-model="msg"/><p>this is a test</p>{{msg}}</div><scripttype="module"src="./index.js"></script></body></html>浏览器预览效果如下
非原创,来源渡一谢杰老师的设计模式讲解,简单记录分享