(1.8w字,建議收藏)Vue源碼解析—動手實現簡化版MVVM

Vue源碼解析—動手實現簡化版MVVM

引言

相信只要去面試 Vue,都會被用到 vue的雙向數據綁定,你若是隻說個 mvvm就是視圖模型模型視圖,只要數據改變視圖也會同步更新,那可能達不到面試官想要的那個層次。甚至能夠說這一點就讓面試官以爲你知識瞭解的還不夠,只是粗略地明白雙向綁定這個概念。javascript

本博客旨在經過一個簡化版的代碼來對 mvvm 理解更加深入,如若存在問題,歡迎評論提出,謝謝您!html

最後,但願你給一個點贊或 star :star:,謝謝您的支持!前端

實現源碼傳送門vue

同時,也會收錄在小獅子前端筆記倉庫裏 ✿✿ヽ(°▽°)ノ✿java

小獅子前端の學習整理筆記 Front-end-learning-to-organize-notesnode

實現效果: git

幾種實現雙向綁定的作法

目前幾種主流的 mvc(vm)框架都實現了單向數據綁定,即用數據操做視圖,數據更新,視圖同步更新。而雙向數據綁定無非就是在單向綁定的基礎上給可輸入元素(如 inputtextarea等)添加了 change(input)事件,來動態修改 modelview,這樣就能用視圖來操做數據了,即視圖更新,數據同步更新。github

實現數據綁定的作法大體有以下幾種:面試

發佈者-訂閱者模式(backbone.js) 髒值檢查(angular.js)將舊值和新值進行比對,若是有變化的話,就會更新視圖,最簡單的方式就是經過 setInterval()定時輪詢檢測數據變更。 數據劫持(vue.js)c#

發佈者-訂閱者模式:通常經過 subpub 的方式實現數據和視圖的綁定監聽,更新數據方式一般作法是 vm.set('property', value)

但上述方式對比如今來講知足不了咱們須要了,咱們更但願經過 vm.property = value 這種方式更新數據,同時自動更新視圖,因而有了下面兩種方式:

