浅谈订阅发布实现vue

简介: 订阅发布模式是开发领域常见的设计模式,在我们的开发中简直无处不在。这次我们一起来揭开其并不神秘的面纱。

订阅发布模式是开发领域常见的设计模式,在我们的开发中简直无处不在。这次我们一起来揭开其并不神秘的面纱。


何为订阅发布模式


订阅发布模式顾名思义分为订阅和发布两个动作。其实不止代码,生活中也有很多订阅发布模式的例子。


以双十一李佳奇直播为例子,为了该死的折扣,我们在淘宝上搜索李佳奇直播间,进入页面发现现在并不是直播时间,这时候页面上会有个开播提醒按钮。


当我们点击开播提醒的时候,其实就是执行了一个订阅操作,向消息中心订阅直播通知。97.png


当下一场直播时间到了,消息中心将接收到直播间的开播事件,接着向所有订阅该直播间的用户推送通知。也就是淘宝APP向你推送消息,“李佳奇直播开始啦”,这时候你再执行对应的操作,打开淘宝,美滋滋地观看直播购物。

98.png


订阅发布的代码实现


我们以面试中常见的 EventEmitter 为例子,来简单实现一个订阅发布中心

// 订阅发布中心:事件发射器
class EventEmitter {
  constructor () {
    this.listeners = {};
  }
  // 订阅事件
  addListener (event, cb) {
    if (this.listeners[event]) {
      this.listeners[event].push(cb)
    } else {
      this.listeners[event] = [].concat(cb)
    }
  }
  // 取消订阅
  removeListener (event, cb) {
    const events = this.listeners[event];
    if (events) {
      const idx = events.indexOf(cb);
      if (idx > -1) {
        events.splice(idx, 1)
      }
    }
  }
  // 通知:执行事件
  emit (event) {
    const events = this.listeners[event];
    if (events) {
      for (let i = 0, len = events.length; i < len; i++) {
        events[i]();
      }
    }
  }
}
const eventEmitter = new EventEmitter();
// 商家订阅
eventEmitter.addListener('李佳奇直播', () => {
  console.log('商家:直播开始啦 上货咯')
})
// 用户2订阅
eventEmitter.addListener('李佳奇直播', () => {
  console.log('用户:直播开始啦 剁手啦')
})
// 用户1订阅
eventEmitter.addListener('李佳奇直播', () => {
  console.log('用户:直播开始啦 剁手啦')
})
// 发布
eventEmitter.emit('李佳奇直播')
复制代码


有没有一种似曾相识的感觉。是的,我们前端经常使用的事件监听 addEventListener 不就是基于订阅发布模式实现的么。


vue源码中订阅发布的实现


我们都知道,只要问及 vue 实现原理,是个人都能扯些 数据劫持订阅发布模式 等词汇。那么 vue 到底是如何实现它们的呢?接下来我们继续卷


数据劫持


数据劫持 是 vue 实现数据响应式的提前,其原理是利用 Object.defineProperty 劫持数据的 settergetter 函数

const observeObj = {
  name: 'xiao ming'
};
// 例子:劫持对象的name属性
let myname = null;
Object.defineProperty(observeObj, 'name', {
  configurable: true, // 可配置
  enumerable: true, // 可枚举
  get () {
    console.log('有人访问observeObj.name啦')
    // 给他返回个我喜欢的变量
    return myname;
  },
  set (newVal) {
    console.log('有人设置observeObj.name啦', newVal);
    myname = 'xiao' + val;
  }
})
复制代码


如此,我们便劫持了 observeObjname 属性,这就是我们所说的 数据劫持。当后面访问或者赋值 observeObj.name 都会访问我们定义的劫持函数 getset,简直无法无天。


有了这个 Object.defineProperty,vue 就有了监听开发者修改数据的能力。所以我们常说 vue 的原理是数据劫持,也就是这么个回事。

多级对象的set get


我们再来学习学习 Object.defineProperty,看看多级对象是如何触发 set get 的,不为其它,只为了解的深入一线。

let deep = {};
const observeObj = {
  deep
};
Object.defineProperty(observeObj, 'deep', {
  get () {
    console.log('get deep')
    return deep
  },
  set (newVal) {
    console.log('set deep')
    deep = newVal
  }
})
Object.defineProperty(deep, 'name', {
  get () {
    console.log('get deep.name')
    return 'xiaoming';
  },
  set () {
    console.log('set deep.name')
  }
})
observeObj.deep.name = 2; // get deep set deep.name 依次访问deep属性的get name属性的set
observeObj.deep.name = 2; // get deep set deep.name 依次访问deep属性的get name属性的set
observeObj.deep.name; // get deep get deep.name 依次访问deep属性的get name属性的get
observeObj.deep = {}; // 访问deep的set
observeObj.deep.name; // 访问deep的get
复制代码


