观察者模式也可以称之为监听者模式。
它由观察者「Observer」与被观察者「Subject」共同组成。
又名发布订阅模式,在复杂场景下,为了适应更多需求,除了观察者与被观察者,还会引入更多的角色,因此复杂场景下,大家更愿意称之为发布订阅模式。
我们会遇到很多这种场景,例如事件监听。当我们点击按钮时,对应按钮如果添加了点击事件监听,那么对应的逻辑就会自动执行。而不需要我们显示的调用该逻辑去执行他。
我们也可以自定义事件。
10var event = new Event('build')2030// 添加观察者40document.addEventListener('build', function () {50console.log('我是新增的一个观察者1,我现在观察到 document 触发了 build 事件')60})7080// 添加观察者90document.addEventListener('build', function () {10console.log('我是新增的一个观察者2,我现在观察到 document 触发了 build 事件')11})1213// 被观察者触发事件14document.dispatchEvent(event)
观察者模式解决的就是这样的问题。当某一个条件满足要求或者触发某一种事件时,所有的观察者都会感知到并执行对应的逻辑。
在前端里最常见的就是 React/Vue 的数据思维:当我们改变 state/data 里的数据时,就会自动渲染 UI。
上图就是 Vue 的内部原理,观察者 Observer 观察数据 Data 的变化,如果一旦发现数据产生了变化就会通知后续的流程,最终完成 View 的更新。
基本实现
代码逻辑比较简单,直接上代码。
10let subjectid = 020let observerid = 03040class Subject {50constructor(name) {60// 观察者队列70this.observers = []80this.id = subjectid++90this.name = name10}1112// 添加观察者13addListener(observer) {14this.observers.push(observer)15}1617// 删除观察者18removeListener(observer) {19this.observers = this.observers.filter(item => item.id !== observer.id)20}2122// 通知23dispatch() {24this.observers.forEach(item => {25item.watch(this.name)26})27}28}2930class Observer {31constructor() {32this.id = observerid++33}34watch(subjectName) {35console.log(`观察者${this.id}发现了被观察者 ${subjectName} 产生了变化。`)36}37}3839const sub = new Subject('div元素')40const observer1 = new Observer()41const observer2 = new Observer()4243sub.addListener(observer1)44sub.addListener(observer2)4546sub.dispatch()
被观察者,通过 addListener 添加观察者。
当被观察者发生变化时,调用 sub.dispatch
通知所有观察者。
观察者通过 watch 接收到通知之后,执行预备好的逻辑。
以 DOM 元素事件绑定为例,我们进行一个类比
1div.addEventListener('click', fn, false)2div.addEventListener('mousemove', fn, false)3div.addEventListener('mouseup', fn, false)
此时,被观察者,就是 div 元素。
没有明确的观察者,而是直接传入回调函数 fn,事件触发时,回调函数全部执行。因此也可以将此处的回调函数理解为观察者。
这种以回调函数作为观察者的方式更符合事件绑定机制。
此处还需要传入一个字符串用于区分事件类型。这种方式又应该怎么实现呢?
思考一下,直接上代码
10class Subject {20constructor(name) {30// 观察者队列40// 格式为: { click: [fn1, fn2], scroll: [fn1, fn2] }50this.events = {}60this.name = name70}8090// 添加观察者10addListener(type, fn) {11const cbs = this.events[type]12if (cbs && cbs.length > 0) {13const _cbs = cbs.filter(cb => cb != fn)14_cbs.push(fn)15this.events[type] = _cbs16} else {17this.events[type] = [fn]18}19}2021// 删除观察者22removeListener(type) {23delete this.events[type]24}2526// 通知27dispatch(type) {28this.events[type].forEach(cb => cb())29}30}3132const sub = new Subject('div')3334sub.addListener('build', function() {35console.log('build 事件触发1')36})37sub.addListener('build', function () {38console.log('build 事件触发2')39})40sub.addListener('click', function() {41console.log('click 事件触发')42})4344sub.dispatch('build')
Vue 的实现原理
Vue 的核心是监听 data 中的数据变化,然后让数据的变化自动响应到 View 的变化。
由于 Vue 的场景更为复杂,因此在这个过程中引入了许多其他的角色。
Data:数据,最初的被观察者
Observer:Data 的观察者,数据劫持,监听数据的变化
Dep:订阅器,收集 Watcher。
Watcher:Dep 的订阅者,收到通知之后更新试图
数据的变化,会被 Observer 劫持,并且通知订阅器 Dep。Dep 收到通知之后,又会通知 Watcher,Watcher 收到属性的变化通知并执行响应的函数去更新试图 View。
Observer
data 数据的监听机制不需要我们去实现,因为在 JavaScript 中,对象中的每一个属性都具备自身的描述对象 descriptor
。
当访问数据时,描述对象中 get 方法会执行。
当修改数据时,描述对象中的 set 方法会执行。
因此,我们只需要利用这个机制,去监听数据即可。核心的方法是 JavaScript 提供的 Object.defineProperty
。
此处我们使用递归的方式,监听数据的所有属性。
与此同时,还需要关注的一个点是,我们需要
在 get 执行时,让 Dep 收集 Watcher。
在 set 执行时,通知 Watcher 执行更新逻辑。
因此,完整的实现如下:
10class Observer {20constructor(data) {30this.data = data;40this.walk(data);50}60walk(data) {70Object.keys(data).forEach((key) => {80this.defineReactive(data, key, data[key]);90});10}11defineReactive(data, key, val) {12const dep = new Dep();13Object.defineProperty(data, key, {14enumerable: true,15configurable: true,16get: () => {17if (Dep.target) {18dep.addSub(Dep.target);19}20return val;21},22set: function (newVal) {23if (newVal === val) {24return;25}26val = newVal;27dep.notify();28}29});30}31}
使用时,可以直接 new,也可以增加一个额外的判断
1function observe(value, vm) {2if (!value || typeof value !== 'object') {3return;4}5return new Observer(value);6};
Dep
此处的 Dep,也是一个被观察者,它与 Watcher 也是被观察者与观察者的关系。因此 Dep 的实现也非常简单,作用就是收集观察者 Watcher,以及通知 Watcher 更新。
10class Dep {20constructor() {30this.subs = []40}50addSub(sub) {60this.subs.push(sub);70}80notify() {90this.subs.forEach(function (sub) {10sub.update();11});12}13}
Watcher
对于订阅者 Watcher 一个非常重要的思考就是何时将自己添加到 Dep 中。
我们刚才在 Observer 的实现中,已经明确好了时机,也就是在访问数据时,可以将 Watcher 添加进去。
因此,我们在初始化 Watcher 时,可以手动去访问一次 data 中的数据,强制触发 get 执行。这样我们就可以在 get 的逻辑中,将 Watcher 添加到 Dep 里了。
10class Watcher {20constructor(vm, exp, cb) {30this.vm = vm;40this.exp = exp;50this.cb = cb;60this.value = this.get();70}80update() {90this.run()10}11run() {12var value = this.vm.data[this.exp];13var oldVal = this.value;14if (value !== oldVal) {15this.value = value;16this.cb.call(this.vm, value, oldVal);17}18}19get() {20Dep.target = this;21// 访问data,触发 get 执行,把当前的 Watcher 实例,添加到 Dep 中22var value = this.vm.data[this.exp]23// 添加成功之后,释放掉自身,其他的实例还需要该引用24Dep.target = null;25return value;26}27}
Vue
最后,我们需要将 data,Observer,Watcher 关联起来,形成一个整体。
10class Vue {20constructor(options, el, exp) {30this.data = options.data;4050// 劫持 data60observe(this.data);7080// 初始化显示90el.innerHTML = this.data[exp];1011// 创建 Watcher 实例12new Watcher(this, exp, function (value) {13el.innerHTML = value;14});15return this;16}17}
这样,我们就可以 new 一个 Vue 对象,然后通过 vue.data.text = 'xxx'
去改变 View 的显示了。
我们也可以对 Vue 的数据做一个代理处理,让 vue.data.text
与 vue.text
的操作是等价的。
10class Vue {20constructor(options, el, exp) {30this.data = options.data;40Object.keys(this.data).forEach((key) => {50this.proxyKeys(key);60});7080// 劫持 data90observe(this.data);1011// 初始化显示12el.innerHTML = this.data[exp];13new Watcher(this, exp, function (value) {14el.innerHTML = value;15});16return this;17}18proxyKeys(key) {19Object.defineProperty(this, key, {20enumerable: false,21configurable: true,22get: () => {23return this.data[key];24},25set: (newVal) => {26this.data[key] = newVal;27}28});29}30}
封装好之后,最后的使用代码就很简单了。
10var ele = document.querySelector('#wrap');20var vue = new Vue({30data: {40text: 'hello world'50}60}, ele, 'text');7080document.addEventListener('click', function() {90vue.data.text = `${vue.data.text} vue click.`10}, false)