1. 程式人生 > 程式設計 >手動實現vue2.0的雙向資料繫結原理詳解

手動實現vue2.0的雙向資料繫結原理詳解

一句話概括:資料劫持(Object.defineProperty)+釋出訂閱模式

雙向資料繫結有三大核心模組(dep 、observer、watcher),它們之間是怎麼連線的,下面來一一介紹。

為了大家更好的理解雙向資料繫結原理以及它們之間是如何實現關聯的,先帶領大家複習一下發布訂閱模式。

一.首先了解什麼是釋出訂閱模式

直接上程式碼:

一個簡單的釋出訂閱模式,幫助大家更好的理解雙向資料繫結原理

//釋出訂閱模式
function Dep() {
  this.subs = []//收集依賴(也就是手機watcher例項),
}
Dep.prototype.addSub = function (sub) { //新增訂閱者
  this.subs.push(sub); //實際上新增的是watcher這個例項
}
Dep.prototype.notify = function (sub) { //釋出,這個方法的作用是遍歷陣列,讓每個訂閱者的update方法去執行
  this.subs.forEach((sub) => sub.update())
}

function Watcher(fn) {
  this.fn = fn;
}
Watcher.prototype.update = function () { //新增一個update屬性讓每一個例項都可以繼承這個方法
  this.fn();
}
let watcher = new Watcher(function () {
  alert(1)
});//訂閱
let dep = new Dep();
dep.addSub(watcher);//新增依賴,新增訂閱者
dep.notify();//釋出,讓每個訂閱者的update方法執行

二.new Vue()的時候做了什麼?

只是針對雙向資料繫結做說明

<template>
   <div id="app">
    <div>obj.text的值:{{obj.text}}</div>
    <p>word的值:{{word}}</p>
    <input type="text" v-model="word">
  </div>
</template>
<script>
  new Vue({
    el: "#app",data: {
      obj: {
        text: "向上",},word: "學習"
    },methods:{
    // ...
    }
  })
</script>

Vue建構函式都幹什麼了?

function Vue(options = {}) {
  this.$options = options;//接收引數
  var data = this._data = this.$options.data;
  observer(data);//對data中的資料進型迴圈遞迴繫結
  for (let key in data) {
    let val = data[key];
    observer(val);
    Object.defineProperty(this,key,{
      enumerable: true,get() {
        return this._data[key];
      },set(newVal) {
        this._data[key] = newVal;
      }
    })
  }
  new Compile(options.el,this)
};

在new Vue({…})建構函式時,首先獲取引數options,然後把引數中的data資料賦值給當前例項的_data屬性上(this._data = this.$options.data),重點來了,那下面的遍歷是為什麼呢?首先我們在操作資料的時候是this.word獲取,而不是this._data.word,所以是做了一個對映,在獲取資料的時候this.word,其實是獲取的this._data.word的值,大家可以在自己專案中輸出this檢視一下

手動實現vue2.0的雙向資料繫結原理詳解

1.接下來看看observer方法幹了什麼

function observer(data) {
  if (typeof data !== "object") return;
  return new Observer(data);//返回一個例項
}
function Observer(data) {
  let dep = new Dep();//建立一個dep例項
  for (let key in data) {//對資料進行迴圈遞迴繫結
    let val = data[key];
    observer(val);
    Object.defineProperty(data,get() {
        Dep.target && dep.depend(Dep.target);//Dep.target就是Watcher的一個例項
        return val;
      },set(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        observer(newVal);
        dep.notify() //讓所有方法執行
      }
    })
  }
}

Observer建構函式,首先let dep=new Dep(),作為之後的觸發資料劫持的get方法和set方法時,去收集依賴和釋出時呼叫,主要的操作就是通過Object.defineProperty對data資料進行迴圈遞迴繫結,使用getter/setter修改其預設讀寫,用於收集依賴和釋出更新。

2.再來看看Compile具體幹了那些事情

function Compile(el,vm) {
  vm.$el = document.querySelector(el);
  let fragment = document.createDocumentFragment(); //建立文件碎片,是object型別
  while (child = vm.$el.firstChild) {
    fragment.appendChild(child);
  };//用while迴圈把所有節點都新增到文件碎片中,之後都是對文件碎片的操作,最後再把文件碎片新增到頁面中,這裡有一個很重要的特性是,如果使用appendChid方法將原dom樹中的節點新增到fragment中時,會刪除原來的節點。
  replace(fragment);

  function replace(fragment) {
    Array.from(fragment.childNodes).forEach((node) => {//迴圈所有的節點
      let text = node.textContent;
      let reg = /\{\{(.*)\}\}/;
      if (node.nodeType === 3 && reg.test(text)) {//判斷當前節點是不是文字節點且符不符合{{obj.text}}的輸出方式,如果滿足條件說明它是雙向的資料繫結,要新增訂閱者(watcher)
        console.log(RegExp.$1); //obj.text
        let arr = RegExp.$1.split("."); //轉換成陣列的方式[obj,text],方便取值
        let val = vm;
        arr.forEach((key) => { //實現取值this.obj.text
          val = val[key];
        });
        new Watcher(vm,RegExp.$1,function (newVal) {
          node.textContent = text.replace(/\{\{(.*)\}\}/,newVal)
        });
        node.textContent = text.replace(/\{\{(.*)\}\}/,val); //對節點內容進行初始化的賦值
      }
      if (node.nodeType === 1) { //說明是元素節點
        let nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach((item) => {
          if (item.name.indexOf("v-") >= 0) {//判斷是不是v-model這種指令
            node.value = vm[item.value]//對節點賦值操作
          }
          //新增訂閱者
          new Watcher(vm,item.value,function (newVal) {
            node.value = vm[item.value]
          });
          node.addEventListener("input",function (e) {
            let newVal = e.target.value;
            vm[item.value] = newVal;
          })
        })
      }
      if (node.childNodes) { //這個節點裡還有子元素,再遞迴
        replace(node);
      }
    })
  }

  //這是頁面中的文件已經沒有了,所以還要把文件碎片放到頁面中
  vm.$el.appendChild(fragment);

}

