1. 程式人生 > 程式設計 >vue mvvm資料響應實現

vue mvvm資料響應實現

為什麼實現資料響應式

當前vue、react等框架流行。無論是vue、還是react框架大家最初的設計思路都是類似的。都是以資料驅動檢視,資料優先。希望能夠通過框架減少開發人員直接操作節點,讓開發人員能夠把更多的精力放在業務上而不是過多的放在操作節點上。另一方面,框架會通過虛擬dom及diff演算法提高頁面效能。這其中需要資料優先最根本的思路就是實現資料響應式。so,本次來看下如何基於原生實現資料響應式。

vue中的資料響應

vue中會根據資料將資料通過大鬍子語法及指令渲染到檢視上,這裡我們以大鬍子語法為例。如下:

<div id="app">
    {{message}}
</div>
let vm = new Vue({
  el:"#app",data:{
    message:"測試資料"
  }
})
setTimeout(()=>{
  vm.message = "修改的資料";
},1000)

如上程式碼,很簡單 。vue做了兩件事情。一、把message資料初次渲染到檢視。二、當message資料改變的時候檢視上渲染的message資料同時也會做出響應。以最簡單的案例。帶著問題來看,通過原生js如何實現??這裡為了簡化操作便於理解,這裡就不去使用虛擬dom。直接操作dom結構。

實現資料初次渲染

根據vue呼叫方式。定義Vue類來實現各種功能。將初次渲染過程定義成編譯compile函式渲染檢視。通過傳入的配置以及操作dom來實現渲染。大概思路是通過正則查詢html 裡 #app 作用域內的表示式,然後查詢資料做對應的替換即可。具體實現如下:

class Vue {
  constructor(options) {
    this.opts = options;
    this.compile();
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    // 獲取所有子節點
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文字節點
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查詢資料替換 “{{}}”
          node.textContent = node.textContent.replace(reg,this.opts.data[$1]);
        }
      }
    })
  }
}

如上完成了初次渲染,將message資料渲染到了檢視上。但是會返現並沒對深層次的dom結構做處理也就是如下情況:

 <div id="app">
    1{{ message }}2
    <div>
      hello,{{ message }}
    </div>
  </div>

vue mvvm資料響應實現

渲染結果如上

發現結果並沒有達到預期。so,需要改下程式碼,讓節點可以深層次查詢就可以了。程式碼如下:

  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 獲取所有子節點
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文字節點
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查詢資料替換 “{{}}”
          node.textContent = node.textContent.replace(reg,this.opts.data[$1]);
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }

上述程式碼通過遞迴查詢節點 實現深層次節點的渲染工作。如此,就實現了檢視的初次渲染。

資料劫持

回過頭來看下上面說的第二個問題:當message資料改變的時候檢視上渲染的message資料同時也會做出響應。如何實現資料響應式?簡而言之就是資料變動影響檢視變動?再將問題拆分下 1. 如何知道資料變動了? 2.如何根據資料變動來更改檢視?

  • 如何知道資料變動了? 這裡就需要用到資料攔截了,或者叫資料觀察。把會變動的data資料觀察起來。當他變動的時候我們可以做後續的渲染事情。如何攔截資料呢 ?vue2裡採取的是definePrototype。
let obj = {
  myname:"張三"
}
Object.defineProperty(obj,'myname',{
  configurable:true,enumerable:true,get(){
    console.log("get.")
    return "張三";
  },set(newValue){
    console.log("set")
    console.log(newValue);
  }
})
console.log(obj);

上述程式碼會發現,通過defineProperty劫持的物件屬性下都會有get及set方法。那麼當我們獲取或者設定資料的時候就能出發對應的get及set 。這樣就能攔截資料做後續操作。

vue mvvm資料響應實現

還有沒有其他方式達到資料劫持的效果呢?ES6中出現了Proxy 代理物件同樣也可以達到類似劫持資料的功能。如下程式碼:

let obj = {
  myname:"張三"
}
let newObj = new Proxy(obj,{
  get(target,key){
    console.log("get...")
    return "張三"
  },set(target,name,newValue){
    console.log("set...");
    return Reflect.set(target,newValue);
  }
})

兩種方式都可以實現資料劫持。proxy功能更加強大,很多方法是defineProperty所不具備的。且proxy直接攔截的是物件而defineProperty攔截的是物件屬性。so,可以利用上述方式將data資料做劫持,程式碼如下:

observe(data){
    let keys = Object.keys(data);
    keys.forEach(key=>{
      let value = data[key];
      Object.defineProperty(data,key,{
        configurable:true,get(){
          return value;
        },set(newValue){
          value = newValue;
        }
      });
    })
 }

觀察者模式實現資料響應

有了劫持資料方式後,接下來需要實現的就是當修改資料的時候將新資料渲染到檢視。如何辦到呢?會發現,需要在data設定的時候觸發檢視的compile編譯。二者之間互相影響,此時可以想到利用觀察者模式,通過觀察者模式讓二者產生關聯,如下:

vue mvvm資料響應實現

圖略小,程式碼也貼上吧。

