1.先来看一个简单的例子
javascript
// 观察者
class Observer {
update(data){
// 观察者收到数据变化,自行处理要做的事情
console.log('接收到了数据:--',data);
}
}
// 目标
class Subject {
constructor(){
// 维护所有的观察者列表
this.observers = [];
}
add(ob){
// 添加观察者
this.observers.push(ob);
}
notify(data){
// 通知观察者数据发生变化
for(const ob of this.observers){
ob.update(data);
}
}
}
// 应用
// 创建目标对象
const subject = new Subject();
// 创建观察者
const ob1 = new Observer();
const ob2 = new Observer();
// 给目标对象提那家观察者ob1,ob2
subject.add(ob1);
subject.add(ob2);
// 通知所有观察者数据有变化啦
subject.notify('hello,world,我来了')
2.写一个ts版本的
typescript
interface IObserver {
update(data:string):void;
}
// Log观察者
class LogNotificationListener implements IObserver {
update(data: string): void {
console.log('LogNotificationListener---',data);
}
}
// Email观察者
class EmailNotificationListener implements IObserver{
update(data: string): void {
console.log('EmailNotificationListener---',data);
}
}
class Subject {
private observers:IObserver[]=[];
// 添加观察者
public add(ob:IObserver){
this.observers.push(ob);
}
public notify(data:string):void{
for(const ob of this.observers){
// 调用观察者自身的更新方法
ob.update(data);
}
}
}
// 创建一个发布者(商店)
const subject = new Subject();
// 再创建两个观察者实例(有需求的顾客)
const emailListener = new EmailNotificationListener();
const logListener = new LogNotificationListener();
// 接下来发布者需要添加观察者
// 这一步就相当于顾客订阅了商店的消息
subject.add(emailListener);
subject.add(logListener);
// 商店发布消息,会向所有订阅了商店消息的顾客发送消息
subject.notify("新来了华为Mate99 pro,欢迎大家前来订阅");
3.说一下他的前端的实际应用吧
3.1dom事件的注册,这其实就是一种观察者模式,一个dom元素(发布者)可以有多个事件监听器(观察者)
xml
<body>
<button id="mybtn">按钮</button>
<script>
// 获取 DOM 元素(发布者)
const button = document.getElementById('mybtn');
// 第一个事件处理器,充当观察者的身份
function firstObserver(){
console.log('First observer responded to button click');
}
// 第二个事件处理器,同样也是充当观察者的身份
function secondObserver(){
console.log('Second observer responded to button click');
}
// 注册事件实际上就可以看作是发布者对观察者进行登记,或者说添加观察者的行为
button.addEventListener('click',firstObserver);
button.addEventListener('click',secondObserver);
// 后期当用户真实的触发点击事件的时候,对应类型的所有的事件处理器都会被触发
// 相当于就是发布者通知所有的观察者,观察者进行自身的一些行为
</script>
</body>
3.2MutationObserver,这是一个WebApi,它允许开发者监听DOM树的变化,包括元素的添加,删除,属性变化之类的,监听到变化之后,可以做出一些响应的行为
xml
<body>
<ul id="myList">
<li>Item1</li>
<li>Item2</li>
</ul>
<button id="addBtn">添加Item</button>
<button id="removeBtn">移除最后一个Item</button>
<button id="modifyBtn">修改最后一个Item</button>
<script>
// 获取相应的 DOM 元素
const myList = document.getElementById("myList");
const addBtn = document.getElementById("addBtn");
const removeBtn = document.getElementById("removeBtn");
const modifyBtn = document.getElementById("modifyBtn");
// 创建一个 MutationObserver 实例
// mutationsList 是一个 MutationRecord 对象的数组
// 每一个 MutationRecord 对象代表一个被观察到的 DOM 对象
const observer = new MutationObserver((mutationsList)=>{
// 遍历这些被观察的 DOM 对象
for (let mutation of mutationsList) {
if (mutation.type === "childList") {
// 这里是 DOM 节点发生了改变
console.log("A child node has been added or removed.");
} else if (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 () {
const li = 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的迷你简单版的响应式系统
typescript
// 定义options的类型接口
interface VueOptions {
el:string;
data:Record<string,any>;
}
// 观察者
class Watcher {
vm:Vue; // 表示Vue的实例对象
el:Node // 代表一个DOM节点
vmKey: string; // 存储data中的key
constructor(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];
}else if(this.el.nodeType==Node.ELEMENT_NODE){
// 说明是一个元素节点,这里简化了,直接更新innerhtml
(this.el as HTMLElement).innerHTML = this.vm[this.vmKey]
}
}
}
class Dep {
// 该静态属性用于暂时保存当前的Watcher对象,主要用于进行依赖的收集
static target: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/setter
function observer(vm:Vue,obj:Record<string,any>):void{
const dep=new Dep(); // 实例化一个发布者
// 遍历数据属性
Object.keys(obj).forEach(key=>{
// 首先,将原来的值先保存下来
let internalVal = obj[key];
Object.defineProperty(vm,key,{
get():any{
// 如果有观察者,应该将观察者添加到发布者的观察者列表里面
if(Dep.target){
dep.addSub(Dep.target);
}
return internalVal;
},
set(newVal:any):void{
internalVal=newVal;
// 数据发生了变化以后,我们就需要通知所有夫人观察者
// 告诉观察者,数据发生变化,你们需要更新一下
dep.notify();
}
});
})
}
function compile(vm:Vue):void{
// 首先拿到Vue实例对象上面的el属性,这个属性是一个选择器
// 这一步其实就是拿到最外层的DOM节点 <div id="app"></div>
const el:HTMLElement | null = document.querySelector(vm.$el);
if(!el){
throw new Error("Element with selector can not be found.");
}
// 接下来创建一个文档碎片
const documentFragment: DocumentFragment = document.createDocumentFragment();
//对节点进行处理,使用正则匹配{{ }},因为猫须字符串会成为一个观察者
const reg:RegExp = /\{\{(.*)\}\}/;
while(el.firstChild){
//拿到第一个子节点,然后我们会进行各种分析处理
const child:ChildNode = el.firstChild;
// 接下来对子节点进行分析操作
if(child.nodeType === Node.ELEMENT_NODE){
// 说明是一个元素节点
const element = child as HTMLElement;
if(reg.test(element.innerHTML)){
// 说明里面是带猫须的,需要将其变成一个观察者
const vmKey:string = RegExp.$1.trim();// $1 是正则表达式匹配到的第一个值,这里其实就是 msg
new Watcher(vm,child,vmKey);
}else{
//说明里面没有猫须,我们还需要判断这个元素节点的属性是否有v-model
// 如果有v-model 也需要进行处理
Array.from(element.attributes).forEach(attr=>{
if(attr.name==='v-model'){
//说明如果进入此分支,说明该元素节点的属性包含v-model
const vmKey:string = attr.value; // 这里其实就是msg
element.addEventListener('input',(ev:Event)=>{
const target = ev.target as HTMLInputElement;
// 这里其实就是将文本框所输入的值赋值给vm实例对象上面的msg属性
vm[vmKey] = target.value;
})
}
})
}
}else if(child.nodeType === Node.TEXT_NODE && reg.test(child.nodeValue||'')){
// 说明这是一个文本节点,并且这个文本节点也是带猫须的
// 那么我们需要将这个文本节点转换为一个观察者
const vmKey:string = RegExp.$1.trim();// $1 是正则表达式匹配到的第一个值,这里其实就是 msg
new Watcher(vm,child,vmKey);
}
//处理完成之后,我们就会将其添加到文档碎片中
//当我们讲一个已有节点添加到另一个节点下面的时候做的是一个移动的操作
documentFragment.appendChild(child);
}
// 因此当退出上面的循环时,el应该是一个空节点
// 所有子节点都放进了文档碎片中
// 我们再见文档中的所有子节点重新添加回到el中
el.appendChild(documentFragment);
}
class Vue {
$el:string;
[key:string]:any;
constructor(options:VueOptions){
this.$el = options.el;
observer(this,options.data); // 做数据劫持,将data上面的数据存储到vue的实例对象上面
compile(this); // 对模版进行编译
}
}
// 实例化Vue时,传入一个配置对象
const options = {
el:'#app',
data:{
msg:'hello Vue!'
}
}
new Vue(options);
4.1上面是一个index.ts文件,把它使用tsc index.ts编译成index.js,然后在html文件中引入该文件,自行验证
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<div>{{msg}}</div>
<input type="text" v-model="msg"/>
<p>this is a test</p>
{{msg}}
</div>
<script type="module" src="./index.js"></script>
</body>
</html>
浏览器预览效果如下


非原创,来源渡一谢杰老师的设计模式讲解,简单记录分享