髒值檢測angular.js 是經過髒值檢測的方式比對數據是否變動,來決定是否更新視圖,最簡單的方式就是經過 setInterval()定時輪詢檢測數據變更。固然,它只在指定的事件觸發時才進入髒值檢測,大體以下:

  • DOM事件,譬如用戶輸入文本,點擊按鈕等。(ng-click)
  • XHR響應事件($http
  • 瀏覽器 Location 變動事件($location
  • Timer 事件($timeout, $interval

數據劫持vue.js 則是採用數據劫持結合發佈者-訂閱者模式的方式,經過 object.defineProperty() 來劫持各個屬性的 settergetter,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。

實現 mvvm 的雙向綁定

要實現 mvvm 的雙向綁定,就必需要實現如下幾點:

  • 實現一個指令解析器 Compile,對每一個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數
  • 實現一個數據監聽器Observer,可以對數據對象的全部屬性進行監聽,若有變更可拿到最新值並通知訂閱者
  • 實現一個Watcher,做爲鏈接ObserverCompile的橋樑,可以訂閱並收到每一個屬性變更的通知,執行指令綁定的相應回調函數,從而更新視圖
  • mvvm入口函數,整合以上三者

整合流程圖以下圖所示:

實現指令解析器 Compile

compile 主要作的事情是解析模板指令,將模板中的變量替換成數據,而後初始化渲染頁面視圖,並將每一個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變更,收到通知,更新視圖,以下圖所示:

由於遍歷解析的過程有屢次操做 dom 節點,爲提升性能和效率,會先將 vue 實例根節點的 el 轉換成文檔碎片fragment進行解析編譯操做,解析完成,再將fragment添加回原來的真實dom節點中。

html 頁面引入咱們從新寫的 myVue.js

<script src="./myVue.js"></script>
複製代碼

建立 myVue

建立一個 myVue 類,構造函數以下所示,將頁面的掛載 el、數據 data、操做集 options 進行保存。

class myVue{
  constructor(options){
    this.$el = options.el
    this.$data = options.data
    this.$options = options
    if(this.$el){
      // 1.實現數據觀察者(省略...)
      // 2.實現指令解析器
      new Compile(this.$el,this)
    }
    // console.log(this)
  }
}
複製代碼

實現 Compile

具體實現步驟:

  • 判斷當前掛載是否爲元素節點,不是的話就得尋找 query
  • 獲取文檔碎片對象,放入內存中來操做咱們的 dom節點,目的是減小頁面的迴流和重繪
  • 最後,將編譯後的模板添加到根元素
class Compile{
  constructor(el,vm){
    // 判斷是否爲元素節點,若是不是就query
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    // 一、獲取文檔碎片對象,放入內存中,會減小頁面的迴流和重繪
    const fragment = this.node2Fragment(this.el)
    // 二、編譯模板
    this.compile(fragment)
    // 三、追加子元素到根元素
    this.el.appendChild(fragment)
  }
複製代碼

判斷是否爲元素節點,直接判斷nodeType是否爲1便可

isElementNode(node){
  return node.nodeType  === 1
}
複製代碼

經過 document.createDocumentFragment() 建立文檔碎片對象,經過 el.firstChild 是否還存在來判斷,而後將 dom 節點添加到文檔碎片對象中,最後 return

node2Fragment(el){
  // 建立文檔碎片對象
  const fragment = document.createDocumentFragment()
  let firstChild
  while(firstChild =  el.firstChild){
    fragment.appendChild(firstChild)
  }
  return fragment
}
複製代碼

編譯模板

解析模板時,會獲取獲得全部的子節點,此時分兩種狀況,即元素節點和文本節點。若是當前節點還存在子節點,則須要經過遞歸操做來遍歷其子節點。

compile(fragment){
  // 一、獲取全部子節點
  const childNodes = fragment.childNodes;
  [...childNodes].forEach(child=>{
    // console.log(child)
    // 若是是元素節點,則編譯元素節點
    if(this.isElementNode(child)){
       // console.log('元素節點',child) 
      this.compileElement(child)
    }else{
      // 其它爲文本節點,編譯文本節點
      // console.log('文本節點',child)
      this.compileText(child)
    }
    if(child.childNodes && child.childNodes.length){
      this.compile(child)
    }
  })
}
複製代碼

編譯元素節點(碰見設計模式)

節點 node 上有一個 attributes 屬性,來獲取當前節點的全部屬性,經過是否以 v- 開頭來判斷當前屬性名稱是否爲一個指令。若是是一個指令的話,還需進行分類編譯,用數據來驅動視圖。更新數據完畢後,再經過 removeAttribute 事件來刪除指令上標籤的屬性。

若是是非指令的話,例如事件 @click="sayHi",僅需經過指令 v-on 來實現便可。

對於不一樣的指令,咱們最好進行一下封裝,這裏就巧妙運用了 策略模式

compileElement(node){
    const attributes = node.attributes;
    [...attributes].forEach(attr=>{
      // console.log(attr)
      const {name,value} = attr;
      // console.log(name,value)
      // 判斷當前name值是否爲一個指令,經過是否以 'v-' 開頭來判斷
      if(this.isDirective(name)){
        // console.log(name.split('-'))
        const [,directive] = name.split('-') // text html model on:click
        // console.log(directive)
        const [dirName,eventName] = directive.split(':') // text html model on
        // 更新數據 數據驅動視圖
        complieUtil[dirName](node,value,this.vm,eventName)
        // 刪除指令上標籤上的屬性
        node.removeAttribute('v-' + directive)
      }else if(this.isEventName(name)){ // @click="sayHi"
        let [,eventName] = name.split('@')
        complieUtil['on'](node,value,this.vm,eventName)
      }
    })
  }
複製代碼

判斷當前 attrName 是否爲一個指令,僅需判斷是否以 v- 開頭

isDirective(attrName){
    return attrName.startsWith('v-')
  }
複製代碼

判斷當前 attrName 是否爲一個事件,就看是否以'@'開頭的事件綁定

isEventName(attrName){
    return attrName.startsWith('@')
  }
複製代碼

指令處理集合

const complieUtil = {
  getVal(expr,vm){
    return expr.split('.').reduce((data,currentVal)=>{
      // console.log(currentVal)
      return data[currentVal]
    },vm.$data)
  },
  text(node,expr,vm){
    let value;
    // 元素節點
    if(expr.indexOf('{{') !== -1){
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
        return this.getVal(args[1],vm);
      })
    }else{ // 文本節點
      value = this.getVal(expr,vm)
    }
    this.updater.textUpdater(node,value)
  },
  html(node,expr,vm){
    const value = this.getVal(expr,vm)
    this.updater.htmlUpdater(node,value)
  },
  model(node,expr,vm){
    const value = this.getVal(expr,vm)
    this.updater.modelUpdater(node,value)
  },
  on(node,expr,vm,eventName){
    let fn = vm.$options.methods && vm.$options.methods[expr] 
    // 一、讓fn經過bind函數指向原來的vm 二、默認冒泡
    node.addEventListener(eventName,fn.bind(vm),false) 
  },
  bind(node,expr,vm,attrName){
    
  },
  // 更新的函數
  updater:{
    textUpdater(node,value){
      node.textContent = value
    },
    htmlUpdater(node,value){
      node.innerHTML = value
    },
    modelUpdater(node,value){
      node.value = value
    }
  }
}
複製代碼

實現數據監聽器 Observer

利用 Obeject.defineProperty() 來監聽屬性變更,那麼將須要 observe 的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 settergetter 。這樣的話,給這個對象的某個值賦值,就會觸發 setter ,那麼就能監聽到了數據變化。具體代碼以下:

class Observer{
  constructor(data){
    this.observe(data)
  }
  observe(data){
    if(data && typeof data === 'object'){
      // console.log(Object.keys(data))
      // 進行數據劫持
      Object.keys(data).forEach(key=>{
        this.defineReactive(data,key,data[key])
      })
    }
  }
  defineReactive(obj,key,value){
    // 遞歸遍歷
    this.observe(value)
    Object.defineProperty(obj,key,{
      enumerable: true,
      configurable: false,
      get(){
        // 訂閱數據變化時,往Dep中添加觀察者,進行依賴收集
        return value
      },
      // 經過箭頭函數改變this指向到class Observer
      set:(newVal)=>{
        this.observe(newVal)
        if(newVal !== value){
          value = newVal
        }
      }
    })
  }
}
複製代碼

data 示例以下:

data: {
  person:{
    name: 'Chocolate',
    age: 20,
    hobby: '寫代碼'
  },
  msg: '超逸の技術博客',
  htmlStr: '<h3>歡迎一塊兒學習~</h3>'
},
複製代碼

實現 watcher 去更新視圖

Watcher 訂閱者做爲 ObserverCompile 之間通訊的橋樑,主要作的事情是:

  • 在自身實例化時往屬性訂閱器( dep )裏面添加本身
  • 自身必須有一個 update()方法
  • 待屬性變更dep.notify()通知時,能調用自身的 update() 方法,並觸發 Compile 中綁定的回調。

Watcher 訂閱者

實例化 Watcher 的時候,調用 getOldVal() 方法,來獲取舊值。經過 Dep.target = watcherInstance(this) 標記訂閱者是當前 watcher實例(即指向本身)。

class Watcher{
  constructor(vm,expr,cb){
    this.vm = vm
    this.expr = expr
    this.cb = cb
    // 先將舊值進行保存
    this.oldVal = this.getOldVal()
  }
  getOldVal(){
    // 將當前訂閱者指向本身
    Dep.target = this
    // 獲取舊值
    const oldVal = complieUtil.getVal(this.expr,this.vm)
    // 添加完畢,重置
    Dep.target = null
    return oldVal
  }
  // 比較新值與舊值,若是有變化就更新視圖
  update(){
    const newVal = complieUtil.getVal(this.expr,this.vm)
    // 若是新舊值不相等,則將新值callback
    if(newVal !== this.oldVal){
      this.cb(newVal)
    }
  }
}
複製代碼

強行觸發屬性定義的 get 方法,get 方法執行的時候,就會在屬性的訂閱器 dep 添加當前watcher 實例,從而在屬性值有變化的時候,watcherInstance(this) 就能收到更新通知。

// 上文省略...
defineReactive(obj,key,value){
    // 遞歸遍歷
    this.observe(value)
    const dep = new Dep()
    Object.defineProperty(obj,key,{
      enumerable: true,
      configurable: false,
      get(){
        // 訂閱數據屬性時,往Dep中添加觀察者,進行依賴收集
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      // 經過箭頭函數改變this指向到class Observer
      set:(newVal)=>{
        this.observe(newVal)
        if(newVal !== value){
          value = newVal
          // 若是新舊值不一樣,則告訴Dep通知變化
          dep.notify()
        }
      }
    })
  }
複製代碼

訂閱器 dep

主要作兩件事情:

  • 收集訂閱者
  • 通知訂閱者更新
class Dep{
  constructor(){
    this.subs = []
  }
  // 收集觀察者
  addSub(watcher){
    this.subs.push(watcher)
  }
  // 通知觀察者去更新
  notify(){
    console.log('觀察者',this.subs);
    this.subs.forEach(watcher => watcher.update())
  }
}
複製代碼

修改咱們本來的 Compile.js 文件

作完上述事情後,此時,當咱們修改某個數據時,數據已經發生了變化,可是視圖沒有更新。那咱們在何時來添加綁定 watcher 呢?請繼續看下圖

也就是說,當咱們訂閱數據變化時,來綁定更新函數,從而讓 watcher 去更新視圖。此時咱們修改咱們本來的 Compile.js 文件以下:

// 指令處理集合
const complieUtil = {
  getVal(expr,vm){
    return expr.split('.').reduce((data,currentVal)=>{
      // console.log(currentVal)
      return data[currentVal]
    },vm.$data)
  },
  // 獲取新值 對{{a}}--{{b}} 這種格式進行處理
  getContentVal(expr,vm){
    return expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
      // console.log(args[1]);
      return this.getVal(args[1],vm);
    })
  },
  text(node,expr,vm){
    let value;
    if(expr.indexOf('{{') !== -1){
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
         // 綁定watcher從而更新視圖
        new Watcher(vm,args[1],()=>{
          this.updater.textUpdater(node,this.getContentVal(expr,vm))
          // console.log(expr);
        })
        return this.getVal(args[1],vm);
      })
    }else{ // 也多是v-text='obj.name' v-text='msg'
      value = this.getVal(expr,vm)
    }
    this.updater.textUpdater(node,value)
  },
  html(node,expr,vm){
    const value = this.getVal(expr,vm)
    new Watcher(vm,expr,(newVal)=>{
      this.updater.htmlUpdater(node,newVal)
    })
    this.updater.htmlUpdater(node,value)
  },
  model(node,expr,vm){
    const value = this.getVal(expr,vm)
    // 訂閱數據變化時 綁定更新函數 更新視圖的變化
    // 數據==>視圖
    new Watcher(vm,expr,(newVal)=>{
      this.updater.modelUpdater(node,newVal)
    })
    this.updater.modelUpdater(node,value)
  },
  on(node,expr,vm,eventName){
    let fn = vm.$options.methods && vm.$options.methods[expr] 
    // 一、讓fn經過bind函數指向原來的vm 二、默認冒泡
    node.addEventListener(eventName,fn.bind(vm),false) 
  },
  bind(node,expr,vm,attrName){
    let attrVal = this.getVal(expr,vm)
    this.updater.attrUpdater(node,attrName,attrVal)
  },
  // 更新的函數
  updater:{
    textUpdater(node,value){
      node.textContent = value
    },
    htmlUpdater(node,value){
      node.innerHTML = value
    },
    modelUpdater(node,value){
      node.value = value
    },
    attrUpdater(node, attrName, attrVal){
      node.setAttribute(attrName,attrVal)
    }
  }
}
class Compile{
  constructor(el,vm){
    // 判斷是否爲元素節點,若是不是就query
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    // 一、獲取文檔碎片對象,放入內存中,會減小頁面的迴流和重繪
    const fragment = this.node2Fragment(this.el)
    // 二、編譯模板
    this.compile(fragment)
    // 三、追加子元素到根元素
    this.el.appendChild(fragment)
  }
  // 判斷是否爲元素節點,直接判斷nodeType是否爲1便可
  isElementNode(node){
    return node.nodeType  === 1
  }
  node2Fragment(el){
    // 建立文檔碎片對象
    const fragment = document.createDocumentFragment()
    let firstChild
    while(firstChild =  el.firstChild){
      fragment.appendChild(firstChild)
    }
    return fragment
  }
  compile(fragment){
    // 一、獲取全部子節點
    const childNodes = fragment.childNodes;
    [...childNodes].forEach(child=>{
      // console.log(child)
      // 若是是元素節點,則編譯元素節點
      if(this.isElementNode(child)){
        // console.log('元素節點',child) 
        this.compileElement(child)
      }else{
        // 其它爲文本節點,編譯文本節點
        // console.log('文本節點',child)
        this.compileText(child)
      }
      if(child.childNodes && child.childNodes.length){
        this.compile(child)
      }
    })
  }
  // 編譯元素節點
  compileElement(node){
    const attributes = node.attributes;
    [...attributes].forEach(attr=>{
      // console.log(attr)
      const {name,value} = attr;
      // console.log(name,value)
      // 判斷當前name值是否爲一個指令,經過是否以 'v-' 開頭來判斷
      if(this.isDirective(name)){
        // console.log(name.split('-'))
        const [,directive] = name.split('-') // text html model on:click
        // console.log(directive)
        const [dirName,eventName] = directive.split(':') // text html model on
        // 更新數據 數據驅動視圖
        complieUtil[dirName](node,value,this.vm,eventName)
        // 刪除指令上標籤上的屬性
        node.removeAttribute('v-' + directive)
      }else if(this.isEventName(name)){ // @click="sayHi"
        let [,eventName] = name.split('@')
        complieUtil['on'](node,value,this.vm,eventName)
      }
    })
  }
  // 編譯文本節點
  compileText(node){
    // {{}} v-text
    // console.log(node.textContent)
    const content = node.textContent
    if(/\{\{(.+?)\}\}/.test(content)){
      // console.log(content)
      complieUtil['text'](node,content,this.vm)
    }
  }
  isDirective(attrName){
    return attrName.startsWith('v-')
  }
  // 判斷當前attrName是否爲一個事件,以'@'開頭的事件綁定
  isEventName(attrName){
    return attrName.startsWith('@')
  }
}
複製代碼

