1. 程式人生 > 程式設計 >Vue2.x 的雙向繫結原理及實現

Vue2.x 的雙向繫結原理及實現

目錄
  • 1、實現過程
  • 2、顯示一個 Observer
  • 3、實現 Watcher
  • 4、實現 Compile
  • 5、新增解析事件
  • 6、完整版 my

Vue 是利用的 Object.defineProperty() 方法進行的資料劫持,利用 set、get 來檢測資料的讀寫。

https://run.net/RMIKp/embedded/all/light

MVVM 框架主要包含兩個方面,資料變化更新檢視,檢視變化更新資料。

檢視變化更新資料,如果是像 input 這種標籤,可以使用 oninput 事件..

資料變化更新檢視可以使用 Object.definProperty() set 方法可以檢測資料變化,當資料改變就會觸發這個函式,然後更新檢視。

1、實現過程

我們知道了如何實現雙向綁定了,首先要對資料進行劫持監聽,所以我們需要設定一個 Observer 函式,用來監聽所有屬性的變化。

如果屬性發生了變化,那就要告訴訂閱者 watcher 看是否需要更新資料,如果訂閱者有多個,則需要一個 Dep 來收集這些訂閱者,然後在監聽器 observer watcher 之間進行統一管理。

還需要一個指令解析器 compile,對需要監聽的節點和屬性進行掃描和解析。

因此,流程大概是這樣的:

  • 實現一個監聽器 Observer,用來劫持並監聽所有屬性,如果發生變動,則通知訂閱者。
  • 實現一個訂閱者 Watcher,當接到屬性變化的通知時,執行對應的函式,然後更新檢視,使用 Dep 來收集這些 Watcher
  • 實現一個解析器 Compile,用於掃描和解析的節點的相關指令,並根據初始化模板以及初始化相應的訂閱器。

Vue2.x 的雙向繫結原理及實現

2、顯示一個 Observer

Observer 是一個數據監聽器,核心方法是利用 Object.defineProperty() 通過遞迴的方式對所有屬性都新增 settergetter 方法進行監聽。

var library = {
  book1: {
    name: ""www.cppcns.com,},book2: "",};
observe(library);
library.book1.name = "vue權威指南"; // 屬性name已經被監聽了,現在值為:“vue權威指南”
library.book2 = "沒有此書籍"; // 屬性book2已經被監聽了,現在值為:“沒有此書籍”

// 為資料新增檢測
function defineReactive(data,key,val) {
  observe(val); // 遞迴遍歷所有子屬性
  let dep = new Dep(); // 新建一個dep
  Object.defineProperty(data,{
    enumerable: true,configurable: true,get: function() {
      if (Dep.target) {
        // 判斷是否需要新增訂閱者,僅第一次需要新增,之後就不用了,詳細看Watcher函式
        dep.addSub(Dep.target); // 新增一個訂閱者
      }
      return val;
    },set: function(newVal) {
      if (val == newVal) return; // 如果值未發生改變就return
      val = newVal;
      console.log(
        "屬性" + key + "已經被監聽了,現在值為:“" + newVal.toString() + "”"
      );
      dep.notify(); // 如果資料發生變化,就通知所有的訂閱者。
    },});
}

// 監聽物件的所有屬性
function observe(data) {
  if (!data || typeof data !== "object") {
    return; // 如果不是物件就return
  }
  Object.keys(data).forEach(function(key) {
    defineReactive(data,data[key]);
  });
}
// Dep 負責收集訂閱者,當屬性發生變化時,觸發更新函式。
function Dep() {
  this.subs = {};
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },notify: function() {
    this.subs.forEach((sub) => sub.update());
  },};

思路分析中,需要有一個可以容納訂閱者訊息訂閱器 Dep,用於收集訂閱者,在屬性發生變化時執行對應的更新函式。

從程式碼上看,將訂閱器 Dep 新增在 getter 裡,是為了讓 Watcher 初始化時觸發,,因此,需要判斷是否需要訂閱者。

setter 中,如果有資料發生變化,則通知所有的訂閱者,然後訂閱者就會更新對應的函式。

到此為止,一個比較完整的 Observer 就完成了,接下來開始設計 Watcher.

3、實現 Watcher

訂閱者 Watcher 需要在初始化的時候將自己新增到訂閱器 Dep 中,我們已經知道監聽器 Observer 是在 get 時執行的 Watcher 操作,所以只需要在 Watcher 初始化的時候觸發對應的 get 函式去新增對應的訂閱者操作即可。

那給如何觸發 get 呢?因為我們已經設定了 Object.defineProperty() , 所以只需要獲取對應的屬性值就可以觸發了。

我們只需要在訂閱者 Watcher 初始化的時候,在 Dep.target 上快取下訂閱者,新增成功之後在將其去掉就可以了。

function Watcher(vm,exp,cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 將自己新增到訂閱器的操作
}

Watcher.prototype = {
  update: function() {
    this.run();
  },run: function() {
    var value = this.vm.data[thisoPxKUBIoeQ.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm,value,oldVal);
    }
  },get: function() {
    Dep.target = this; // 快取自己,用於判斷是否新增watcher。
    var value = this.vm.data[this.exp]; // 強制執行監聽器裡的get函式
    Dep.target = null; // 釋放自己
    return value;
  },};

到此為止, 簡單的額 Watcher 設計完畢,然後將 Observer Watcher 關聯起來,就可以實現一個簡單的的雙向綁定了。

