深刻億點點之Vue:數據響應式

前言

數據響應式原理也是老生常談了,什麼是數據響應式呢?
數據響應式,從官方定義來講,將Model綁定到View,當用代碼更新Model時,View會自動更新。
數據響應式強調數據驅動DOM生成,而不是直接操做DOM。
而經常和數據響應式混爲一談的數據雙向綁定,則特指v-model,該指令實現了若是用戶更新了View,Model也會隨之更新。結合響應式原理,則造成了雙向綁定,即雙向綁定 = 單向綁定 + UI事件監聽
本文分爲四個部分:javascript

  • 一圖理解響應式原理
  • 手把手教你實現數據雙向綁定
  • 來個極簡實現方案
  • 數據響應式的思考

一圖理解響應式原理

Vue經過訂閱發佈者模式來實現,經過三個類ObserverDepWatcher來實現,主要關注每一個類的功能和類之間的關係。
html

訂閱發佈者模式示意圖

首先明確的是三個類之間的對應關係: ObserverDep是一對一的關係,DepWatcher是多對多的關係
每一個類的具體功能以下:
Observer:數據的 觀察者(我的理解上是一個代理髮布者),當初始化數據時,遍歷數據全部屬性,爲每個屬性設置一個調度中心( Dep實例對象),經過 Object.defineProperty把屬性轉爲 getter/setter,注入相關的 Dep調度方法。
Watcher:數據的 訂閱者,當接收到調度中心 Dep的更新通知時, Watcher實例執行回調cb,更新視圖。
Dep調度中心,做爲發佈者和訂閱者之間的消息傳遞樞紐。當 Observer類觸發 getter時, Dep收集依賴的 Watcher對象。當 Observer類觸發** setter**時, Observer將數據更新信息發送給 DepDep通知訂閱者 Watcher更新。
這三個類構成了主要的Model到View邏輯。至於View到Model的邏輯,只須要對輸入控件進行事件監聽,便可實現View到Model。這樣就造成了閉環的MVVM模型。

手把手教你實現數據雙向綁定

1. 實現訂閱-發佈者模式架構

這一步驟主要是肯定好整個流程框架,參照流程圖能夠肯定:vue

  • 肯定初始化須要的數據
  • 劫持數據,下發更新
  • 編譯模板,收集依賴
  • 視圖更新
class Vue{
  constructor(options) {
    // 1. 初始化數據
    this.options = options
    this.$data = options.data
    this.$el = document.querySelector(options.el)
    this._directive = [] // 收集依賴的容器
    this.observer()  // 2. 數據監測
    this.compiler()  // 3. 編譯模板,收集依賴
  }

  // TODO: 數據劫持,下發更新
  observer() {}

  // TODO: 判斷指令,收集依賴
  compiler() {}
}

// 訂閱者,主要更新視圖
class Watcher {
  constructor() {
    this.update()
  }
  // TODO: 更新視圖
  update() {}
}

var vm = new Vue({
  el: '#app',
  data: {
    myText: '一開始,只是平平無奇的text',
    myModel: '普普統統的model',
  }
})

複製代碼

2. 實現M->V,把模型裏的數據綁定到視圖

在框架上補充具體的邏輯:java

  • 初始數據:除了記錄傳入的一些數據外,還須要一個容器來記錄訂閱者。因爲訂閱者是對data的屬性監聽的,也就是當data[prop]更新時,只有訂閱prop屬性的訂閱者會有更新操做,其餘訂閱者不會收到更新。因此訂閱者容器是一個屬性對應一個列表。
  • 劫持數據,下發更新:經過Object.defineProperty重寫get/set(Vue3經過ES6的Proxy實現),對數據屬性進行劫持監聽。當更改數據屬性時,下發更新。

    爲何用Proxy 替代 Object.defineProperty ? Object.defineProperty只能劫持對象的屬性,所以咱們須要對每一個對象的每一個屬性進行遍歷。經過遞歸以及遍歷data對象來實現對數據的監控的,若是屬性值也是對象那麼須要深度遍歷。
    Proxy相比較與Object.defineProperty,優勢以下:
    1. 能夠劫持整個對象,並返回一個新對象。顯然能夠大幅度提高性能
    2. z多種劫持操做node

  • 編譯模板,收集依賴:解析html模板,經過檢測設定的特定屬性如v-model,進行依賴收集操做
  • 視圖更新:訂閱者收到更新信息後,對DOM進行操做,更新視圖
