[深刻13] 觀察者 發佈訂閱 雙向數據綁定

導航

[深刻01] 執行上下文
[深刻02] 原型鏈
[深刻03] 繼承
[深刻04] 事件循環
[深刻05] 柯里化 偏函數 函數記憶
[深刻06] 隱式轉換 和 運算符
[深刻07] 瀏覽器緩存機制(http緩存機制)
[深刻08] 前端安全
[深刻09] 深淺拷貝
[深刻10] Debounce Throttle
[深刻11] 前端路由
[深刻12] 前端模塊化
[深刻13] 觀察者模式 發佈訂閱模式 雙向數據綁定
[深刻14] canvas
[深刻15] webSocket
[深刻16] webpack
[深刻17] http 和 https
[深刻18] CSS-interview
[react] Hookscss

[部署01] Nginx
[部署02] Docker 部署vue項目
[部署03] gitlab-CIhtml

[源碼-webpack01-前置知識] AST抽象語法樹
[源碼-webpack02-前置知識] Tapable
[源碼-webpack03] 手寫webpack - compiler簡單編譯流程前端

前置知識

一些單詞

subject:目標對象
observer:觀察者對象

pattern:模式
notify:通知

Publisher:發佈者
Subscriber:訂閱者

directive:指令
compile:編譯

Recursive:遞歸 adj
recursion:遞歸 n
factorial:階乘
複製代碼

splice

arr.splice(start, count, addElement1, addElement2, ...);
start:開始位置,從0開始( 若是是負數,表示從倒數位置開始刪除 )
count:刪除的個數
addElement1...:被添加的元素vue

做用:splice 刪除( 原數組 )的一部分紅員,並能夠在刪除的位置( 添加 )新的數組成員
返回值:被刪除的元素組成的數組,注意是一個數組
注意:splice 改變原數組
特別:若是隻有一個參數( 第一個參數 ),則變相至關於把數組拆分紅兩個數組,一個是返回值數組,一個是改變後的原數組react

for...in 和 for...of

  • for...in:能夠用於 ( 數組 ) 或 ( 對象 )
  • for...of:只能用於( 數組 ),由於對象沒有部署 iterator 接口
for...in 和 for...of

(1)
for...in:能夠用於數組 或 對象
for...of:只能用於數組,由於對象沒有部署 iterator 接口


(2)
1. 數組
- for(let i of arr)  ----------- i 表示值
- for(let i in arr)  ----------- i 表示下標
2. 對象
- 對象只能用for in循環,不能用for of循環
- 由於對象沒有部署iterator接口,不用使用for of循環
- for (let i in obj) ----------- i 表示key
複製代碼

如何優雅的遍歷對象

  • ( Object對象 ) 和 ( Array.prototype對象 ) 都部署了三個方法 keysvaluesentries
  • Object.keys(obj) , Object.values(obj),Object.entries(obj) ---- 靜態方法
  • Array.prototype.keys(),Array.prototype.values(),Array.prototype.entries() - 返回 Iterator 遍歷器對象
如何優雅的遍歷對象

const arr = [{name: '123'},2,3,4]
const obj = {name: '111', age: 222}


對象 
// (注意Object.keys() values() entries() 能夠用於對象,也能夠用於數組)
for(let [key, value] of Object.entries(obj)) { // object || array
  console.log(key, value)
  // name 111
  //  age 222
  // Object.entries(obj) 返回一個數組,每一個成員也是一個數組,由( 鍵,值 )組成的數組
}


// 數組
// for(let [key, value] of arr.entries()) { // 返回 iterator 對象接口,能夠用for...of遍歷
//  console.log(key, value)
// }
複製代碼

Element.children 和 Node.ChildNodes

  • Element.children
    • 返回一個相似數組的對象,( HTMLCollection ) 實例
    • 包括 當前元素節點的( 全部子元素 ) ---- 只包括元素節點
    • 若是當前元素沒有子元素,則返回的對象包含 0 個成員
    • 注意:返回的是當前元素的全部子( 子元素節點 ),不包括其餘節點
  • Node.childNodes
    • 返回一個相似數組的對象,( NodeList ) 集合
    • 包括 當前元素的全部( 子節點 ) ---- 包括元素節點,文本節點,註釋節點
  • Node.childNodes 和 Element.children的區別
    • Node.childNodes是NodeList集合, Element.children是HTMLCollection集合
    • Node.childNodes包括當前節點的全部子節點,包括元素節點,文本節點,註釋節點
    • Element.children包括當前元素節點的全部子元素節點,只包括元素節點
    • 注意:相似數組的對象具備length屬性,且 鍵 是0或正整數
