Vue響應式原理簡單實現

簡單闡述一下vue中的MVVM響應式原理:

vue是採用數據劫持配合發佈者訂閱模式的方式,經過Object.defineProperty()來劫持各個屬性setter,getter。在數據發生變化時,發佈消息給依賴收集器(dep),去通知觀察者(watcher),作出對應的回調函數,去更新視圖。從而實現數據驅動視圖。html


MVVM做爲綁定的入口,整合Observer,Compile和Watcher三者,經過Observer來監聽model數據變化,經過Compile來解析編譯模板指令,最終利用Watcher搭起Observer,Compile之間的通訊橋樑,實現了數據變化=》視圖更新,視圖交互變化=》數據model變動的雙向綁定結果vue

實現本身的Vue

簡單功能介紹:node

1.數據改變,視圖自動更新git

2.視圖交互變化,數據變動github

3.解析v-text,v-html指令數組

4.綁定v-on和@的事件bash

5.數據代理markdown



1.建立一個入口MVVM

如圖所示,MVVM要實現劫持監聽全部屬性和解析指令,同時也要實現數據代理功能app

2.Compile解析指令

咱們知道頻繁的操做dom是很是消耗性能的,因此咱們須要將真實dom移入內存中操做--文檔碎片。因此Compile主要作了一下幾件事:dom

  • 將當前根節點全部子節點遍歷放到內存中
  • 編譯文檔碎片,替換模板(元素、文本)節點中屬性的數據
  • 將編譯的內容回寫到真實DOM上
  • 添加wacther觀察者到模板中渲染頁面的表達式

'''
class Compile {  constructor(el, vm) {    // 判斷el類型,獲得dom對象    this.el = this.isElementNode(el) ? el : document.querySelector(el)    // 保存MVVM實例對象    this.vm = vm    // 獲取文檔碎片對象,存放內存中減小重繪和迴流    const fragment = this.node2Frament(this.el)    // 編譯模板    this.compile(fragment)    // 添加到根元素上    this.el.appendChild(fragment)  }  // 判斷元素節點  isElementNode (node) {    return node.nodeType === 1  }  // 文檔碎片對象  node2Frament (el) {    // 建立文檔碎片對象    const f = document.createDocumentFragment()    let firstChild    // 將dom對象中的節點以此添加到f文檔碎片對象    while (firstChild = el.firstChild) {      f.appendChild(firstChild)    }    return f  }  // 編譯模板  compile (fragment) {    // 獲取子節點    var childNodes = fragment.childNodes    // 轉換數組遍歷    childNodes = [...childNodes]    childNodes.forEach((child) => {      // 元素節點      if (child.nodeType === 1) {        // 處理元素節點        this.compileElement(child)      } else {        // 處理文本文本節點        this.compileText(child)      }      // 元素節點是否有子節點      if (child.childNodes && child.childNodes.length) {        // 遞歸        this.compile(child)      }    })  }  // 處理文本節點  compileText (node) {    // 獲取文本信息    var content = node.textContent    const rgb = /\{\{(.+?)\}\}/    // 若是有{{}}表達式    if (rgb.test(content)) {      // 編譯內容      compileUtil['text'](node, content, this.vm)    }  }  // 處理元素節點  compileElement (node) {    // 獲取元素上屬性    var attributes = node.attributes    attributes = [...attributes]    // 遍歷屬性    attributes.forEach((attr) => {      // 解構賦值獲取屬性名屬性值      const { name, value } = attr;      // 判斷屬性名是否v-開頭      if (this.isDirective(name)) {        //  分割字符串        const [, dirctive] = name.split('-')        // dirname 爲html text等,eventName爲事件名        const [dirName, eventName] = dirctive.split(':')        // 跟新數據 數據驅動視圖        compileUtil[dirName](node, value, this.vm, eventName)        // 刪除標籤上的屬性        node.removeAttribute('v-' + dirctive)      } else if (this.isEventive(name)) {             //@開頭的事件        let [, eventName] = name.split('@')        compileUtil['on'](node, value, this.vm, eventName)        node.removeAttribute('@' + eventName)      }    })  }  isDirective (attrName) {    return attrName.startsWith('v-')  }  isEventive (eventName) {    return eventName.startsWith('@')  }}
'''複製代碼

編譯工具對象

經過Compile類解析了節點及文本各個指令和{{}}表達式,在經過編譯工具數據與視圖結合