此時,咱們就能經過數據變化來驅動視圖了,例如更改咱們的年齡 age 從原來的 20 設置爲 22,以下圖所示,發現數據更改, watcher 去更新了視圖。

知識再梳理

有了以前的代碼與流程圖結合,我想對於Vue源碼分析應該更加了解了,那麼咱們再次來梳理一下咱們學習的知識點。依舊是結合下面流程圖:

最開始,咱們實現了 Compile解析指令,找到 {{xxx}}、指令、事件、綁定等等,而後再 初始化視圖。但此時還有一件事情沒作,就是當數據發生變化的時候,在更新數據以前,咱們還要 訂閱數據變化,綁定更新函數,此時就須要加入訂閱者 Watcher了。當訂閱者觀察到數據變化時,就會觸發 Updater來更新視圖。

固然,建立 Watcher的前提時要進行數據劫持來監聽全部屬性,因此建立了 Observer.js 文件。在 get方法中,須要給 Dep 通知變化,此時就須要將 Dep 的依賴收集關聯起來,而且添加訂閱者 Watcher(這個 WatcherComplie 訂閱數據變化,綁定更新函數時就已經建立了的)。此時 Dep 訂閱器裏就有不少個 Watcher 了,有多少個屬性就對應有多少個 Watcher


那麼,咱們舉一個簡單例子來走一下上述流程圖:

