导航
[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式
[封装03-设计模式] Decorator 装饰器模式在前端的应用
[封装04-设计模式] Publish Subscribe 发布订阅模式在前端的应用
[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署
[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] koa
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend
[源码-vue06] Vue.nextTick 和 vm.$nextTick
[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现
[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI
[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber
[深入25] Typescript
[深入26] Drag
[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon
复习笔记-01
复习笔记-02
复习笔记-03
复习笔记-04
前置知识
(1) 一些单词
broadcast 广播
channel 通道 // new BroadcastChannel(channelName)
(2) function.name
- 定义:函数的name属性返回
函数的名字 - 应用:
当一个函数作为另一个函数的参数时,获取参数函数的名字
1. 基本情况
function a() {}
const b = function(){}
const c = function myName(){}
a.name // 'a'
b.name // 'b'
c.name // 'myName' 使用变量赋值时,如果函数有具名函数,返回 function后面的函数名
2. 应用
function test(f) {
console.log(f.name); // 获取参数函数的名字
}
(3) 观察者模式
- 定义:对程序中的某个对象进行观察,并在对象发生变化时接收到通知
- 角色
- subject 目标对象
- observer 观察者对象
- 关系
- 一个目标对象 -> 可以被多个观察者对象观察
- (
观察者对象) 在目标对象上 (订阅事件),(目标对象) 负责向观察者对象 (广播事件)
- 观察者模式和发布订阅模式的比较
- 耦合性
- 观察者模式中,目标对象中具有观察者对象数组,目标对象维护观察者对象组成的数组
- 发布订阅模式中,发布者是不需要自己维护订阅者数组的,而是通过中介完成,完全解耦
- 使用上
- 观察者模式,多用于单个应用内部
- 发布订阅模式,则更多的是一种跨应用的模式(cross-application pattern)
- 耦合性
Subject 目标对象
- 维护一个(观察者实例对象)组成的数组,并且具有( 添加,删除,通知 ) 操作该数组的各种方法
Observer 观察者对象
- 仅仅只需要维护收到通知后( 更新 )操作的方法
存在问题:
- 因为 Subject 目标对象中维护了 Observer 组成的数组,所以存在耦合性
解决问题
- 可以使用发布订阅模式,全权由 EventChannel 来互责维护,订阅,和取消订阅
---
案例 观察者模式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
function Subject() {
// 目标对象的构造函数
this.observers = []; // 观察者实例对象组成的数组
}
Subject.prototype = {
// 目标对象上具有操作 ( 观察者对象数组 ) 的方法,比如 ( 添加,删除,通知 )
add(...observers) {
if (observers?.length) {
this.observers = this.observers.concat(observers);
}
},
delete(...observers) {
if (this.observers.length) {
observers.forEach((observer) => {
const index = this.observers.findIndex(
(item) => observer === item
);
if (index > -1) {
this.observers.splice(index, 1);
}
});
}
},
notify() {
this.observers.forEach((observer) => observer?.update());
},
};
Subject.prototype.constructor = Subject;
function Observer(fn, params) {
this.update = function () {
return fn.call(this, params);
};
}
function o(p) {
console.log(p);
}
const observer1 = new Observer(o, "observer1");
const observer2 = new Observer(o, "observer2");
const subject = new Subject();
subject.add(observer1, observer2);
subject.delete(observer1);
subject.notify();
</script>
</body>
</html>
(一) 多页面通信
- 同源多页面通信
- new BroadcastChannel()
- storage事件
- 非同源多页面通信 - 跨域通信
- otherWindow.postMessage()
(1) 同源 - 多页面通信 - BroadcastChannel
const bc = new BroadcastChannel('channel_name')- 参数:表示频道,通信的双方必须是同一个频道才可以
发送消息:bc.postMessage(obj)接收消息:bc.onmessage = function (e) { e.data }断开链接:bc.close()- 定义
- 可以让指定 origin 下的任意 browsing context 来订阅它
- 它允许
同源的不同浏览器窗口,Tab页,frame或者iframe下的不同文档之间相互通信 - 全双工(双向)通信
- 应用
不同标签页之间的通信
- 案例源码地址
// 连接到广播频道
// 可以在多个tab中多次实例话,双方通信只需要保证参数一样,即同一个频道
var bc = new BroadcastChannel('test_channel');
// 发送消息
bc.postMessage('This is a test message.');
// 接收消息
// 当消息被发送之后,所有连接到该频道的 BroadcastChannel 对象上都会触发 `message` 事件
// ev对象上的属性
// - currentTarget.name:当前加入的广播频道
// - data:发送的消息
// - origin:当前所在源
bc.onmessage = function (ev) { console.log(ev); }
// 断开频道连接
bc.close()
(2) 非同源跨域 - 多页面通信 - otherWindow.postMessage()
- 特点
非同源:表示具有跨域能力安全性:安全性没有 BroadcastChannel 好
- otherWindow.postMessage(message, targetOrigin, [transfer])
- otherWindow
- 其他窗口的一个引用
iframe ------> contentWindow 属性window.open -> 返回的新窗口对象命名过或数值索引的 window.frames
- message
- 将要发送到其他 window 的数据
- targetOrigin
- 通过窗口的origin属性来指定哪些窗口能接收到消息事件
- 其值可以是字符串"*"(表示无限制)
- 或者一个URI,但必须是启动的server服务的地址,不能是本地静态文件
- transfer
- transfer 是一串和 message 同时传递的
Transferable对象 - 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
- transfer 是一串和 message 同时传递的
- 注意
- 在 targetWindow.open() 后,要等到目标页面加载完成才能进行 postMessage 跨域通信,但是在跨域的情况下,无法对目标窗口进行onload监听
- 1.所以可以用 setTimeout 延时
- 2.也可以通过一个按钮去打开新的页面,等新开的页面加载完,然后手动的再发送消息
- otherWindow
- iframe 相关
- 元素:
<iframe />能够将另一个HTML页面嵌入到当前页面 - 发消息:
HTMLIFrameElement.contentWindow.postMessage(message, targetOrigin)
- 收消息:
window.addEventListener("message",()=>{})window.onmessage = () => {}
- 元素:
- window.open 相关
var window = window.open(url, windowName, [windowFeatures])- 返回值
- window.open() 返回值是一个 ( 窗口对象 )
- 该窗口对象就是 otherWindow.postMessage() 时的 otherWindow
- 参数
- url:打开的新页面的url服务器地址
- windowName:窗口名称
- 返回值
- 案例源码地址
(一)
otherWindow.postMessage(message, targetOrigin, [transfer]) 跨域通信
(1) otherWindow
- otherWindow指的是其他窗口的一个引用
1. iframe 的 contentWindow 属性
2. window.open() 返回的一个窗口对象
3. 命名过或数值索引的 window.frames
4. 两个窗口之间
- a -> b,otherWindow是b窗口
- b -> a,otherWindow是a窗口,即 ( top ) 或者 ( parent )
(2) message
- message指发送给其他窗口的数据
- message会被序列化,所以无需自己序列化
(3) targetOrigin !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- targetOrigin:设置目标窗口的源(协议域名端口组成的字符串),指定哪些窗口能 ( 接收 ) 到消息事件
- 在 ( 发消息 ) 的时候,如果( 目标窗口 ) 的 ( 协议,域名,端口) 任意一项不满足 targetOrigin 提供的值,消息就不会发送
- 三者要全部匹配才会发送
- targetOrigin的值可以是 ( * ) 号,表示所有窗口都能接收到消息
(4) transfer
- transfer 是一串和message同时传递的 Transferable 对象
- 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权
发消息:--------------- otherWindow.postMessage(message, targetOrigin, [transfer])
收消息:--------------- window.addEventListener('message', (data) => {console.log(data.data, data.origin)}, false)
收消息:--------------- window.onmessage = (data) => {...}
注意:----------------- 通过 ( targetOrigin - 验证接收方 ) 和 ( data.origin - 验证发送方 ) 来精确通信
- data.origin 和 data.source 和 data.data
- 在接收端的监听函数中,注意 origin 和 source
- origin: ------------- 发送方的协议,域名,端口组成的字符串
- source:------------- 发送方窗口对象的引用
- data:--------------- 接收到的数据
window.addEventListener("message", receiveMessage, false); // 接收消息的tab标签页面监听message事件
function receiveMessage(event) {
// For Chrome, the origin property is in the event.originalEvent
// object.
// 这里不准确,chrome没有这个属性
// var origin = event.origin || event.originalEvent.origin;
var origin = event.origin
if (origin !== "http://example.org:8080")
return;
// ...
}
(二)
注意事项:
- 使用postMessage将数据发送到其他窗口时,始终要指定精确的目标origin,而不是使用 *
- 使用 origin 和 source 验证 ( 发件人 ) 的身份
(三)
实例:
--------------
a页面
<!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="a-open">点击,打开b页面</div>
<div id="a-send">点击,发送消息,a->b</div>
<script>
const aOpen = document.getElementById('a-open')
const aSend = document.getElementById('a-send')
var a = null
aOpen.addEventListener('click', () => a = window.open('http://127.0.0.1:5500/b.html'), false)
aSend.addEventListener('click', () => a.postMessage('this message is a to b', 'http://127.0.0.1:5500'), false)
// 注意:
// 1. a.postMessage只有在目标页面(b页面)的页面加载完成时才能发送
// 2. a.postMessage的第二个参数,表示targetOrigin目标源,即目标窗口的协议域名端口组成的字符串
// 3. targetOrigin设置过后,只有目标窗口完全符合targetOrigin字符串的值才能接收到消息
</script>
</body>
</html>
---------------
b页面
<!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>b页面</div>
<script>
window.addEventListener('message', (data) => { // ----- 监听message事件
console.log(data)
}, false)
</script>
</body>
</html>
(3) 同源 - 多页面通信 - storage事件
- 原理
- 当
localStorage变化时,会触发storage事件
- 当
- storage事件的event事件对象
- e.newValue 获取变化后的最新的值
- e.oldValue 获取变化前的值
(二) 发布订阅模式基本实现 - es5
// 发布订阅模式
// 1 模型
// 发布者 -- 事件中心 --- 订阅者
// publisher -- topics/eventChannel --- subscriber
// 2 关系
// 订阅者 -> 可以订阅多个事件
// 3 实现过程的注意点
// 订阅者身份的唯一标记,因为在unSubscribe时需要通过唯一性识别订阅者对象
---
const publishSubscribeDesignPattern = () => {
// topics对象,存放每个 ( 事件 ) 对应的 ( 订阅者对象 ) 组成的 ( 数组 )
// key: 事件名
// value: 订阅者对象组成的数组
// eventName:[{functionName: fn.name, fn: fn}]
const topics = {};
// ID 订阅者为唯一性
let id = 0;
// 订阅
topics.subscribe = (eventName, fn) => {
// 1
// 每一个事件可以有多个订阅者,所以 topics[eventName] 是一个数组,并且事件中心用一个对象去维护
if (!topics[eventName]) topics[eventName] = [];
// 2
// topics[eventName].push({ fnName: fn.name, fn });
// 问题:这里使用了 fn.name 来作为订阅者的唯一识别是有问题的,因为如果订阅者是一个对象时,对象的方法是没有name属性的,只有函数才有
// 解决:我们使用为一个id来实现,每次自增
if (fn) {
fn.id = id++; // 给每个订阅者添加一个唯一的id,id用来做取消订阅时的身份识别
}
topics[eventName].push({
id: fn.id,
fn,
});
};
// 发布
topics.publish = (eventName, params) => {
const subscribers = topics[eventName];
subscribers?.length && subscribers.forEach(({ fn }) => fn(params));
};
// 取消订阅
topics.unSubscribe = (eventName, fn) => {
const subscribers = topics[eventName];
if (subscribers?.length) {
const index = subscribers.findIndex(({ id }) => id === fn.id); // 在 subscribe 时已经为每个订阅者对象添加了唯一的id
subscribers.splice(index, 1);
}
};
return topics;
};
const topics = publishSubscribeDesignPattern();
const a = (params) => console.log("a", params);
const b = (params) => console.log("b", params);
topics.subscribe("click", a);
topics.subscribe("click", b);
topics.unSubscribe("click", b);
topics.publish("click", "2021/12/05");
(三) 发布订阅模式基本实现 - es6
class EventChannel {
topic = {};
id = 0;
subscribe = (eventName, fn) => {
if (fn) {
fn.id = this.id++;
}
if (!this.topic[eventName]) {
this.topic[eventName] = [];
}
this.topic[eventName].push({
id: fn.id,
fn,
});
};
publish = (eventName, params) => {
if (this?.topic[eventName]?.length) {
this.topic[eventName].forEach(({ fn }) => fn(params));
}
};
unSubscribe = (eventName, fn) => {
if (this?.topic[eventName]?.length) {
const index = this?.topic[eventName].findIndex(
({ id }) => id === fn.id
);
this.topic[eventName].splice(index, 1);
}
};
}
const eventChannel = new EventChannel();
class A {
a = (params) => console.log("a", params);
}
class B {
b = (params) => console.log("a", params);
}
const a = new A().a;
const b = new B().b;
eventChannel.subscribe("click", a);
eventChannel.subscribe("click", b);
eventChannel.unSubscribe("click", b);
eventChannel.publish("click", "2021/12/05");
(四) 发布订阅模式 - 手写vue双向数据绑定
前置知识
1. Element.children
- Element.children 返回一个类似数组的对象 ( HTMLCollection ) 集合
- 包含 当前元素的所有子元素
- 如果当前元素没有子元素,则返回的对象包含0个成员
2. Node.childNodes
- 返回一个类似数组的对象( NodeList集合 ),成员包括当前节点的所有子节点
- NodeList是一个动态集合
3. Node.childNodes 和 Element.children 的区别
- 类数组的对象
- 它们都是类似数组的对象
- 静态和动态
- Element.chilren -> HTMLCollection -> 都是动态集合 ----> 动态时添加减少dom后 ( 实时变化 )
- Node.childNodes -> NodeList -------> 动态或者静态 -> 静态时添加减少dom后 ( 不会变化 )
- 动态HTMLCollection:Element.children
- 动态HTMLCollection:Element.getElementsByClassName
- 动态HTMLCollection:Element.getElementsByTagName
-
- 动态NodeList:Node.childNodes 是动态的NodeList
- 静态NodeList:Element.querySelectorAll() 方法返回的是一个静态的NodeLis
- 静态NodeList:Element.querySelector() 方法返回的是一个静态的NodeLis
- 节点类型
- Element.children ---> 只包含 ( 元素类型的子节点 ),不包含其他类型的子节点
- Node.childNodes ----> 包含 ( 元素节点,文本节点,注释节点 )
4.
- Object.keys()
- 只遍历自身属性 + 可枚举属性,不包括继承的属性
- Object.keys() 和 Object.values() 和 Object.entries() 三者都具有上面的特征
- Object.getOwnPropertyNames()
- 遍历自身属性 + 可枚举属性 + 不可枚举属性,不包括继承的属性
- for...in
- 自身的属性 + 可枚举属性 + 继承的属性
- Object.keys() 和 Object.getOwnPropertyNames()的对比
- 相同点:
- 都是遍历自身属性,都不可遍历继承的属性
- 都可以遍历 ( 对象和数组 ),即参数是数组或对象
- 不同点
- `Object.keys() = 自身属性 + 可枚举属性`
- `Object.getOwnPropertyNames() = 自身属性 + 可枚举属性 + 不可枚举属性`
- 所以
- 一般情况都是使用 Object.keys() 去遍历对象
- 而不是使用 Object.getOwnPropertyNames()
- 复习链接
- https://juejin.cn/post/7029703494877577246#heading-22
利用发布订阅模式 - 手写一个简单的vue双向数据绑定
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="name" />
<div v-text="name"></div>
</div>
<script>
// const obj1 = { a: 1 }; // ------------ 1. 原型属性
// const obj2 = Object.create(obj1);
// obj2.b = 2;
// Object.defineProperty(obj2, "c", {
// value: 3,
// configurable: true,
// enumerable: true, // -------------- 2. 可枚举属性
// writeable: true,
// });
// const keys = Object.keys(obj2);
// console.log(`keys`, keys);
// Object.keys() 返回 自身属性 + 可枚举属性 ,不包含继承的属性
// Watcher
// 监听类
// 主要作用是:将改变之后的data中的 ( 最新数据 ) 更新到 ( ui视图 ) 中
class Watcher {
constructor(directiveName, el, attr, exp, vm) {
// this.name = directiveName; // 指令的名字,比如 'v-text','v-model'
this.el = el; // 每个具体的 DOM 节点
this.attr = attr; // --- el中的属性,需要需改的属性 - key
this.exp = exp; // ----- el中属性对应的 ( 属性值 ) - value
this.vm = vm; // MyVue实例对象
this.update(); // 注意这里在实例化Watcher时,会执行_update()方法
}
update = () => (this.el[this.attr] = this.vm.$data[this.exp]);
// 将MyVue实例的data属性的最新值更新到ui视图中
}
// Vue
class MyVue {
constructor(props) {
const { el, data } = props || {};
// 在vue中 $ 开头的属性,是vue的保留属性
this.$el = el;
this.$data = typeof data === "function" ? data() : data;
this._subs = {};
// key:data对象中的 key,递归循环data从而获取每一层的属性作为key
// value:数组,用来存放Watcher实例
this._observer(this.$data);
this._compiler(this.$el);
}
_observer = (data) => {
// 1
// object.keys() object.values() object.entries() --> 自身属性 + 可枚举属性
// for...in ----------------------------------------> 自身属性 + 可枚举属性 + 继承的属性
// 上面的三者包含的成员包括 ( 自身属性 + 可枚举属性 ),但 ( 不包含继承的属性 )
// 所以 object.entries 不用像 for...in 那样判断是否具有继承的属性,因为 for...in 会遍历继承的属性
// 详细:https://juejin.cn/post/7029703494877577246#heading-22
Object.entries(data).forEach(([key, value]) => {
if (typeof value === "object" && value !== null) {
this.observer(value); // 还是对象,递归做依赖收集和派发更新
}
if (!this._subs[key]) {
this._subs[key] = []; // 新建
}
const that = this;
Reflect.defineProperty(this.$data, key, {
enumerable: true, // 可枚举
configurable: true, // 可修改配置对象,可删除对象的属性
// writable: true, // value的值可以被修改,但这里使用了get和set,没有使用value
get() {
return value;
},
set(newValue) {
if (value !== newValue) {
value = newValue;
}
// 修改data后去更新ui
that._subs[key].forEach((watcher) => watcher.update());
},
});
});
};
_compiler = (el) => {
// const app = document.querySelector(el) // 这里不能用 querySelector,因为是静态的NodeList,不能动态变化
const app = document.getElementById(el);
const children = app.children; // 返回所有子元素的类似数组的对象
Object.entries(children).forEach(([key, value]) => {
if (value.length) {
this._compiler(value); // 还有子元素,递归
}
const vText = value.getAttribute("v-text");
const vModel = value.getAttribute("v-model");
// if (value.hasAttribute('v-text')){} 这样判断也是可以的
if (vText) {
this._subs[vText].push(
new Watcher("v-text", value, "innerHTML", vText, this)
);
}
if (
(vModel && value.tagName === "INPUT") ||
value.tagName === "TEXTAREA"
) {
this._subs[vModel].push(
new Watcher("v-model", value, "value", vModel, this)
); // watcher 订阅 data中vModel对应的属性的变化
const that = this;
value.addEventListener(
"input",
(e) => (that.$data[vModel] = e.target.value) // 监听input输入值的变化,修改 data,触发派发更新
);
}
});
};
}
new MyVue({
el: "app",
data() {
return {
name: "woow_wu7",
};
},
});
</script>
</body>
</html>
资料
- 无处不在的发布订阅 juejin.cn/post/691371…
- 跨页面通信 juejin.cn/post/684490…
- otherWindow.postMessage developer.mozilla.org/zh-CN/docs/…
- HTMLCollection 和 NodeList segmentfault.com/a/119000000…