Vue之深刻響應式原理

理解Vue響應式原理,只需經過解決如下幾個問題便可

  • 如何實現數據劫持?
  • 如何實現數據代理?(如何對this.xxx的訪問代理到this.data.xxx上?)
  • 如何實現數據編譯?
  • 如何實現發佈訂閱模式?
  • 如何實現更新視圖?(如何監聽數據的讀寫操做?如何實現數據修改DOM更新?)
  • 如何實現雙向數據綁定?
  • 如何實現依賴緩存?
  • template改變的時候,如何清理依賴項集合?eg:v-if和組件銷燬

前提

<div id="app">
  <div id="app">
    <p>{{ a.a }}</p>
    <span>{{ b }}</span>
</div>
</div>
<script>
const vm = new Mvvm({
    el: '#app',
    data: {
      a: {
        a: '我是a'
      },
      b: '我是b'
    }
  })
</script>

問題:如何實現數據劫持

答:經過Object.defineProperty()方法,對data中的屬性,在訪問或者修改對象的其中某個屬性時,經過一段代碼攔截這個行爲,進行額外的操做或者修改返回結果node

// TODO:1.定義Mvvm類
function Mvvm(options={}){
    // TODO:this表明的是fade實例對象
    // TODO:將全部屬性掛載到$options
    this.$options = options
    var data = this._data = this.options.data
    // TODO:調用數據劫持
    observe(data)
}

// TODO:3.觀察者
function Observe(obj){
  for (let key in obj) {
    let val = obj[key]
    // TODO:深度劫持
    observe(val)
    
    Object.defineProperty(obj,key,{
      enumerable : true,
      get(){
        return val
      },
      set(newVal){
        if (newVal === val) return
        val = newVal
        // TODO:深度劫持
        observe(newVal)
      }
    })
  }
}

// TODO:2.數據劫持-使每一個對象都具備get和set方法
function observe(vmData){
  if (typeof data !== 'object') return
  return new Observe(vmData)
}

問題:如何實現數據代理-如何對this.xxx的訪問代理到this.data.xxx上?

答:對於每一個data上的屬性,都在app上作一個代理,實際操做的是this.data
實現的代碼以下:數組

function Mvvm(options = {}) {
  // TODO:this表明的是zhufeng實例對象
  // TODO:將全部屬性掛載到$options
  this.$options = options
  //this._data
  var data = this._data = this.$options.data
  observe(data) 
  // TODO:4.數據代理
  for (let key in data) {
    Object.defineProperty(this,key,{
      enumerable : true,
      get(){
        return this._data[key]
      },
      set(newVal){
        this._data[key] = newVal
      }
    })
  }
}

問題:如何實現數據編譯

答:經過獲取vm管理DOM的根節點,讓其在內存中完成相關的正則匹配工做,替換DOM中的文本節點緩存

// TODO:5數據編譯
function Compile(el,vm){
  vm.$el = document.querySelector(el)
  let fragment = document.createDocumentFragment()
  while (child = vm.$el.firstChild) {
    fragment.appendChild(child)
  }

  replace(fragment)

  // TODO:6.數據替換
  function replace(frag){
    Array.from(frag.childNodes).forEach(function (node) {
      let text = node.textContent
      let regExp = /\{\{(.*)\}\}/
      if (node.nodeType === 1 && regExp.test(text)) {
        let arr = RegExp.$1.trim().split('.')
        let val = vm
        arr.forEach(function (k) {
          val = val[k]
        })
        node.textContent = text.replace(regExp,val).trim()
      }
      if (node.childNodes && node.childNodes.length) {
        replace(node)
      }
    })
  }
  vm.$el.appendChild(fragment)
}
function Mvvm(options = {}) {
  ....
  // TODO:進行編譯
  new Compile(optionns.el,this)
}

問題:如何實現發佈訂閱模式

答:發佈訂閱主要靠的就是數組關係,訂閱就是放入函數,發佈就是讓數組裏的函數執行app

// TODO:8.發佈訂閱模式
// TODO:橋樑
function Dep(){ // 橋樑
  this.subs = [] // 訂閱事件池
}
// TODO:進行訂閱的方法(往裏面扔函數)
Dep.property.addSub = function (sub) {  //sub就是watcher
  this.subs.push(sub)
}
// TODO:進行發佈/通知的方法(讓函數的每一項一次執行)
Dep.prototype.notify = function () {
  this.subs.forEach(sub => sub.update())//綁定的事件,都有一個update屬性
}

// TODO:訂閱者
function Watcher(fn){ //Watcher是一個類,經過這個類建立的實例都擁有update方法
  this.fn = fn
}

Watcher.prototype.update = function () { //調用fn()
  this.fn()
}

問題:如何更新視圖-當數據改變須要從新刷新視圖

答:如今咱們要訂閱一個事件,當數據改變須要從新刷新視圖,這就須要在replace替換的邏輯裏來處理
經過new Watcher把數據訂閱一下,數據一變就執行改變內容的操做函數

  • 監聽變化
// TODO:6.數據替換
  function replace(frag) {
    ...
    node.textContent = text.replace(regExp, val).trim()
    
    // TODO:監聽變化
    new Watcher(vm,RegExp.$1,function (newVal) {
        node.textContent = text.replace(regExp,newVal).trim()
    })
    
    if (node.childNodes && node.childNodes.length) {
        replace(node)
    }
    ...
  }
  • 重寫Watcher構造函數
// TODO:訂閱者
function Watcher(vm,exp,fn){ //Watcher是一個類,經過這個類建立的實例都擁有update方法
  this.fn = fn
  this.vm = vm
  this.exp = exp

  Dep.target = this
  let arr = exp.trim().split('.')
  let val = vm
  arr.forEach(function (key) {
    val = val[key]
  })
  Dep.target = null //  // 上面獲取val[key]的時候會調用get方法, 所以使用完畢以後須要把該屬性置爲null
}
  • 重寫數據劫持get和set方法this

    • 解:當獲取值的時候就會自動調用get方法,因而咱們去找一下數據劫持那裏的get方法
// TODO:3.觀察者
function Observe(obj){
  // TODO:建立橋樑
  let dep = new Dep()
  for (let key in obj) {
    let val = obj[key]
    // TODO:深度劫持
    observe(val)

    Object.defineProperty(obj,key,{
      enumerable : true,
      get(){
        // TODO:將watcher添加到訂閱事件中 [watcher]
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set(newVal){ // 更改值得時候
        if (newVal === val) return // 設置的值和之前的是同樣的東西
        val = newVal // 若是之後在獲取值的時候將剛纔設置的值丟回去
        // TODO:深度劫持
        observe(newVal)
        // TODO:執行update方法
        dep.notify()
      }
    })
  }
}
  • 修改watcher的update方法spa

    • 解:當set修改值的時候執行了dep.notify方法,這個方法是執行watcher的update方法,那麼咱們再對update進行修改一下
Watcher.prototype.update = function () { //調用fn()
  this.fn()
  // notify的時候值已經更改了
  // 再經過vm, exp來獲取新的值
  let arr = this.exp.trim().split('.')
  let val = this.vm
  arr.forEach(function(key){
    val = val[key]
  })
  this.fn(val) // 將每次拿到的新值去替換{{}}的內容便可
}

問題:雙向數據綁定

未完待續...prototype

相關文章
相關標籤/搜索