<div id='content'>
  <div>111</div>
  <div>
    <div>222</div>
    <div>333</div>
  </div>
  <p1>444</p1>
</div>

<script>
  const contentHTMLCollection = document.getElementById('content') ---------- HTMLCollection
  console.log(content.children)
  // HTMLCollection(3) [div, div, p1]

  const contentNodeList = document.querySelector('div') --------------------- NodeList
  console.log(contentNodeList.childNodes)
  // NodeList(7) [text, div, text, div, text, p1, text]
</script>
複製代碼

document.querySelector(css選擇器)

  • 返回第一個匹配的元素節點
  • document.querySelectorAll() 返回全部匹配的元素節點

遞歸 尾遞歸 尾調用

  • 遞歸的含義:函數調用自身,尾調用自身,尾遞歸
  • 遞歸的條件:邊界條件,遞歸前進段,遞歸返回段
    • 不知足邊界條件,遞歸前進
    • 知足邊界條件,遞歸返回
  • 尾調用:函數內部最後一個動做是函數調用,該調用的返回值,直接返回給函數
  • 尾調用和非尾調用的區別:
    • 執行上下文棧變化不同( 即函數調用棧不同,內存佔用不同 )
尾調用 和 非尾調用

尾調用
function a() {
    return b()
}

非尾調用
function a() {
    return b() + 1
    // 非尾調用
    // 由於調用b()時,a函數並未執行完,未出棧 => b執行完後 + 1 ,這時a函數才執行完畢
}
複製代碼
----
尾調用優化
----

// (1) 正常版 - 階乘函數
function factorial(number) {
  if(number === 1 ) return number;
  return number * factorial(number - 1)
}
const res = factorial(3)
console.log(res)
// 調用棧
// 1 ------------------------------ 3 * factorial(2)
// factorial(3) 
// 2 ------------------------------ 3 * factorial(2)
// factorial(2)
// factorial(3)
// 3 ------------------------------ 3 * 2 * factorial(1)
// factorial(1)
// factorial(2)
// factorial(3)
// 4 ------------------------------ 3 * 2 * 1
// factorial(2) 
// factorial(3)
// 5 ------------------------------ 6
// factorial(3)



// (2) 尾遞歸版 - 階乘函數
function factorial(number, multiply) {
  if (number === 1) return multiply;
  return factorial(number-1, number * multiply)
}
const res = factorial(4, 1)
console.log(res)
// factorial(4, 1)
// factorial(3, 4*1)
// factorial(2, 3 * (4 * 1))
// factorial(1, 2 * (3 * 4 * 1))
// return 1 * 3 * 4 * 1 
// 12
// 每次都是尾遞歸,因此執行上線問棧中始終只有一個factorial()
// 即下一個 factorial 調用時, 外層的 factorial 已經執行完畢出棧了




// (3) 優化尾遞歸版 - 階乘函數
function factorialTemp(multiply, number) {
  if (number === 1) return multiply;
  return factorialTemp(number * multiply, number-1)
}
function partial(fn) { // --------------------------------------- 偏函數
  let params = Array.prototype.slice.call(arguments, 1) // ------ 固定部分參數
  return function closure() {
    params = params.concat([...arguments])
    if (params.length < fn.length) { // 參數小於fn形參個數,就繼續收集參數
      return closure
    }
    return fn.apply(this, params) // 不然,證實參數收集完畢,執行fn
  }
}
const factorial = partial(factorialTemp, 1) // 固定參數1
const res = factorial(4)
console.log(res)
複製代碼

觀察者模式

概念

  • 對程序中的某個對象進行觀察,並在其發生改變時獲得通知webpack

  • 存在( 觀察者對象 ) 和 ( 目標對象 )兩種角色git

  • 目標對象:subjectes6

  • 觀察者對象:observerweb