假設本來 data 數據中有一個 a:1,此時咱們進行更新爲 a:10,因爲早已經對咱們的數據進行了數據劫持而且監聽了全部屬性,此時就會觸發 set 方法,在 set方法裏就會通知 Dep 訂閱器發生了變化,而後就會通知相關 Watcher 觸發 update 函數來更新視圖。而這些訂閱者 WatcherComplie 訂閱數據變化,綁定更新函數時就已經建立了。

視圖->數據

上述,咱們基本完成了數據驅動視圖,如今咱們來完成一下經過視圖的變化來更新數據,真正實現雙向數據綁定的效果。

在咱們 complieUtil 指令處理集合中的 model 模塊,給咱們當前節點綁定一個 input 事件便可。咱們能夠經過 e.target.value 來獲取當前 input 輸入框的值。而後比對一下舊值和新值是否相同,若是不一樣的話,就得須要更新,調用 setVal 方法(具體見下文代碼)。

model(node,expr,vm){
    let value = this.getVal(expr,vm)
    // 訂閱數據變化時 綁定更新函數 更新視圖的變化
    // 數據==>視圖
    new Watcher(vm,expr,(newVal)=>{
      this.updater.modelUpdater(node,newVal)
    })
    // 視圖==》數據
    node.addEventListener('input',(e)=>{
      var newValue = e.target.value
      if(value == newValue) return
      // 設置值
      this.setVal(expr,vm,newValue)
      value = newValue
    })
    this.updater.modelUpdater(node,value)
  },