通过上面的示例我们可以得出


  1. 当我们设置链式属性的时候,实际上是会依次访问链中属性的 get 及末尾属性的 set
object.deep1.deep2.name = 'x' // deep1 get -> deep2 get -> name set
复制代码

  1. 我们设置对象的 getset 函数的时候,其实也是设置指针指向地址的变量对象。当我们重新设置对象地址时,之前设置的访问器不再访问
let deep = {}
Object.defineProperty(deep, 'age', {
  get () {
    console.log('get deep.age')
    return 1;
  },
  set () {
    console.log('set deep.age')
  }
})
deep.age = 2 // 访问set
// 更改指向地址
deep = {}
deep.age = 3 // 不再访问set
复制代码

  1. 尽管设置重复相同的值,也会访问 set
deep.age = 2 // 访问set
deep.age = 2 // 访问set +1
deep.age = 2 // 访问set +1
复制代码

vue数据响应式模拟


有了前面订阅发布及数据劫持的储备知识,我们就来结合其两者来简单模拟下 vue 的数据响应式


首先先实现我们的订阅发布中心 Dep

// 存储全局订阅者
let targetWatch = null
// 订阅发布中心
class Dep {
  constructor () {
    // 订阅者
    this.subs = [];
  }
  // 订阅
  addSub (sub) {
    this.subs.push(sub)
  }
  // 发布通知
  notify () {
    const subs = this.subs;
    for (let i = 0, len = subs.length; i < len; i++) {
      // 通知订阅者
      subs[i].update();
    }
  }
}
复制代码


接着实现订阅者 Watcher

class Watcher {
  constructor (expFn, cb) {
    // 回调函数
    this.cb = cb;
    // 通过全局变量来标记当前订阅者
    targetWatch = this;
    // expFn用于触发订阅操作
    this.expFn = expFn;
    this.expFn();
    targetWatch = null;
  }
  update () {
    this.cb();
  }
}
复制代码


再来看看订阅动作,这里得先做数据劫持,实现监测器 Observer

class Observer {
  constructor (data) {
    this.walk(data);
  }
  walk (obj) {
    // 遍历对象属性
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 为每个键进行劫持
      this.defineReactive(obj, keys[i])
    }
  }
  defineReactive (obj, key) {
    let val = obj[key];
    // 为每个key实例化独享的Dep实例
    const dep = new Dep();
    // 若键为对象则需递归劫持
    if (dep && typeof val === 'object') {
      new Observer(val)
    }
    // 数据劫持器
    Object.defineProperty(obj, key, {
      get () {
        // 订阅发布中心触发订阅 dep添加当前订阅者
        if (targetWatch) dep.addSub(targetWatch)
        return val;
      },
      set (newVal) {
        if (val !== newVal) {
          val = newVal
          // 订阅发布中心触发通知
          dep.notify();
        }
      }
    })
  }
}
复制代码


ObserverdefineReactive 实现中我们可以看到订阅 dep.addSub(targetWatch) 及发布 dep.notify()


至此我们实现了

  1. 订阅发布中心 Dep

  2. 订阅者 Watcher

  3. 监听者 Observer


现在我们再来看看如何利用上面已实现的内容来做到数据响应式

// 我们需要监听的数据
const data = {
  name: '',
  deep: {
    age: 0
  }
}
// 为data添加监听
new Observe(data);
// 添加订阅者
new Watcher(function expFn() {
  // 访问name属性
  data.name;
  console.log('这里是订阅函数,订阅了data.name',)
}, function cb() {
  console.log('数据更新啦,得重新渲染页面了', data.name)
})
// 添加订阅者
new Watcher(function expFn() {
  // 访问deep属性
  data.deep;
  console.log('这里是订阅函数,订阅了data.deep',)
}, function cb() {
  console.log('数据更新啦,得重新渲染页面了', data.deep)
})
// 更改数据
data.name = 'new' // 数据更新啦,得重新渲染页面了 new
data.name = 'new2' // 数据更新啦,得重新渲染页面了 new2
data.name = 'new3' // 数据更新啦,得重新渲染页面了 new3
复制代码