class Vue extends EventTarget {
  constructor(options) {
    super();
    this.opts = options;
    this.observe(this.opts.data);
    this.compile();
  }
  observe(data){
    let keys = Object.keys(data);
    let _this = this;
    keys.forEach(key=>{
      let value = data[key];
      Object.defineProperty(data,set(newValue){
          _this.dispatchEvent(new CustomEvent(key,{
            detail:newValue
          }));
          value = newValue;
        }
      });
    })
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 獲取所有子節點
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文字節點
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查詢資料替換 “{{}}”
          node.textContent = node.textContent.replace(reg,this.opts.data[$1]);
          this.addEventListener($1,e=>{
            let oldValue = this.opts.data[$1];
            let newValue = e.detail;
            let reg = new RegExp(oldValue);
            node.textContent = node.textContent.replace(reg,newValue);
          })
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }
}

如上,成功的通過觀察者模式實現了資料的響應。但是會發現data與compile之間需要通過鍵名來進行關聯。如果data資料結構巢狀關係複雜後面會比較難處理。有沒有一種方式讓二者鬆解耦呢?這時候可以用釋出訂閱模式來進行改造。

釋出訂閱模式改造響應式

vue mvvm資料響應實現

還是略小,也還是貼上程式碼:

class Vue {
  constructor(options) {
    this.opts = options;
    this.observe(this.opts.data);
    this.compile();
  }
  observe(data){
    let keys = Object.keys(data);
    let _this = this;
    keys.forEach(key=>{
      let value = data[key];
      let dep = new Dep();
      Object.defineProperty(data,get(){
          if(Dep.target){
            dep.addSub(Dep.target); 
          }
          return value;
        },set(newValue){
          dep.notify(newValue);
          value = newValue;
        }
      });
    })
  }
  compile() {
    let ele = document.querySelector(this.opts.el);
    this.compileNodes(ele);
  }
  compileNodes(ele) {
    // 獲取所有子節點
    let childNodes = ele.childNodes;
    childNodes.forEach(node => {
      if (node.nodeType === 3) {
        // 找到所有的文字節點
        let nodeContent = node.textContent;
        // 匹配“{{}}”
        let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
        if (reg.test(nodeContent)) {
          let $1 = RegExp.$1;
          // 查詢資料替換 “{{}}”
          node.textContent = node.textContent.replace(reg,this.opts.data[$1]);
          new Watcher(this.opts.data,$1,(newValue)=>{
            let oldValue = this.opts.data[$1];
            let reg = new RegExp(oldValue);
            node.textContent = node.textContent.replace(reg,newValue);
          })
        }
      } else if (node.nodeType === 1) {
        if (node.childNodes.length > 0) {
          this.compileNodes(node);
        }
      }
    })
  }
}

class Dep{
  constructor(){
    this.subs = [];
  }
  addSub(sub){
    this.subs.push(sub);
  }
  notify(newValue){
    this.subs.forEach(sub=>{
      sub.update(newValue);
    })
  }
}

class Watcher{
  constructor(data,cb){
    Dep.target = this;
    data[key];
    this.cb = cb;
    Dep.target = null;
  }
  update(newValue){
    this.cb(newValue);
  }
}

如上程式碼思路是 針對每個資料會生成一個dep(依賴收集器)在資料get的時候收集watcher,將watcher 新增到dep裡儲存。資料一旦有改變觸發notify釋出訊息從而影響compile編譯更新檢視。這個流程也可以參看下圖:

vue mvvm資料響應實現

如上就完成了檢視響應。通過上述程式碼,我們可以看出實現資料響應兩個核心點1.資料劫持。2.觀察者和釋出訂閱。在這我們可以思考一個問題,2個設計模式都是可以實現的但是有什麼區別呢?

觀察者與釋出訂閱

這裡需要從概念來看

  • 觀察者模式:定義一個物件與其他物件之間的一種依賴關係,當物件發生某種變化的時候,依賴它的其它物件都會得到更新,一對多的關係。
  • 釋出訂閱模式:是一種訊息正規化,訊息的傳送者(稱為釋出者)不會將訊息直接傳送給特定的接收者(稱為訂閱者)。而是將釋出的訊息分為不同的類別,無需瞭解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達對一個或多個類別的興趣,只接收感興趣的訊息,無需瞭解哪些釋出者(如果有的話)存在。

vue mvvm資料響應實現

兩者之間關係,釋出訂閱是三者之間關係。釋出訂閱會多了一個關係器來組織主題和觀察者之間的關係。這樣做的好處就是鬆解耦。看上面響應式例子可以看出觀察者需要通過事件名稱來進行關聯。釋出訂閱定義dep管理器之後data和compile徹底解耦,讓二者鬆散解耦。在處理多層資料結構上釋出訂閱會更清晰。鬆解耦能夠應對更多變化,把模組之間依賴降到最低。釋出訂閱廣義上是觀察者模式。

好了 暫時先over 。 如果覺得有收穫的話可以點個贊,贈人玫瑰,手有餘香!!!!

以上就是vue mvvm資料響應實現的詳細內容,更多關於vue mvvm資料響應的資料請關注我們其它相關文章!