複製代碼

setValgetVal 二者沒有多大區別,只是 set 時多了一個 inputVal。它們都是找到最底層 key 值,而後更新 value 值。

getVal(expr,vm){
  return expr.split('.').reduce((data,currentVal)=>{
    // console.log(currentVal)
    return data[currentVal]
  },vm.$data)
},
setVal(expr,vm,inputVal){
  return expr.split('.').reduce((data,currentVal)=>{
     data[currentVal] = inputVal
  },vm.$data)
},
複製代碼

更新 bug:在上文,對於 v-text指令處,咱們遺漏了綁定 Watcher 步驟,如今進行補充。

text(node,expr,vm){
    let value;
    if(expr.indexOf('{{') !== -1){
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
        // 綁定watcher從而更新視圖
        new Watcher(vm,args[1],()=>{
          this.updater.textUpdater(node,this.getContentVal(expr,vm))
          // console.log(expr);
        })
        return this.getVal(args[1],vm);
      })
    }else{ // 也多是v-text='obj.name' v-text='msg'
      value = this.getVal(expr,vm)
      // 綁定watcher從而更新視圖
      new Watcher(vm,expr,(newVal)=>{
        this.updater.textUpdater(node,newVal)
        // console.log(expr);
      })
    }
    this.updater.textUpdater(node,value)
  },
