浅谈订阅发布实现vue
订阅发布模式是开发领域常见的设计模式,在我们的开发中简直无处不在。这次我们一起来揭开其并不神秘的面纱。
何为订阅发布模式
订阅发布模式顾名思义分为订阅和发布两个动作。其实不止代码,生活中也有很多订阅发布模式的例子。
以双十一李佳奇直播为例子,为了该死的折扣,我们在淘宝上搜索李佳奇直播间,进入页面发现现在并不是直播时间,这时候页面上会有个开播提醒按钮。
当我们点击开播提醒的时候,其实就是执行了一个订阅操作,向消息中心订阅直播通知。
当下一场直播时间到了,消息中心将接收到直播间的开播事件,接着向所有订阅该直播间的用户推送通知。也就是淘宝APP向你推送消息,“李佳奇直播开始啦”,这时候你再执行对应的操作,打开淘宝,美滋滋地观看直播购物。
订阅发布的代码实现
我们以面试中常见的 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
劫持数据的 setter
getter
函数
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; } }) 复制代码
如此,我们便劫持了 observeObj
的 name
属性,这就是我们所说的 数据劫持
。当后面访问或者赋值 observeObj.name
都会访问我们定义的劫持函数 get
set
,简直无法无天。
有了这个 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 复制代码
通过上面的示例我们可以得出
当我们设置链式属性的时候,实际上是会依次访问链中属性的
get
及末尾属性的set
object.deep1.deep2.name = 'x' // deep1 get -> deep2 get -> name set 复制代码
我们设置对象的
get
set
函数的时候,其实也是设置指针指向地址的变量对象。当我们重新设置对象地址时,之前设置的访问器不再访问
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 复制代码
尽管设置重复相同的值,也会访问
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(); } } }) } } 复制代码
从 Observer
的 defineReactive
实现中我们可以看到订阅 dep.addSub(targetWatch)
及发布 dep.notify()
。
至此我们实现了
订阅发布中心 Dep
订阅者 Watcher
监听者 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 复制代码
可以看到,通过上面得代码,我们实现了数据得自动监听及订阅发布,我们再次梳理下这个流程
实现订阅发布中心 Dep
实现订阅者 Watcher
实现监听者 Observer
以监听目标 data 为参数实例化 Observer
data 下的每个属性都被遍及递归,进行数据劫持
每个属性实例化一个订阅发布中心 dep
实例化订阅者 watcher
watcher 的第一个参数为 expFn 访问函数,访问 data 的某个属性
实例化时在构造函数中将实例 watcher 赋值给全局 targetWatch 调用 expFn,将访问到 data 的某个属性如 name
在 name 的 get 函数中,此时 targetWatch 有值,则为该属性对应的 dep 添加订阅者 watcher
开发者手动修改数据如
data.name = new
,将访问到 name 属性的 set 函数,此时判断前后值不相同,则通知 dep 数据更新dep 在属性的 set 函数中收到数据更新的通知,遍历调用订阅者 watcher 的 update 方法
在 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.deep
和 data.deep.name
都会触发发布通知。
但是此时再次修改 data.deep.name
不再触发更新,因为我们订阅的 name 属性的 deep 对象已经发生实际改变。
总结
本篇文章简单介绍了订阅发布模式及数据劫持,及实现了 vue 中结合这两者实现数据响应式的例子。实际上写的比较多也比较乱,希望能将就将就看看。本篇文章是为了后面的 vue 源码学习打基础,后面将实际从源码的角度分析 vue 中数据响应式的实现。
作者:沐晓
链接:https://juejin.cn/post/7026608140523143175