``` class Compile { constructor(el, vm) { // 判斷el類型,獲得dom對象 this.el = this.isElementNode(el) ? el : document.querySelector(el) // 保存MVVM實例對象 this.vm = vm // 獲取文檔碎片對象,存放內存中減小重繪和迴流 const fragment = this.node2Frament(this.el) // 編譯模板 this.compile(fragment) // 添加到根元素上 this.el.appendChild(fragment) } // 判斷元素節點 isElementNode (node) { return node.nodeType === 1 } // 文檔碎片對象 node2Frament (el) { // 建立文檔碎片對象 const f = document.createDocumentFragment() let firstChild // 將dom對象中的節點以此添加到f文檔碎片對象 while (firstChild = el.firstChild) { f.appendChild(firstChild) } return f } // 編譯模板 compile (fragment) { // 獲取子節點 var childNodes = fragment.childNodes // 轉換數組遍歷 childNodes = [...childNodes] childNodes.forEach((child) => { // 元素節點 if (child.nodeType === 1) { // 處理元素節點 this.compileElement(child) } else { // 處理文本文本節點 this.compileText(child) } // 元素節點是否有子節點 if (child.childNodes && child.childNodes.length) { // 遞歸 this.compile(child) } }) } // 處理文本節點 compileText (node) { // 獲取文本信息 var content = node.textContent const rgb = /\{\{(.+?)\}\}/ // 若是有{{}}表達式 if (rgb.test(content)) { // 編譯內容 compileUtil['text'](node, content, this.vm) } } // 處理元素節點 compileElement (node) { // 獲取元素上屬性 var attributes = node.attributes attributes = [...attributes] // 遍歷屬性 attributes.forEach((attr) => { // 解構賦值獲取屬性名屬性值 const { name, value } = attr; // 判斷屬性名是否v-開頭 if (this.isDirective(name)) { // 分割字符串 const [, dirctive] = name.split('-') // dirname 爲html text等,eventName爲事件名 const [dirName, eventName] = dirctive.split(':') // 跟新數據 數據驅動視圖 compileUtil[dirName](node, value, this.vm, eventName) // 刪除標籤上的屬性 node.removeAttribute('v-' + dirctive) } else if (this.isEventive(name)) { //@開頭的事件 let [, eventName] = name.split('@') compileUtil['on'](node, value, this.vm, eventName) node.removeAttribute('@' + eventName) } }) } isDirective (attrName) { return attrName.startsWith('v-') } isEventive (eventName) { return eventName.startsWith('@') }} ```複製代碼

渲染工具對象

```
const updater = {  textUpdater (node, value) {    node.textContent = value  },  htmlUpdater (node, value) {    node.innerHTML = value  },  modelUpdater (node, value) {    node.value = value  }}
```複製代碼

經過Compile及兩個工具對象完全將html上模板中的{{}}及指令轉換爲所須要的數據,完成了視圖上解析指令--初始化視圖

3.劫持監聽全部屬性Observer

經過Object,defineProperty的方法來劫持每一個屬性,當屬性被修改時,會觸發set函數,此時,咱們要通知變化屬性的每一個訂閱者(watcher)來改變視圖在劫持每一個屬性的時候。

那每一個訂閱者又存在哪裏呢,這時候咱們須要一個dep實例來收集他們。

```
let uid = 0class Dep {  constructor() {    this.id = uid++    this.subs = []  }  // 收集觀察者  addSub (watcher) {    this.subs.push(watcher)  }  // 通知觀察者  notify () {    this.subs.forEach(w => w.update())  }}
```複製代碼

每一個dep實例都有一個數組來存放watcher,當數據發生改變,會觸發每一個實例的notify方法,watcher再去更新視圖,達到了響應式。

```
defineReactive: function(data, key, val) {        var dep = new Dep();        var childObj = observe(val);        Object.defineProperty(data, key, {            enumerable: true, // 可枚舉            configurable: false, // 不能再define            get: function() {                if (Dep.target) {                    dep.depend();                }                return val;            },            set: function(newVal) {                if (newVal === val) {                    return;                }                val = newVal;                // 新的值是object的話,進行監聽                childObj = observe(newVal);                // 通知訂閱者                dep.notify();            }        });    }```複製代碼

那麼watcher又是怎麼存放到subs數組中的呢?

在Compile函數中,咱們每次實現解析操做的時候,咱們添加一個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 = compileUtil.getVal(this.expr, this.vm)    Dep.target = null    return oldVal  }  // 更新視圖  update () {    const newVal = compileUtil.getVal(this.expr, this.vm)    if (this.oldVal !== newVal) {      this.cb(newVal)    }  }}```複製代碼

在watcher實例裏面綁定了一個更新函數cb,當調用update時候,就去執行更新函數到達渲染。

重點重點,dep和watcher產生關係

在初始模板的時候,在Compile裏,每一個watcher獲取oldVal值時,會觸發Object.defineProperty中get的方法,再觸發get函數前,執行Dep.target=this,將watcher實例綁定到Dep.target,當咱們執行get方法時,調用dep.addSub(Dep.target),就將watcher實例添加到了dep中的數組裏了。當模板渲染完成時。模板裏每一個須要解析的數據都綁定了一個watcher,同時,每一個watcher都被添加到了對應的dep中。


整個流程概述:

  1. 實例一個MVVM
  2. Observer劫持每一個屬性變成響應式,每一個屬性都會實例化一個dep
  3. Compile編譯模板,須要解析指令和{{}}時,添加一個watcher實例
  4. watcher實例內部會去讀取屬性值,觸發get方法,同時將watcher添加到dep裏
  5. 數據更新,觸發set方法,通知dep中全部收集的watcher,在經過watcher更新視圖


項目github地址

https://github.com/6sy/vue-mvvm.git複製代碼

總結

經過本身實現一下響應式原理,發現了本身還有不少須要走的路,在這項目中,是沒法監聽到數組變化的,而在vue當中,經過對源碼的瞭解,是經過添加攔截器的方式,在數組和原型對象之間添加,改寫了數組的七個方法,從而達到了響應式。


以上若是有什麼錯誤以及不足的地方,請大佬們熱心指出來。讓咱們一塊兒進步。

相關文章
相關標籤/搜索