在觀察者模式中,subject 和 observer 相互獨立又相互聯繫

  • 一個目標對象對應多個觀察者對象 ( 一對多 )
  • 觀察者對象在目標對象中( 訂閱事件 ),目標對象( 廣播事件 )
  • Subject 目標對象:維護一個觀察者實例組成的數組,而且具備( 添加,刪除,通知 )操做該數組的各類方法
  • Observer 觀察者對象:僅僅只須要維護收到通知後( 更新 )操做的方法

代碼實現 - ES5

說明:
(1) Subject構造函數
- Subject構造函數的實例( 目標對象 ),維護一個( 觀察者實例對象 ) 組成的數組
- Subject構造函數的實例( 目標對象 ),擁有( 添加,刪除,發佈通知) 等操做觀察者數組的方法

(2) Observer構造函數
- Observer構造函數的實例( 觀察者對象 ),僅僅只須要維護收到通知後,更新的方法



---------
代碼:

function Subject() { // 目標對象的構造函數
  this.observes = [] // 觀察者對象實例組成的數組
}
Subject.prototype = { // 原型上掛載操做數組的方法
  add(...params) { // --------------------------------- 添加觀察者
    // 對象方法的縮寫,添加觀察者對象
    // params是ES6中的( rest參數數組 ),用來代替 arguments 對象,能夠接收多個參數
    // this在調用時肯定指向,Subject構造函數的實例在調用,因此實例具備observes屬性
    this.observes = this.observes.concat(params)
  },
  delete(obj) { // ------------------------------------ 刪除觀察者
    const cashObservers = this.observes // ------------ 緩存能提升性能,由於下面有屢次用到
    cashObservers.forEach((item, index) => {
      if (item === obj) cashObservers.splice(index, 1)
      // 這裏是三等判斷,由於 obj 和 item 的指針都執行同一個堆內存地址
      // 舉例
      // const a = {name: 'woow_wu7'}
      // const b = [a, 1, 2]
      // b[0] === a 
      // 上面的結果是true - 由於b[0]和a指向了同一個堆內存地址
      
      // 注意:cashObservers.splice(index, 1)
      // 刪除cashObservers至關於刪除this.observes
      // 由於:this.observes賦值給了cashObservers,而且this.observes是複合類型的數據,因此兩個變量指針一致
    })
  },
  notify() { // --------------------------------------- 通知觀察者,並執行觀察者各自的更新函數
    if(this.observes.length) this.observes.forEach(item => item.update())
  }
}
Subject.prototype.constructor = Subject // ------------- 改變prototype的同時修改constructor,防止引用出錯

function Observer(fn) { // ----------------------------- 觀察者對象僅僅維護更新函數
  this.update = fn
}

const obsObj1 = new Observer(() => console.log(111111))
const obsObj2 = new Observer(() => console.log(222222))
const subObj = new Subject()
subObj.add(obsObj1, obsObj2)
subObj.notify()
subObj.delete(obsObj1)
subObj.notify()

複製代碼

代碼實現 - ES6

-----------
觀察者模式 - es6語法實現

class Subject {
  constructor() {
    this.observers = []
  }
  add = (...rest) => {
    this.observers = this.observers.concat(rest)
  }
  delete = (obj) => {
    const cachObservers = this.observers
    cachObservers.forEach((item, index) => {
      if (item === obj) cachObservers.splice(index, 1)
    })
  }
  notify = () => {
    this.observers.forEach(item => {
      item.update()
    });
  }
}
class Observer {
  constructor(fn) {
    this.update = fn
  }
}
const objserverIns = new Observer(() => console.log(1111))
const objserverIns2 = new Observer(() => console.log(2222))
const subjectIns = new Subject()
subjectIns.add(objserverIns, objserverIns2)
subjectIns.notify()
複製代碼

發佈訂閱模式

角色

  • 發佈者:Publisher
  • 訂閱者:Subscriber
  • 中介:Topic/Event Channel
  • ( 中介 ) 既要 '接收' ( 發佈者 )所發佈的消息事件,又要將消息 '派發' 給( 訂閱者 )
  • 因此中介要根據不一樣的事件,存儲響應的訂閱者信息
  • 經過中介對象,徹底解耦了發佈者和訂閱者

發佈訂閱模式 es5代碼實現

發佈訂閱模式 - 代碼實現