複製代碼

最終,當咱們更改 input 輸入框中的值時,發現其餘節點也跟着修改,這表明咱們的數據進行了修改,相關訂閱者觸發了 update 方法,雙向綁定功能實現!

實現 proxy

咱們在使用 vue 的時候,一般能夠直接 vm.msg 來獲取數據,這是由於 vue 源碼內部作了一層代理.也就是說把數據獲取操做 vm 上的取值操做 都代理到 vm.$data 上。

class myVue{
  constructor(options){
    this.$el = options.el
    this.$data = options.data
    this.$options = options
    if(this.$el){
      // 1.實現數據觀察者
      new Observer(this.$data)
      // 2.實現指令解析器
      new Compile(this.$el,this)
      // 3.實現proxy代理
      this.proxyData(this.$data)
    }
    // console.log(this)
  }
  proxyData(data){
    for(const key in data){
      Object.defineProperty(this,key,{
        get(){
          return data[key]
        },
        set(newVal){
          data[key] = newVal
        }
      })
    }
  }
}
複製代碼

咱們簡單測試一下,例如咱們給 button 綁定一個 sayHi() 事件,經過設置 proxy 作了一層代理後,咱們不須要像後面那樣經過 this.$data.person.name來更改咱們的數據,而直接能夠經過 this.person.name 來獲取咱們的數據。

