(十)观察者模式
觀察者模式
- 觀察者模式
- 觀察者模式 介紹
- 概念
- 觀察者模式 & 發布訂閱模式
- 示例
- 觀察者模式 演示
- 觀察者模式 場景
- 網頁事件綁定
- JS 異步
- Promise
- jQuery callbacks
- nodejs 自定義事件
- nodejs stream
- nodejs 處理 htttp 請求
- nodejs 多進程通訊
- Vue 和 React 組件生命周期觸發
- vue watch
- vue 響應式的實現
- 一對多的關系
- 觀察者模式 總結
觀察者模式
- 介紹
- 演示
- 場景
- 總結
觀察者模式 介紹
- 發布 & 訂閱
- 一對多,一對n,n可能是1
觀察者模式是前端最常用、最重要的設計模式,如果讓你只掌握一種設計模式,那肯定就是觀察者模式!!!
概念
當對象間存在一對多關系時,則使用觀察者模式(Observer Pattern)。比如,當一個對象被修改時,則會自動通知它的依賴對象。一對多的關系。
觀察者模式 & 發布訂閱模式
示例
點咖啡,點好之后坐等被叫
例如你在星巴克點了咖啡,此時你并不需要在吧臺坐等,你只需要回到位子上玩手機,等咖啡好了服務員會叫你。不光叫你,其他人的咖啡好了,服務員也會叫他們來取。
觀察者模式 演示
傳統的 UML 類圖
簡化之后的 UML 類圖
代碼演示
// 主題,接收狀態變化,觸發每個觀察者 class Subject {constructor() {this.state = 0this.observers = []}getState() {return this.state}setState(state) {this.state = statethis.notifyAllObservers()}attach(observer) {this.observers.push(observer)}notifyAllObservers() {this.observers.forEach(observer => {observer.update()})} }// 觀察者,等待被觸發 class Observer {constructor(name, subject) {this.name = namethis.subject = subjectthis.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').EventEmitterconst 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') // 讀取文件的 Streamvar 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 = 200vm.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 源碼。切記切記!!!
一對多的關系
觀察者模式表示的是一對多關系。上述例子中,有些明顯是一對多關系,有些演示的是一對一的關系 —— 但是,他們也都支持一對多,只不過暫時用一對一而已。
觀察者模式 總結
- 什么是觀察者模式
- 觀察者模式的核心:主題和觀察者分開,狀態變化時,觀察者等待被觸發。一對多的關系。
- 前端應用場景
設計原則驗證:
- 主題和觀察者分離,不是主動觸發而是被動監聽,兩者解耦
- 符合開放封閉原則
總結
- 上一篇: 盒马app怎么买东西
- 下一篇: 游戏里dps是什么意思 4399小游戏