var pubsub = {}; // 中介對象
(function(pubsub) {
  var topics = {} 
  // topics對象,存放每一個事件對應的( 訂閱者對象 )組成的( 數組 )
  // key: 事件名 
  // eventName:[{functionName: fn.name, fn: fn}]
  
  pubsub.subscribe = function(eventName, fn) {
    // ------------------------------------------------------------ 訂閱 
    if (!topics[eventName]) topics[eventName] = [] // 不存在,新建
    topics[eventName].push({
      functionName: fn.name, // 函數名做爲標識,注意函數名不能相同
      fn, // 更新函數
    })
  }
  pubsub.publish = function(eventName, params) {
    // ---------------- --------------------------------------------- 發佈 
    if (!topics[eventName].length) return; // 若是空數組,即沒有該事件對象的訂閱者對象組成的數組,跳出整個函數
    topics[eventName].forEach(item => {
      item.fn(params) // 數組不爲空,就循環執行該數組全部訂閱者對象綁定更新函數
    })
  }
  pubsub.unSubscribe = function(eventName, fn) {
    // -------------------------------------------------------------- 取消訂閱
    if (!topics[eventName]) {
      return
    }
    topics[eventName].forEach((item, index) => {
      if (item.functionName = fn.name) {
        // 若是:事件名 和 函數名 相同
        // 就:刪除這個事件對應的數組中的訂閱者對象,該對象包含函數名,fn兩個屬性
        topics[eventName].splice(index)
      }
    })
  }
})(pubsub)

function closoleLog(args) { // 訂閱者收到通知後的更新函數
  console.log(args)
} 
function closoleLog2(args) {
  console.log(args)
}

pubsub.subscribe('go', closoleLog) // 訂閱
pubsub.subscribe('go', closoleLog2)
pubsub.publish('go', 'home') // 發佈
pubsub.unSubscribe('go', closoleLog) // 取消發佈
console.log(pubsub)

複製代碼

發佈訂閱模式 es6代碼實現

class PubSub {
  constructor() {
    this.topics = {}
  }
  subscribe(eventName, fn) {
    if (!this.topics[eventName]) {
      this.topics[eventName] = []
    }
    this.topics[eventName].push({
      fname: fn.name,
      fn
    })
  }
  publish(eventName, params) {
    if (!this.topics[eventName].length) {
      return
    }
    this.topics[eventName].forEach((item, index) => {
      item.fn(params)
    })
  }
  unSubscribe(eventName, fname) {
    if (!this.topics[eventName]) {
      return
    }
    this.topics[eventName].forEach((item, index) => {
      if (item.fname === fname) {
        this.topics[eventName].splice(index, 1)
      }
    })
  }
}

const pubsub = new PubSub()

function goFn1(params) {
  console.log('goFn1', params)
}
function goFn2(params) {
  console.log('goFn2', params)
}

pubsub.subscribe('go', goFn1)
pubsub.subscribe('go', goFn2)

pubsub.publish('go', '1') // 發佈

pubsub.unSubscribe('go', goFn2.name) // 取消訂閱
pubsub.publish('go', '2')
複製代碼

觀察者模式,發佈-訂閱模式的區別和聯繫

(1)區別

  • 觀察者模式:須要觀察者本身定義事件發生時的響應函數
  • 發佈-訂閱模式:在(發佈者對象),和(訂閱者對象)之間,增長了(中介對象)

(2)聯繫

  • 兩者都下降了代碼的(耦合性)
  • 都具備消息傳遞的機制,以(數據爲中心)的設計思想

vue雙向數據綁定

前置知識:

1. Element.children
- 返回一個相似數組的對象(HTMLCollection實例)
- 包括當前元素節點的全部子元素
- 若是當前元素沒有子元素,則返回的對象包含0個成員

2. Node.childNodes
- 返回一個相似數組的對象(NodeList集合),成員包括當前節點的全部子節點
- NodeList是一個動態集合

3. Node.childNodes 和 Element.children 的區別
- Element.children只包含元素類型的子節點,不包含其餘類型的子節點
- Node.childNodes包含元素節點,文本節點,註釋節點