methods: {
   sayHi() {
     this.person.name = '超逸'
     //this.$data.person.name = 'Chaoyi'
     console.log(this)
   }
}
複製代碼

大廠面試題

請闡述一下你對 MVVM 響應式的理解

vue.js 則是採用數據劫持結合發佈者-訂閱者模式的方式,經過 Object.defineProperty()來劫持各個屬性的gettersetter,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。

MVVM做爲數據綁定的入口,整合ObserverCompileWatcher 三者,經過Observer來監聽本身的model數據變化,經過Compile來解析編譯模板指令,最終利用Watcher搭起ObserverCompile之間的通訊橋樑,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變動的雙向綁定效果。

最開始,咱們實現了 Compile解析指令,找到 {{xxx}}、指令、事件、綁定等等,而後再初始化視圖。但此時還有一件事情沒作,就是當數據發生變化的時候,在更新數據以前,咱們還要訂閱數據變化,綁定更新函數,此時就須要加入訂閱者Watcher了。當訂閱者觀察到數據變化時,就會觸發Updater來更新視圖。

固然,建立 Watcher的前提時要進行數據劫持來監聽全部屬性,因此建立了 Observer.js 文件。在 get方法中,須要給 Dep 通知變化,此時就須要將 Dep 的依賴收集關聯起來,而且添加訂閱者 Watcher(這個 WatcherComplie 訂閱數據變化,綁定更新函數時就已經建立了的)。此時 Dep 訂閱器裏就有不少個 Watcher 了,有多少個屬性就對應有多少個 Watcher


那麼,咱們舉一個簡單例子來走一下上述流程圖:

假設本來 data 數據中有一個 a:1,此時咱們進行更新爲 a:10,因爲早已經對咱們的數據進行了數據劫持而且監聽了全部屬性,此時就會觸發 set 方法,在 set方法裏就會通知 Dep 訂閱器發生了變化,而後就會通知相關 Watcher 觸發 update 函數來更新視圖。而這些訂閱者 WatcherComplie 訂閱數據變化,綁定更新函數時就已經建立了。

總結與答疑

總算是把這篇長文寫完了,字數也是達到將近 1w8。經過學習 Vue MVVM源碼,對於 Vue 雙向數據綁定這一塊理解也更加深入了。固然,本文書寫的代碼還算是比較簡單,也參考了大佬的博客與代碼,同時,也存在不足而且小部分功能沒有實現,相較於源碼來講仍是有不少可優化和可重構的地方,那麼也歡迎小夥伴們來 PR。一塊兒來動手實現 mvvm

本篇博客參考文獻 笑馬哥:Vue的MVVM實現原理 github:mvvm 視頻學習:Vue源碼解析

相關文章
相關標籤/搜索