@TOC
观察者模式
- 介绍
- 演示
- 场景
- 总结
观察者模式 介绍
-
发布 & 订阅
-
一对多,一对n,n可能是1
观察者模式是前端最常用、最重要的设计模式,如果让你只掌握一种设计模式,那肯定就是观察者模式!!!
概念
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。一对多的关系。
观察者模式 & 发布订阅模式
示例
点咖啡,点好之后坐等被叫 例如你在星巴克点了咖啡,此时你并不需要在吧台坐等,你只需要回到位子上玩手机,等咖啡好了服务员会叫你。不光叫你,其他人的咖啡好了,服务员也会叫他们来取。
观察者模式 演示
传统的 UML 类图
简化之后的 UML 类图
代码演示
// 主题,接收状态变化,触发每个观察者
class Subject {
constructor() {
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
attach(observer) {
this.observers.push(observer)
}
notifyAllObservers() {
this.observers.forEach(observer => {
observer.update()
})
}
}
// 观察者,等待被触发
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)
}
update() {
console.log(`${this.name} update, state: ${this.subject.getState()}`)
}
}
// 测试代码
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
let o3 = new Observer('o3', s)
s.setState(1)
s.setState(2)
s.setState(3)
观察者模式 场景
观察者模式在 JS 中的应用场景非常多,绝不仅限于以下列举的场景。本节列举了很多示例,旨在从多角度理解观察者模式的应用场景,以及体会观察者模式在 JS 中的应用之广泛。
增加一个场景:vue 的 watcher
网页事件绑定
网页事件绑定就是最常用的观察者模式
<button id="btn1">btn</button>
<script>
$('#btn1').click(function () {
console.log(1)
})
$('#btn1').click(function () {
console.log(2)
})
$('#btn1').click(function () {
console.log(3)
})
</script>
JS 异步
JS 异步,只要用到 callback 函数,都是观察者模式
setTimeout(function () {
alert(100)
}, 1000)
Promise
一开始说到异步有 callback 的都是观察者模式,而 Promise 作为异步的解决方案,也避免不了要使用。
function loadImg(src) {
var promise = new Promise(function (resolve, reject) {
var img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('图片加载失败')
}
img.src = src
})
return promise
}
var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
result.then(function (img) {
console.log('width', img.width)
return img
}).then(function (img) {
console.log('height', img.height)
})
resolve和reject就相当于之前的setState,状态改变,其实这也是 Promise 的真实状态变化:pending -> fulfilled 或者 pending -> rejected 。两个then就是观察者,状态变化就会触发观察者update 。
预告:下面讲到状态模式的时候,会演示如何自己实现一个 Promise
jQuery callbacks
jQuery callbacks 是 jQuery 的内部底层功能,服务于对外的 API 如 ajax deferred 等。jQuery 这么通用的 lib 都有必要维护一个通用的观察者功能,可见观察者模式在 JS 中的应用之广泛
var callbacks = $.Callbacks() // 注意大小写
callbacks.add(function (info) {
console.log('fn1', info)
})
callbacks.add(function (info) {
console.log('fn2', info)
})
callbacks.add(function (info) {
console.log('fn3', info)
})
callbacks.fire('gogogo')
callbacks.fire('fire')
nodejs 自定义事件
简单 demo
const EventEmitter = require('events').EventEmitter
const emitter1 = new EventEmitter()
emitter1.on('some', () => {
// 监听 some 事件
console.log('some event is occured 1')
})
emitter1.on('some', () => {
// 监听 some 事件
console.log('some event is occured 2')
})
// 触发 some 事件
emitter1.emit('some')
以上代码中,先引入 nodejs 提供的EventEmitter构造函数,然后初始化一个实例emitter1。实例通过on可监听事件,emit可以触发事件,事件名称可以自定义,如some。
tip:写代码注释中文和英文或者数字中间空一格
自定义事件触发的时候还可传递参数,例如
const EventEmitter = require('events').EventEmitter
const emitter = new EventEmitter()
emitter.on('sbowName', name => {
console.log('event occured ', name)
})
emitter.emit('sbowName', 'zhangsan') // emit 时候可以传递参数过去
上文说到EventEmitter实例有on和emit接口,其实自定义 class 的实例也可以有,只不过需要继承EventEmitter。使用 ES6 的继承语法很容易实现
const EventEmitter = require('events').EventEmitter
// 任何构造函数都可以继承 EventEmitter 的方法 on emit
class Dog extends EventEmitter {
constructor(name) {
super()
this.name = name
}
}
var simon = new Dog('simon')
simon.on('bark', function () {
console.log(this.name, ' barked')
})
setInterval(() => {
simon.emit('bark')
}, 500)
和 jQuery callbacks 一样,自定义事件也是 nodejs 中底层、通用的功能,很多其他功能要继承EventEmitter以实现自定义事件功能,下文会讲到,也能看出观察者模式在 nodejs 中应用的广泛。
nodejs stream
stream 是 nodejs 的基础模块,就是把大数据(一次性读取内存放不开)的操作当做一个流,来一点一点的读取,直到读取完毕。
例如一个大文件,几百万行(一般是日志文件),想要得知它的字符长度,就需要用到 stream 。既然是一点一点的读取,那么每次读取一点就得知道读取的是什么,读取完毕也得得到通知,这就需要观察者模式。
// Stream 用到了自定义事件,不会一行一行读取文件,可能读到中间就截断了
var fs = require('fs')
var readStream = fs.createReadStream('./data/file1.txt') // 读取文件的 Stream
var length = 0
readStream.on('data', function (chunk) {
length += chunk.toString().length
})
readStream.on('end', function () {
console.log(length)
})
nodejs 还专门指定了 readline ,跟上述的模式一样,只不过是一行一行读取文件,例如要知道上述文件一共有多少行,可以使用
// readline 用到了自定义事件
var readline = require('readline');
var fs = require('fs')
var rl = readline.createInterface({
input: fs.createReadStream('./data/file1.txt')
});
var lineNum = 0
rl.on('line', function(line){
lineNum++
});
rl.on('close', function() {
console.log('lineNum', lineNum)
});
nodejs 处理 htttp 请求
当 nodejs 接收 post 请求时,也需要一点一点的接收
var http = require('http')
function serverCallback(req, res) {
var method = req.method.toLowerCase() // 获取请求的方法
if (method === 'get') {
console.log('get 请求不处理')
}
if (method === 'post') {
// 接收 post 请求的内容
var data = ''
req.on('data', function (chunk) {
// “一点一点”接收内容
console.log('chunk', chunk.toString())
data += chunk.toString()
})
req.on('end', function () {
// 接收完毕,将内容输出
console.log('end')
res.writeHead(200, {'Content-type': 'text/html'})
res.write(data)
res.end()
})
}
}
http.createServer(serverCallback).listen(8081) // 注意端口别和其他 server 的冲突
console.log('监听 8081 端口……')
使用 curl 模拟一下即可
curl -H "Content-Type:application/json" -X POST -d '{"user": "admin", "passwd":"12345678"}' http://127.0.0.1:8081/
nodejs 多进程通讯
// parent.js
var cp = require('child_process')
var n = cp.fork('./sub.js')
n.on('message', function (m) {
console.log('PARENT got message: ' + m)
})
n.send({hello: 'workd'})
// sub.js
process.on('message', function (m) {
console.log('CHILD got message: ' + m)
})
process.send({foo: 'bar'})
同理于浏览器端的 webworker ,不再演示。
Vue 和 React 组件生命周期触发
vue React 使用组件化,每个组件实例都有固定的生命周期,生命周期的意思就是在实例运行的某个特定的节点,执行你要做的操作,例如created生命周期,打印一句话:
new Vue({
data: {
a: 1
},
created: function () {
// `this` 指向 vm 实例
console.log('a is: ' + this.a)
}
})
这些生命周期的函数,其实也都是观察者,当组件实例运行到某个阶段时,就会触发这个观察者。这是 vue 源码中的某个片段
function callHook (vm, hook) {
var handlers = vm.$options[hook]; // 获取生命周期的所有函数
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(vm); // 遍历,挨个触发
}
}
}
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate'); // 触发 beforeCreate 生命周期
initInjections(vm);
initState(vm);
initProvide(vm);
callHook(vm, 'created'); // 触发 created 生命周期
vue watch
vue 响应式的实现
vue 响应式大家都清楚,data 变化立即触发 view 的变化
<div id="app">
<p>{{price}}</p>
<p>{{name}}</p>
</div>
<script src="https://cdn.bootcss.com/vue/2.5.9/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
price: 100,
name: 'zhangsan'
}
})
// 修改 vm.price ,页面内容会立刻修改
vm.price = 200
vm.name = 'imooc'
</script>
网上一搜 vue 响应式的原理特别复杂,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hYTnGBXa-1598951537693)(./img/4.png)]
其实单就了解原理,就没必要看这么多细节,你也没必要了解这么多细节,抛开细节就简单多了。其实只有三点:
- 如何监听
vm.price的变化? - 如何更新 view ,让它作为观察者
- 两者形成观察者模式
先说第一个问题,借用Object.defineProperty就可以实现这种需求
var obj = {}
var name = 'zhangsan'
Object.defineProperty(obj, "name", {
get: function () {
console.log('get')
return name
},
set: function (newVal) {
console.log('set')
name = newVal
}
});
console.log(obj.name) // 可以监听到
obj.name = 'lisi' // 可以监听到
第二个问题,更新 view 内部逻辑比较复杂,但是我们这里没必要细究,只知道它就是重新获取了 data 中的数据,重新渲染 view ,就可以了
var updateComponent = function () {
vm._update(vm._render(), hydrating);
};
最后,两者形成观察者模式,这里的细节也非常复杂,毕竟 vue 是全球都在用的 MVVM 框架,但是没必要细究,从下面的代码中可以窥探出,它就是用了观察者模式。
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate'); // 触发 beforeUpdate 生命周期
}
}
}, true /* isRenderWatcher */);
再次重申:不要在这里抱怨没有深入、细致了讲解 vue 的源码和实现!我很能理解你想深入学习 vue 源码的冲动,但是本课程的主题是设计模式,再花费很大精力讲解 vue 源码,课程就跑偏了!另外,本课程宣传和介绍时,也未曾说过要讲解 vue 源码。切记切记!!!
一对多的关系
观察者模式表示的是一对多关系。上述例子中,有些明显是一对多关系,有些演示的是一对一的关系 —— 但是,他们也都支持一对多,只不过暂时用一对一而已。
观察者模式 总结
- 什么是观察者模式
- 观察者模式的核心:主题和观察者分开,状态变化时,观察者等待被触发。一对多的关系。
- 前端应用场景
设计原则验证:
- 主题和观察者分离,不是主动触发而是被动监听,两者解耦
- 符合开放封闭原则