因為還沒有設計解析器 Compile,所以可以先將模板資料寫死。

將程式碼轉化為 ES6 建構函式的寫法,預覽試試。

https://jsrun.net/8SIKp/embed...

這段程式碼因為沒有實現編譯器而是直接傳入了所繫結的變數,我們只在一個節點上設定一個數據(name)進行繫結,然後在頁面上進行 new MyVue,就可以實現雙向綁定了。

並兩秒後進行值得改變,可以看到,頁面也發生了變化。

// MyVue
proxyKeys(key) {
    var self = this;
    Object.defineProperty(this,{
        enumerable: false,get: function proxyGetter() {
            return self.data[key];
        },set: function proxySetter(newVal) {
            self.data[key] = newVal;
        }
    });
}

上面這段程式碼的作用是將 this.data 的 key 代理到 this 上,使得我可以方便的使用 this.xx 就可以取到 this.data.xx

4、實現 Compile

雖然上面實現了雙向資料繫結,但是整個過程都沒有解析 DOM 節店,而是固定替換的,所以接下來要實現一個解析器來做資料的解析和繫結工作。

解析器 compile 的實現步驟:

  • 解析模板指令,並替換模板資料,初始化檢視。
  • 將模板指定對應的節點繫結對應的更新函式,初始化相應的訂閱器。

為了解析模板,首先需要解析 DOM 資料,然oPxKUBIoeQ後對含有 DOM 元素上的對應指令進行處理,因此整個 DOM 操作較為頻繁,可以新建一個 fragment 片段,將需要的解析的 DOM 存入 fragment 片段中在進行處理。

function nodeToFragment(el) {
  var fragment = document.createDocumentFragment();
  var child = el.firstChild;
  while (child) {
    // 將Dom元素移入fragment中
    fragment.appendChild(child);
    child = el.firstChild;
  }
  return fragment;
}

接下來需要遍歷各個節點,對含有相關指令和模板語法的節點進行特殊處理,先進行最簡單模板語法處理,使用正則解析“{{變數}}”這種形式的語法。

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/; // 匹配{{xx}}
        var text = node.textContent;
        if (self.isTextNode(node) && reg.test(text)) {  // 判斷是否是符合這種形式{{}}的指令
            self.compileText(node,reg.exec(text)[1]);
        }
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 繼續遞迴遍歷子節點
        }
http://www.cppcns.com    });
},function compileText (node,exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node,initText);  // 將初始化的資料初始化到檢視中
    new Watcher(this.vm,function (value) {  // 生成訂閱器並繫結更新函式
        self.updateText(node,value);
    });
},function updateText (node,value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

獲取到最外層的節點後,呼叫 compileElement 函式,對所有的子節點進行判斷,如果節點是文字節點切匹配{{}}這種形式的指令,則進行編譯處理,初始化對應的引數。

然後需要對當前引數生成一個對應的更新函式訂閱器,在資料發生變化時更新對應的 DOM。

這樣就完成了解析、初始化、編譯三個過程了。

接下來改造一個 myVue 就可以使用模板變數進行雙向資料綁定了。

https://jsrun.net/K4IKp/embed...

5、新增解析事件

新增完 compile 之後,一個數據雙向繫結就基本完成了,接下來就是在 Compile 中新增更多指令的解析編譯,比如 v-modelv-onv-bind 等。

新增一個 v-model 和 v-on 解析:

function compile(node) {
  var nodeAttrs = node.attributes;
  var self = this;
  Array.prototype.forEach.call(nodeAttrs,function(attr) {
    var attrName = attr.name;
    if (isDirective(attrName)) {
      var exp = attr.value;
      var dir = attrName.substring(2);
      if (isEventDirective(dir)) {
        // 事件指令
        self.compileEvent(node,self.vm,dir);
      } else {
        // v-model 指令
        self.compileModel(node,dir);
      }
      node.removeAttribute(attrName); // 解析完畢,移除屬性
    }
  });
}
// v-指令解析
function isDirective(attr) {
  return attr.indexOf("v-") == 0;
}
// on: 指令解析
function isEventDirective(dir) {
  return dir.indexOf("on:") === 0;
}

上面的 compile 函式是用於遍歷當前 dom 的所有節點屬性,然後判斷屬性是否是指令屬性,如果是在做對應的處理(事件就去監聽事件、資料就去監聽資料..)

6、完整版 myVue

MyVue 中新增 mounted 方法,在所有操作都做完時執行。

class MyVue {
  constructor(options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
    Object.keys(this.data).forEach(function(key) {
      self.proxyKeys(key);
    });
    observe(this.data);
    new Compile(options.el,this);
    options.mounted.call(this); // 所有事情處理好後執行mounted函式
  }
  proxyKeys(key) {
    // 將this.data屬性代理到this上
    var self = this;
    Object.defineProperty(this,{
      enumerable: false,get: function getter() {
        return self.data[key];
      },set: function setter(newVal) {
        self.data[key] = newVal;
      },});
  }
}

然後就可以測試使用了。

https://jsrun.net/Y4IKp/embed...

總結一下流程,回頭在哪看一遍這個圖,是不是清楚很多了。

Vue2.x 的雙向繫結原理及實現

可以檢視的程式碼地址:Vue2.x 的雙向繫結原理及實現

到此這篇關於Vue2.x 的雙向繫結原理及實現的文章就介紹到這了,更多相關Vue 資料雙向繫結原理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!