(十)观察者模式

观察者模式

  • 介绍
  • 演示
  • 场景
  • 总结

观察者模式 介绍

  • 发布 & 订阅
  • 一对多,一对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)
})

resolvereject就相当于之前的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实例有onemit接口,其实自定义 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 源码。切记切记!!!

一对多的关系

观察者模式表示的是一对多关系。上述例子中,有些明显是一对多关系,有些演示的是一对一的关系 —— 但是,他们也都支持一对多,只不过暂时用一对一而已。

观察者模式 总结

  • 什么是观察者模式
  • 观察者模式的核心:主题和观察者分开,状态变化时,观察者等待被触发。一对多的关系。
  • 前端应用场景

设计原则验证:

  • 主题和观察者分离,不是主动触发而是被动监听,两者解耦
  • 符合开放封闭原则
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值