複製代碼
代碼
-------------

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div v-text="name"></div>
    <input type="text" v-model="name">
  </div>

  <script>
    // 監聽類
    // 主要做用是:將改變以後的data中的 ( 最新數據 ) 更新到 ( ui視圖 ) 中
    class Watcher {
      constructor(directiveName, el, vm, exp, attr) {
        this.name = directiveName // 指令的名字,好比 'v-text''v-model'
        this.el = el // 每一個具體的DOM節點
        this.vm = vm // MyVue實例對象
        this.exp = exp // el中的directiveName屬性對應的屬性值
        this.attr = attr // el的屬性,須要需改的屬性

        this._update() // 注意這裏在實例化Watcher時,會執行_update()方法
      }
      _update() {
        this.el[this.attr] = this.vm.$data[this.exp]
        // 將MyVue實例的data屬性的最新值更新到ui視圖中
      }
    }

    class MyVue {
      constructor(options) {
        const {el, data} = this.options = options // 參數結構賦值
        this.$el = document.querySelector(el) // 獲取el選擇器的元素節點
        this.$data = data // data數據

        this._directive = {}
        // key:data對象中的 key,遞歸循環data從而獲取每一層的屬性做爲key
        // value:數組,用來存放Watcher實例
        
        this._observes(this.$data) 
        // 1. 遞歸循環,獲取data每一層的key
        // 2. 把_directive對象,用每一個key作( 鍵 ),用一個Watcher實例組成的數組作( 值 )
        // 3. 監聽data中每一個key對應的value是否變化
            // 變化就執行 => _directive對象中key對應的數組中的Watcher實例對象的_update()方法
    
        this._compile(this.$el)
        // 1. 遞歸循環,獲取全部$el對象的元素節點的 全部子元素節點 Element.children
        // 2. 若是元素節點有 v-text 指令,就把Watcher實例push進_directive對象該指令值對應的數組
            // 由於執行了new Watcher(),因此constructor中調用了_update,因此會執行一次
            // 即push實例的同時,也會執行_update(),已是會把data的數據寫入到對應的指令所在的元素節點中
        // 3. 若是元素節點有 v-model指令,同理
            // 不一樣的是:須要監聽( input事件 ),鍵最新的input框的value值,賦值給data
            // data改變,會被Object.defineProperty監聽,從而執行更新函數
            // 更新函數負責把最新的data中的數據更新到頁面中
      }

      _observes(data) {
        for(let [key, value] of Object.entries(data)) {
          if (data.hasOwnProperty(key)) {
            this._directive[key] = []
          }
          if (typeof value === 'object') { // 數組 或 對象
            this._observes(value) // 遞歸調用
          }
          const _dir = this._directive[key]
          Object.defineProperty(this.$data, key, { // ------------- 監聽屬性變化
            enumerable: true,
            configurable: true,
            get() {
              return value
            },
            set(newValue) {
              if (newValue !== value) { // ------------------------ 屬性變化後執行因此訂閱者對象的更新函數
                value = newValue
                _dir.forEach(item => item._update())
              }
            }
          })
        }
      }

      _compile(el) {
        for(let [key, value] of Object.entries(el.children)) {
          if (value.length) {
            _compile(value)
          }
          if (value.hasAttribute('v-text')) {
            const attrValue = value.getAttribute('v-text')
            this._directive[attrValue].push(new Watcher('input', value, this, attrValue, 'innerHTML'))
          }
          if(value.hasAttribute('v-model') && (value.tagName === 'INPUT' || value.tagName === 'TEXTAREA')) {
            const attrValue = value.getAttribute('v-model')
            this._directive[attrValue].push(new Watcher('v-model', value, this, attrValue, 'value'))
            let that = this
            value.addEventListener('input', function() { // input事件監聽
              that.$data[attrValue] = value.value 
              // 獲取最新的input框值,賦值給data => Object.defineProperty監聽到data變化 => 執行更新函數更新視圖
            })
          }
        }
      }
    }

    // 實例化MyVue
    new MyVue({ 
      el: '#app', 
      data: {
        name: 'woow_wu7'
      }
    })
  </script>
</body>
</html>
複製代碼

資料

詳細 juejin.im/post/5cc577…
精簡 juejin.im/post/5bb1bb…
實現vue數據雙向綁定 juejin.im/post/5bce9a…
segmentfault.com/a/119000001…
juejin.im/post/5c10ed…
個人簡書:www.jianshu.com/p/bdb03feab…canvas

相關文章
相關標籤/搜索