Compile(編譯方法)

首先解釋一下DocuemntFragment(文件碎片)它是一個dom節點收容器,當你創造了多個節點,當每個節點都插入到文件當中都會引發一次迴流,也就是說瀏覽器要回流多次,十分耗效能,而使用文件碎片就是把多個節點都先放入到一個容器中,最後再把整個容器直接插入就可以了,瀏覽器只回流了1次。

Compile方法首先遍歷文件碎片的所有節點,1.判斷是否是文字節點且符不符合{{obj.text}}的雙大括號的輸出方式,如果滿足條件說明它是雙向的資料繫結,要新增訂閱者(watcher),new Watcher(vm,動態繫結的變數,回撥函式fn) 2.判斷是否是元素節點且屬性中是否含有v-model這種指令,如果滿足條件說明它是雙向的資料繫結,要新增訂閱者(watcher),new Watcher(vm,動態繫結的變數,回撥函式fn) ,直至遍歷完成。

最後別忘了把文件碎片放到頁面中

3.Dep建構函式(怎麼收集依賴的)

var uid=0;
//釋出訂閱
function Dep() {
  this.id=uid++;
  this.subs = [];
}
Dep.prototype.addSub = function (sub) { //訂閱
  this.subs.push(sub); //實際上新增的是watcher這個例項
}
Dep.prototype.depend = function () { // 訂閱管理器
  if(Dep.target){//只有Dep.target存在時採取新增
    Dep.target.addDep(this);
  }
}
Dep.prototype.notify = function (sub) { //釋出,遍歷陣列讓每個訂閱者的update方法去執行
  this.subs.forEach((sub) => sub.update())
}

Dep建構函式內部有一個id和一個subs,id=uid++,id用於作為dep物件的唯一標識,subs就是儲存watcher的陣列。depend方法就是一個訂閱的管理器,會呼叫當前watcher的addDep方法新增訂閱者,當觸發資料劫持(Object.defineProperty)的get方法時會呼叫Dep.target && dep.depend(Dep.target)新增訂閱者,當資料改變時觸發資料劫持(Object.defineProperty)的set方法時會呼叫dep.notify方法更新操作。

4.Watcher建構函式幹了什麼

function Watcher(vm,exp,fn) {
  this.fn = fn;
  this.vm = vm;
  this.exp = exp //
  this.newDeps = [];
  this.depIds = new Set();
  this.newDepIds = new Set();
  Dep.target = this; //this是指向當前(Watcher)的一個例項
  let val = vm;
  let arr = exp.split(".");
  arr.forEach((k) => { //取值this.obj.text
    val = val[k] //取值this.obj.text,就會觸發資料劫持的get方法,把當前的訂閱者(watcher例項)新增到依賴中
  });
  Dep.target = null;
}
Watcher.prototype.addDep = function (dep) {
  var id=dep.id;
  if(!this.newDepIds.has(id)){
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if(!this.depIds.has(id)){
      dep.addSub(this);
    }
  }
 
}
Watcher.prototype.update = function () { //這就是每個繫結的方法都新增一個update屬性
  let val = this.vm;
  let arr = this.exp.split(".");
  arr.forEach((k) => { 
    val = val[k] //取值this.obj.text,傳給fn更新操作
  });
  this.fn(val); //傳一個新值
}

Watcher建構函式幹了什麼

1 接收引數,定義了幾個私有屬性( this.newDep,this.depIds
,this.newDepIds)

2. Dep.target = this,通過引數進行data取值操作,這就會觸發Object.defineProperty的get方法,它會通過訂閱者管理器(dep.depend())新增訂閱者,新增完之後再將Dep.target=null置為空;

3.原型上的addDep是通過id這個唯一標識,和幾個私有屬性的判斷防止訂閱者被多次重複新增

4.update方法就是當資料更新時,dep.notify()執行,觸發訂閱者的update這個方法, 執行釋出更新操作。

總結一下

vue2.0中雙向資料繫結,簡單來說就是Observer、Watcher、Dep三大部分;

1.首先用Object.defineProperty()迴圈遞迴實現資料劫持,為每個屬性分配一個訂閱者集合的管理陣列dep;

2.在編譯的時候,建立文件碎片,把所有節點新增到文件碎片中,遍歷文件碎片的所有結點,如果是{{}},v-model這種,new Watcher()例項並向dep的subs陣列中新增該例項

3.最後修改值就會觸發Object.defineProperty()的set方法,在set方法中會執行dep.notify(),然後迴圈呼叫所有訂閱者的update方法更新檢視。

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