可以看到,通过上面得代码,我们实现了数据得自动监听及订阅发布,我们再次梳理下这个流程


  1. 实现订阅发布中心 Dep

  2. 实现订阅者 Watcher


  1. 实现监听者 Observer

  2. 以监听目标 data 为参数实例化 Observer

  • data 下的每个属性都被遍及递归,进行数据劫持

  • 每个属性实例化一个订阅发布中心 dep

  1. 实例化订阅者 watcher

  • watcher 的第一个参数为 expFn 访问函数,访问 data 的某个属性

  • 实例化时在构造函数中将实例 watcher 赋值给全局 targetWatch 调用 expFn,将访问到 data 的某个属性如 name

  • 在 name 的 get 函数中,此时 targetWatch 有值,则为该属性对应的 dep 添加订阅者 watcher

  1. 开发者手动修改数据如 data.name = new,将访问到 name 属性的 set 函数,此时判断前后值不相同,则通知 dep 数据更新

  2. dep 在属性的 set 函数中收到数据更新的通知,遍历调用订阅者 watcher 的 update 方法

  3. 在 update 方法中调用 cb 收到数据更新及获取最新数据,以便完成下一步渲染等操作


以上便是 vue 中数据监听及发布订阅模式的简单实现,实际上写的比较粗糙,没有去兼容数组及对新值进行劫持监听等


但这不是重点,重点是我们能从其中明白 vue 的实现原理即可,后面将分析实际源码去了解整个 vue 的实现。


补充


之前我们学习了下多层级下的 Object.defineProperty 表现,那么在我们的 DEMO 中,它又是如何表现得呢


// 我们需要监听的数据
const data = {
  name: '',
  deep: {
    name: ''
  }
}
// 增加添加订阅者
new Watcher(function expFn() {
  // 访问deep.name属性
  data.deep.name;
  console.log('这里是订阅函数,订阅了data.deep.name',)
}, function cb() {
  console.log('数据更新啦,得重新渲染页面了', data.deep.name)
})
// 更改数据
data.deep.name = 'new' // 数据更新啦,得重新渲染页面了 new
data.deep = {
  name: 'new2'
} // 得重新渲染页面了 new2
data.deep.name = 'new2'
复制代码


表现符合预期,修改 deep.name 时,订阅者收到更新通知。


data.deep 指向新地址也会通知,因为我们在 expFn 中链式访问了多个属性的 get,实际上会有多个属性的 dep 添加按订阅者 watcher,所以不管修改 data.deepdata.deep.name 都会触发发布通知。


但是此时再次修改 data.deep.name 不再触发更新,因为我们订阅的 name 属性的 deep 对象已经发生实际改变。


总结


本篇文章简单介绍了订阅发布模式及数据劫持,及实现了 vue 中结合这两者实现数据响应式的例子。实际上写的比较多也比较乱,希望能将就将就看看。本篇文章是为了后面的 vue 源码学习打基础,后面将实际从源码的角度分析 vue 中数据响应式的实现。




相关文章
|
25天前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
27天前
|
JavaScript 前端开发 开发者
vue 数据驱动视图
总之,Vue 数据驱动视图是一种先进的理念和技术,它为前端开发带来了巨大的便利和优势。通过理解和应用这一特性,开发者能够构建出更加动态、高效、用户体验良好的前端应用。在不断发展的前端领域中,数据驱动视图将继续发挥重要作用,推动着应用界面的不断创新和进化。
|
1天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
28天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
32 1
vue学习第一章
|
28天前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
26 1
vue学习第三章
|
28天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
35 1
vue学习第四章
|
28天前
|
JavaScript 前端开发 算法
vue学习第7章(循环)
欢迎来到瑞雨溪的博客,一名热爱JavaScript和Vue的大一学生。本文介绍了Vue中的v-for指令,包括遍历数组和对象、使用key以及数组的响应式方法等内容,并附有综合练习实例。关注我,将持续更新更多优质文章!🎉🎉🎉
24 1
vue学习第7章(循环)
|
28天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
29 1
vue学习第九章(v-model)
|
28天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
39 1
vue学习第十章(组件开发)
|
28天前
|
JavaScript 前端开发
vue学习第十一章(组件开发2)
欢迎来到我的博客,我是瑞雨溪,一名自学前端两年半的大一学生,专注于JavaScript与Vue。本文介绍Vue中的插槽(slot)使用方法,包括基本插槽、具名插槽及作用域插槽,帮助你在组件开发中实现内容的灵活定制。如果你觉得有帮助,请关注我,持续更新中!🎉🎉🎉
23 1
vue学习第十一章(组件开发2)