<body>
<div id="app">
  <h2>響應式原理實現</h2>
  <div v-text="myText"></div>
  <div v-text="myModel"></div>
  <input v-model="myModel" />
</div>
</body>
<script> class Vue{ constructor(options) { // 1. 初始化數據 this.options = options this.$data = options.data this.$el = document.querySelector(options.el) this._directive = {} // 收集依賴的容器 // 2. 數據監測 this.observer(this.$data) // 3. 編譯模板 this.compiler(this.$el) } observer(data) { for (var key in data) { this._directive[key] = [] // 初始化訂閱者容器 let val = data[key] let watchers = this._directive[key] Object.defineProperty(this.$data, key, { get: function() { return val }, set: function(newVal) { // 更新數值,下發更新 val = newVal watchers.forEach(watcher => watcher.update()) }, }) } // es6 // const handler = { // set: function(data, prop, val) { // }, // } // data = new Proxy(data, handler) } compiler(el) { let nodes = el.children for(let i=0; i<nodes.length; i++) { let node = nodes[i] // 若是有子元素,遞歸調用 if (node.children.length > 0) this.compiler(node) // 判斷指令,收集依賴 if(node.hasAttribute("v-model")) { let attrVal = node.getAttribute("v-model") this._directive[attrVal].push(new Watcher(node, this, attrVal, 'value')) } if(node.hasAttribute("v-text")) { let attrVal = node.getAttribute("v-text") this._directive[attrVal].push(new Watcher(node, this, attrVal, 'innerText')) } } } } // 訂閱者,主要更新數據 class Watcher { // el: 訂閱節點 // vm: vue實例 // exp: 訂閱的data屬性值 // attr: 不一樣訂閱者更新視圖時,修改的屬性不一樣 constructor(el, vm, exp, attr) { this.el = el this.vm = vm this.exp = exp this.attr = attr this.update() } // 更新 update() { this.el[this.attr] = this.vm.$data[this.exp] } } var vm = new Vue({ el: '#app', data: { myText: '一開始,只是平平無奇的text', myModel: '普普統統的model', }, }) setTimeout(function(){ console.log(vm.$data) vm.$data["myText"] = '3秒後,text更新了' }, 3000) </script>
複製代碼

3. 實現V->M

v-model指令,編譯時加入事件監聽。react

// ...
if(node.hasAttribute("v-model")) {
  let attrVal = node.getAttribute("v-model")
  this._directive[attrVal].push(new Watcher(node, this, attrVal, 'value'))
  // V -> M: 加入事件監聽
  node.addEventListener("input", (function () {
    return function() {
      this.$data[attrVal] = node.value
    }
  })().bind(this))
}
複製代碼

20行代碼極簡實現

const input = document.getElementById('input')
const span = document.getElementById('span')
const obj = {
  text: '文本文本文本'
}
const handler = {
  set: function(target, prop, val) {
    target[prop] = val
    span.innerText = val
    input.value = val
  }
}

const myText = new Proxy(obj, handler);

input.addEventListener('keyup', function(e){
  // 賦值觸發了set,初始化視圖
  // myText代理了text屬性,當text改變觸發set
  myText.text = e.target.value;
})
複製代碼

對數據響應式的一些思考

優勢

  • 雙向綁定把數據變動的操做隱藏在框架內部,調用者並不會直接感知。
  • 在表單交互多的狀況下,能夠簡化大量代碼。

注意點

  • 數據之間互相依賴,因爲"黑盒"的存在,在複雜應用中難以追蹤數據變化和管理,不如引入如vuex的狀態管理來得便利。
  • 使用Object.defineProperty實現數據綁定時,直接添加對象屬性obj[prop],該屬性並不是響應式,即沒法經過數據驅動視圖。對於數組對象也有相似的限制,直接經過索引修改數組也不會驅動視圖更新。爲了成爲響應式屬性,須要經過Vue.set來設置。(Vue3.0使用Proxy實現,屬性的添加和刪除、數組索引和長度的變動均可以被監聽,並能夠支持 Map、Set、WeakMap 和 WeakSet,該問題獲得解決)
  • 創造訂閱者自己有必定的消耗,且訂閱者一直存在於內存中。

參考

[1] 深刻響應式原理
[2] 深刻理解Vue響應式原理
[3] VUE數據響應式原理es6

相關文章
相關標籤/搜索