[深入13] 观察者 发布订阅 双向数据绑定

导航 [[深入01] 执行上下文](https://juejin.im/post/684490...
[[深入02] 原型链](https://juejin.im/post/684490...
[[深入03] 继承](https://juejin.im/post/684490...
[[深入04] 事件循环](https://juejin.im/post/684490...
[[深入05] 柯里化 偏函数 函数记忆](https://juejin.im/post/684490...
[[深入06] 隐式转换 和 运算符](https://juejin.im/post/684490...
[[深入07] 浏览器缓存机制(http缓存机制)](https://juejin.im/post/684490...
[[深入08] 前端安全](https://juejin.im/post/684490...
[[深入09] 深浅拷贝](https://juejin.im/post/684490...
[[深入10] Debounce Throttle](https://juejin.im/post/684490...
[[深入11] 前端路由](https://juejin.im/post/684490...
[[深入12] 前端模块化](https://juejin.im/post/684490...
[[深入13] 观察者模式 发布订阅模式 双向数据绑定](https://juejin.im/post/684490...
[[深入14] canvas](https://juejin.im/post/684490...
[[深入15] webSocket](https://juejin.im/post/684490...
[[深入16] webpack](https://juejin.im/post/684490...
[[深入17] http 和 https](https://juejin.im/post/684490...
[[深入18] CSS-interview](https://juejin.im/post/684490...
[[深入19] 手写Promise](https://juejin.im/post/684490...
[[深入20] 手写函数](https://juejin.im/post/684490...
[[react] Hooks](https://juejin.im/post/684490...
[[部署01] Nginx](https://juejin.im/post/684490...
[[部署02] Docker 部署vue项目](https://juejin.im/post/684490...
[[部署03] gitlab-CI](https://juejin.im/post/684490...
[[源码-webpack01-前置知识] AST抽象语法树](https://juejin.im/post/684490...
[[源码-webpack02-前置知识] Tapable](https://juejin.im/post/684490...
[[源码-webpack03] 手写webpack - compiler简单编译流程](https://juejin.im/post/684490...
[[源码] Redux React-Redux01](https://juejin.im/post/684490...
[[源码] axios ](https://juejin.im/post/684490...
[[源码] vuex ](https://juejin.im/post/684490...
[[源码-vue01] data响应式 和 初始化渲染 ](https://juejin.im/post/684490...
[[源码-vue02] computed 响应式 - 初始化,访问,更新过程 ](https://juejin.im/post/684490...
前置知识 一些单词

subject:目标对象 observer:观察者对象pattern:模式 notify:通知Publisher:发布者 Subscriber:订阅者directive:指令 compile:编译Recursive:递归 adj recursion:递归 n factorial:阶乘

splice
arr.splice(start, count, addElement1, addElement2, ...);
start:开始位置,从0开始( 如果是负数,表示从倒数位置开始删除 )
count:删除的个数
addElement1...:被添加的元素
作用:splice 删除( 原数组 )的一部分成员,并可以在删除的位置( 添加 )新的数组成员
返回值:被删除的元素组成的数组,注意是一个数组
注意:splice 改变原数组
特别:如果只有一个参数( 第一个参数 ),则变相相当于把数组拆分成两个数组,一个是返回值数组,一个是改变后的原数组
for...in 和 for...of
  • for...in:可以用于 ( 数组 ) 或 ( 对象 )
  • for...of:只能用于( 数组 ),因为对象没有部署 iterator 接口
    for...in 和 for...of(1) for...in:可以用于数组 或 对象 for...of:只能用于数组,因为对象没有部署 iterator 接口

  • 数组
  • for(let i of arr) ----------- i 表示值
  • for(let i in arr) ----------- i 表示下标
  • 对象
  • 对象只能用for in循环,不能用for of循环
  • 因为对象没有部署iterator接口,不用使用for of循环
  • for (let i in obj) ----------- i 表示key
如何优雅的遍历对象
  • ( Object对象 ) 和 ( Array.prototype对象 ) 都部署了三个方法 keysvaluesentries
  • Object.keys(obj) , Object.values(obj),Object.entries(obj) ---- 静态方法
  • Array.prototype.keys(),Array.prototype.values(),Array.prototype.entries() - 返回 Iterator 遍历器对象
    如何优雅的遍历对象const arr = [{name: '123'},2,3,4] const obj = {name: '111', age: 222}对象 // (注意Object.keys() values() entries() 可以用于对象,也可以用于数组) for(let [key, value] of Object.entries(obj)) { // object || array console.log(key, value) // name 111 //age 222 // Object.entries(obj) 返回一个数组,每个成员也是一个数组,由( 键,值 )组成的数组 }// 数组 // for(let [key, value] of arr.entries()) { // 返回 iterator 对象接口,可以用for...of遍历 //console.log(key, value) // }

Element.children 和 Node.ChildNodes
  • Element.children
    • 返回一个类似数组的对象,( HTMLCollection ) 实例
    • 包括 当前元素节点的( 所有子元素 ) ---- 只包括元素节点
    • 如果当前元素没有子元素,则返回的对象包含 0 个成员
    • 注意:返回的是当前元素的所有子( 子元素节点 ),不包括其他节点
  • Node.childNodes
    • 返回一个类似数组的对象,( NodeList ) 集合
    • 包括 当前元素的所有( 子节点 ) ---- 包括元素节点,文本节点,注释节点
  • Node.childNodes 和 Element.children的区别
    • Node.childNodes是NodeList集合, Element.children是HTMLCollection集合
    • Node.childNodes包括当前节点的所有子节点,包括元素节点,文本节点,注释节点
    • Element.children包括当前元素节点的所有子元素节点,只包括元素节点
    • 注意:类似数组的对象具有length属性,且 键 是0或正整数
    111222 333444

document.querySelector(css选择器)
  • 返回第一个匹配的元素节点
  • document.querySelectorAll() 返回所有匹配的元素节点
递归 尾递归 尾调用
  • 递归的含义:函数调用自身,尾调用自身,尾递归
  • 递归的条件:边界条件,递归前进段,递归返回段
    • 不满足边界条件,递归前进
    • 满足边界条件,递归返回
  • 尾调用:函数内部最后一个动作是函数调用,该调用的返回值,直接返回给函数
  • 尾调用和非尾调用的区别:
    • 执行上下文栈变化不一样( 即函数调用栈不一样,内存占用不一样 )
    尾调用 和 非尾调用尾调用 function a() { return b() }非尾调用 function a() { return b() + 1 // 非尾调用 // 因为调用b()时,a函数并未执行完,未出栈 => b执行完后 + 1 ,这时a函数才执行完毕 }

---- 尾调用优化 ----// (1) 正常版 - 阶乘函数 function factorial(number) { if(number === 1 ) return number; return number * factorial(number - 1) } const res = factorial(3) console.log(res) // 调用栈 // 1 ------------------------------ 3 * factorial(2) // factorial(3) // 2 ------------------------------ 3 * factorial(2) // factorial(2) // factorial(3) // 3 ------------------------------ 3 * 2 * factorial(1) // factorial(1) // factorial(2) // factorial(3) // 4 ------------------------------ 3 * 2 * 1 // factorial(2) // factorial(3) // 5 ------------------------------ 6 // factorial(3)// (2) 尾递归版 - 阶乘函数 function factorial(number, multiply) { if (number === 1) return multiply; return factorial(number-1, number * multiply) } const res = factorial(4, 1) console.log(res) // factorial(4, 1) // factorial(3, 4*1) // factorial(2, 3 * (4 * 1)) // factorial(1, 2 * (3 * 4 * 1)) // return 1 * 3 * 4 * 1 // 12 // 每次都是尾递归,所以执行上线问栈中始终只有一个factorial() // 即下一个 factorial 调用时, 外层的 factorial 已经执行完毕出栈了// (3) 优化尾递归版 - 阶乘函数 function factorialTemp(multiply, number) { if (number === 1) return multiply; return factorialTemp(number * multiply, number-1) } function partial(fn) { // --------------------------------------- 偏函数 let params = Array.prototype.slice.call(arguments, 1) // ------ 固定部分参数 return function closure() { params = params.concat([...arguments]) if (params.length < fn.length) { // 参数小于fn形参个数,就继续收集参数 return closure } return fn.apply(this, params) // 否则,证明参数收集完毕,执行fn } } const factorial = partial(factorialTemp, 1) // 固定参数1 const res = factorial(4) console.log(res)

观察者模式 概念
  • 对程序中的某个对象进行观察,并在其发生改变时得到通知
  • 存在( 观察者对象 ) 和 ( 目标对象 )两种角色
  • 目标对象:subject
  • 观察者对象:observer
在观察者模式中,subject 和 observer 相互独立又相互联系
  • 一个目标对象对应多个观察者对象 ( 一对多 )
  • 观察者对象在目标对象中( 订阅事件 ),目标对象( 广播事件 )
  • Subject 目标对象:维护一个观察者实例组成的数组,并且具有( 添加,删除,通知 )操作该数组的各种方法
  • Observer 观察者对象:仅仅只需要维护收到通知后( 更新 )操作的方法
代码实现 - ES5
说明: (1) Subject构造函数 - Subject构造函数的实例( 目标对象 ),维护一个( 观察者实例对象 ) 组成的数组 - Subject构造函数的实例( 目标对象 ),拥有( 添加,删除,发布通知) 等操作观察者数组的方法(2) Observer构造函数 - Observer构造函数的实例( 观察者对象 ),仅仅只需要维护收到通知后,更新的方法--------- 代码:function Subject() { // 目标对象的构造函数 this.observes = [] // 观察者对象实例组成的数组 } Subject.prototype = { // 原型上挂载操作数组的方法 add(...params) { // --------------------------------- 添加观察者 // 对象方法的缩写,添加观察者对象 // params是ES6中的( rest参数数组 ),用来代替 arguments 对象,可以接收多个参数 // this在调用时确定指向,Subject构造函数的实例在调用,所以实例具有observes属性 this.observes = this.observes.concat(params) }, delete(obj) { // ------------------------------------ 删除观察者 const cashObservers = this.observes // ------------ 缓存能提高性能,因为下面有多次用到 cashObservers.forEach((item, index) => { if (item === obj) cashObservers.splice(index, 1) // 这里是三等判断,因为 obj 和 item 的指针都执行同一个堆内存地址 // 举例 // const a = {name: 'woow_wu7'} // const b = [a, 1, 2] // b[0] === a // 上面的结果是true - 因为b[0]和a指向了同一个堆内存地址// 注意:cashObservers.splice(index, 1) // 删除cashObservers相当于删除this.observes // 因为:this.observes赋值给了cashObservers,并且this.observes是复合类型的数据,所以两个变量指针一致 }) }, notify() { // --------------------------------------- 通知观察者,并执行观察者各自的更新函数 if(this.observes.length) this.observes.forEach(item => item.update()) } } Subject.prototype.constructor = Subject // ------------- 改变prototype的同时修改constructor,防止引用出错function Observer(fn) { // ----------------------------- 观察者对象仅仅维护更新函数 this.update = fn }const obsObj1 = new Observer(() => console.log(111111)) const obsObj2 = new Observer(() => console.log(222222)) const subObj = new Subject() subObj.add(obsObj1, obsObj2) subObj.notify() subObj.delete(obsObj1) subObj.notify()

代码实现 - ES6
----------- 观察者模式 - es6语法实现class Subject { constructor() { this.observers = [] } add = (...rest) => { this.observers = this.observers.concat(rest) } delete = (obj) => { const cachObservers = this.observers cachObservers.forEach((item, index) => { if (item === obj) cachObservers.splice(index, 1) }) } notify = () => { this.observers.forEach(item => { item.update() }); } } class Observer { constructor(fn) { this.update = fn } } const objserverIns = new Observer(() => console.log(1111)) const objserverIns2 = new Observer(() => console.log(2222)) const subjectIns = new Subject() subjectIns.add(objserverIns, objserverIns2) subjectIns.notify()

[深入13] 观察者 发布订阅 双向数据绑定
文章图片

2021/4/7 观察者模式优化
  • 优化 add => subscribe订阅
  • 优化 delete => unSubscribe取消订阅
    Document - 锐客网

  • 2020/12/28 复习
    [深入13] 观察者 发布订阅 双向数据绑定
    文章图片
发布订阅模式 角色
  • 发布者:Publisher
  • 订阅者:Subscriber
  • 中介:Topic/Event Channel
  • ( 中介 ) 既要 '接收' ( 发布者 )所发布的消息事件,又要将消息 '派发' 给( 订阅者 )
  • 所以中介要根据不同的事件,存储响应的订阅者信息
  • 通过中介对象,完全解耦了发布者和订阅者
发布订阅模式 es5代码实现
发布订阅模式 - 代码实现var pubsub = {}; // 中介对象 (function(pubsub) { var topics = {} // topics对象,存放每个事件对应的( 订阅者对象 )组成的( 数组 ) // key: 事件名 // eventName:[{functionName: fn.name, fn: fn}]pubsub.subscribe = function(eventName, fn) { // ------------------------------------------------------------ 订阅 if (!topics[eventName]) topics[eventName] = [] // 不存在,新建 topics[eventName].push({ functionName: fn.name, // 函数名作为标识,注意函数名不能相同 fn, // 更新函数 }) } pubsub.publish = function(eventName, params) { // ---------------- --------------------------------------------- 发布 if (!topics[eventName].length) return; // 如果空数组,即没有该事件对象的订阅者对象组成的数组,跳出整个函数 topics[eventName].forEach(item => { item.fn(params) // 数组不为空,就循环执行该数组所有订阅者对象绑定更新函数 }) } pubsub.unSubscribe = function(eventName, fn) { // 这里其实可以有化成传入回调函数名而不用传入整个回调函数 // -------------------------------------------------------------- 取消订阅 if (!topics[eventName]) { return } topics[eventName].forEach((item, index) => { if (item.functionName = fn.name) { // 如果:事件名 和 函数名 相同 // 就:删除这个事件对应的数组中的订阅者对象,该对象包含函数名,fn两个属性 topics[eventName].splice(index) } }) } })(pubsub)function closoleLog(args) { // 订阅者收到通知后的更新函数 console.log(args) } function closoleLog2(args) { console.log(args) }pubsub.subscribe('go', closoleLog) // 订阅 pubsub.subscribe('go', closoleLog2) pubsub.publish('go', 'home') // 发布 pubsub.unSubscribe('go', closoleLog) // 取消发布 console.log(pubsub)

发布订阅模式 es6代码实现
class PubSub { constructor() { this.topics = {} } subscribe(eventName, fn) { if (!this.topics[eventName]) { this.topics[eventName] = [] } this.topics[eventName].push({ fname: fn.name, fn }) } publish(eventName, params) { if (!this.topics[eventName].length) { return } this.topics[eventName].forEach((item, index) => { item.fn(params) }) } unSubscribe(eventName, fname) { if (!this.topics[eventName]) { return } this.topics[eventName].forEach((item, index) => { if (item.fname === fname) { this.topics[eventName].splice(index, 1) } }) } }const pubsub = new PubSub()function goFn1(params) { console.log('goFn1', params) } function goFn2(params) { console.log('goFn2', params) }pubsub.subscribe('go', goFn1) pubsub.subscribe('go', goFn2)pubsub.publish('go', '1') // 发布pubsub.unSubscribe('go', goFn2.name) // 取消订阅 pubsub.publish('go', '2')

[深入13] 观察者 发布订阅 双向数据绑定
文章图片

2020/12/29 复习 - 发布订阅模式ES5实现
Document - 锐客网

2020/12/29 复习 - 发布订阅模式ES56实现
Document - 锐客网

观察者模式,发布-订阅模式的区别和联系 (1)区别
  • 观察者模式:需要观察者自己定义事件发生时的响应函数
  • 发布-订阅模式:在(发布者对象),和(订阅者对象)之间,增加了(中介对象)
(2)联系
  • 二者都降低了代码的(耦合性)
  • 都具有消息传递的机制,以(数据为中心)的设计思想
vue双向数据绑定
前置知识:1. Element.children - 返回一个类似数组的对象(HTMLCollection实例) - 包括当前元素节点的所有子元素 - 如果当前元素没有子元素,则返回的对象包含0个成员2. Node.childNodes - 返回一个类似数组的对象(NodeList集合),成员包括当前节点的所有子节点 - NodeList是一个动态集合3. Node.childNodes 和 Element.children 的区别 - Element.children只包含元素类型的子节点,不包含其他类型的子节点 - Node.childNodes包含元素节点,文本节点,注释节点

代码 ------------- Document - 锐客网

2020/12/29 vue双向数据绑定 - 亲测可用
Document - 锐客网v-text的内容

资料 【[深入13] 观察者 发布订阅 双向数据绑定】详细 https://juejin.im/post/684490...
精简 https://juejin.im/post/684490...
实现vue数据双向绑定 https://juejin.im/post/684490...
https://segmentfault.com/a/11...
https://juejin.im/post/684490...
我的简书:https://www.jianshu.com/p/bdb...

    推荐阅读