面試官:VUE雙向數據綁定原理&&實現,你知否蜡励?

敲黑板劃重點令花,這是考點。vue帶給我們便利凉倚,我們也要知其然知其所以然兼都,才能稱對得起碼農菜鳥這個稱謂,才能和面試官閑話把vue家常稽寒。接下來扮碧,請集中注意力,我們來抽絲剝繭杏糙。

一慎王、原理

先來看js對象的基本方法defineProperty():

var obj  = {};
Object.defineProperty(obj, 'name', {
    get: function() {
         console.log('我獲取了name屬性')
         return val;
     },
    set: function (newVal) {
         console.log('我設置了name屬性為:' + newVal)
     }
})
obj.name = '魔丸';//在設置obj的name屬性時,觸發(fā)了set方法
var val = obj.name;//在獲取obj的name屬性時宏侍,觸發(fā)了get方法

相信這個方法大家都了解赖淤,沒錯,vue就是運用了該方法實現的雙向數據綁定谅河。嘮叨:vue.js 采用數據劫持結合發(fā)布者-訂閱者模式的方式咱旱,通過Object.defineProperty()來劫持各個屬性的setter确丢,getter,在數據變動時發(fā)布消息給訂閱者吐限,觸發(fā)相應的監(jiān)聽回調鲜侥。也就是說數據和視圖同步,數據發(fā)生變化诸典,視圖跟著變化描函,視圖變化,數據也隨之發(fā)生改變狐粱,大家都是拴在一條繩子上的螞蚱舀寓。是不是似懂非懂,別急脑奠,繼續(xù)上網圖:

原理圖講解:

1 .observer(數據監(jiān)聽器/觀察者):用來實現對vue的data中定義的每個屬性循環(huán)用Object.defineProperty()實現數據劫持基公,以便利用其中的setter和getter,然后通知watcher(訂閱者)宋欺,watcher會觸發(fā)它的update方法轰豆,對視圖進行更新。

**2.指令解析器Compile: **對每個元素節(jié)點的指令進行掃描和解析齿诞,根據指令模板替換數據酸休,并綁定相應的更新函數。

3 .訂閱者

  • 連接Observer和Compile的橋梁祷杈,能夠訂閱并收到每個屬性變動的通知斑司,執(zhí)行指令綁定的相應回調函數,從而更新視圖。
  • 在vue中v-model,v-name眉反,{{}}等都可以對數據進行顯示,假如一個屬性同時綁定了這三個指令僵缺,那么當這個屬性值改變時,這三個指令對應的html視圖都要改變踩叭。每當用到這樣一個指令磕潮,就在Dep中增加一個訂閱者。訂閱者只是更新自己的指令對應的數據容贝,也就是 v-model='name' 和 {{name}} 有兩個對應的訂閱者自脯,各自管理自己的地方。

4.消息訂閱器Dep: 收集訂閱者斤富,數據變動后會觸發(fā)notify膏潮,調用訂閱者的update方法。
5.mvvm入口函數: 整合以上三者满力。

二戏罢、just do it

1.Observer實現思路:observe對被監(jiān)聽數據對象進行遞歸遍歷屋谭,包括子屬性對象的屬性,都加上 setter 和 getter龟糕。這樣的話,給這個對象的某個值賦值悔耘,就會觸發(fā)setter讲岁,進而監(jiān)聽到數據變化。

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>雙向綁定</title>
</head>

<body>
   <div id="app">
      <input type="text" class="name1" v-model="name">
      <div class="name2">{{name}}</div>
  </div>
</body>
<script> 

/** Vue構造函數
 * @param {*} param
 * */
 function Vue(options) {
  this.data = options.data;
  observe(this.data)
  this.$compile = new Compile(document.querySelector(options.el), this)
}
window.onload = function() {
    var app = new Vue({
      el:'#app',
      data: {
        name: '魔丸'
      }
    })
  }
function observe(data) {
  if(!data || typeof data  !== 'object') {
      return;
  }
  // 遍歷所有屬性
  Object.keys(data).forEach(function(key) {
    defineProp(data, key, data[key]);
  });
};

/** description
 * @data {*} 被修改data對象
 * @key {*} 被修改data對象的屬性
 * @val {*} 被修改data對象的值
 * */
function defineProp(data, key, val) {
  observe(val); // 監(jiān)聽子屬性
  //定義要修改對象的屬性
  Object.defineProperty(data, key, {
      enumerable: true, // 可枚舉
      configurable: false, // 不能再define
      get: function() {
          return val;
      },
      set: function(newVal) {
          console.log('監(jiān)聽到了衬以,新屬性值變化為 ', val, ' --> ', newVal);
          val = newVal;
      }
  });
}

</script>

</html>

2. compile訂閱器實現:接下來我們需要訂閱器去接收訂閱者缓艳。當屬性值變化時執(zhí)行對應訂閱者的更 新函數。顯然訂閱器是個數組容器看峻。

設計思路:

  • Dep類定義在defineProp()函數中:每個屬性對應多個Watcher阶淘,它們需要放在一個訂閱器,當該屬性值變化時互妓,遍歷并執(zhí)行訂閱器中的所有訂閱者的update方法溪窒。
  • 添加訂閱者操作放置在getter里面:讓Watcher初始化時觸發(fā)(需要判斷是否需要添加訂閱者)。
  • 通知watcher更新的操作放在在setter里面:若數據變化冯勉,就會去通知所有訂閱者澈蚌,訂閱者們就會去執(zhí)行對應的更新的函數。
function defineProp(data, key, val) {
  var dep = new Dep();
  observe(val); // 監(jiān)聽子屬性
  //定義要修改對象的屬性
  Object.defineProperty(data, key, {
      enumerable: true, // 可枚舉
      configurable: false, // 不能再define
      get: function() {
          //添加訂閱者watcher到主題對象Dep
          if (Dep.currentWatcher) {
              dep.addWatcher(watcher); 
           }
          return val;
      },
      set: function(newVal) {
          console.log('監(jiān)聽到了灼狰,新屬性值變化為 ', val, ' --> ', newVal);
          val = newVal;
          dep.notify(); // 通知所有訂閱者
      }
  });
}
// 消息訂閱器
function Dep() {
    this.watcherList = [];
}
Dep.prototype = {
    addWatcher: function(watcher) {
        this.watcherList.push(watcher);
    },
    notify: function() {
        this.watcherList.forEach(function(watcher) {
          watcher.update();
        });
    }
};

三. Watcher實現:

設計思路:

1宛瞄、在自身實例化時往屬性訂閱器(dep)里面添加自己。
2交胚、自身必須有一個update()方法:待屬性變動份汗,訂閱器調用notice()通知時,能調用自身的update()方法蝴簇。

/**訂閱者
 * @param {*} vm 指令所屬vue實例
 * @param {*} exp 指令對應的值
 * @param {*} dataItem 指令對應的data中的屬性
 * */
function Watcher(vm, node, dataItem) {
  // 將當前訂閱者指向自己杯活,標記訂閱者是當前watcher實例
  Dep.currentWatcher = this;   
  this.vm = vm; //當前vue實例
  this.node = node;//指令對應的DOM元素
  this.dataItem = dataItem; //指令對應的data中的屬性
  this.value = this.get(); // 此處為了觸發(fā)屬性的getter,從而在dep添加自己
  // 添加完畢军熏,釋放對象轩猩。 Dep.currentWatcher 設為空。因為它是全局變量荡澎,
  // 也是 watcher 與 dep 關聯的唯一橋梁均践,任何時刻都必須保證 Dep.currentWatcher 只有一個值。  
  Dep.currentWatcher = null;   
}
Watcher.prototype = {
  // 屬性值變化收到通知
    update: function() {
      var newValue = this.get(); // 最新值
        var oldVal = this.value;
        if (newValue !== oldVal) {
            this.value = newValue;
            this.node.nodeValue = newValue; //更改節(jié)點內容的關鍵
        }    
    },
    get: function() {
         // 強行觸發(fā)屬性定義的getter方法摩幔,getter方法執(zhí)行的時候彤委,就會在屬性的訂閱器dep添加當前watcher實例,
        var value = this.vm.data[this.dataItem];  
        return value;
    }
};

四.compile

設計思路

  • 為了減少頁面渲染DOM元素的次數或衡,需先將文檔碎片化焦影,等Dom節(jié)點渲染完畢车遂,再將Dom內容插入原來的文檔流中。
  • 需遍歷所有節(jié)點及其子節(jié)點斯辰,掃描解析編譯舶担,調用對應的指令渲染函數進行數據渲染,并調用對應的指令更新函數進行綁定彬呻。
/** 解析器
 * @author liuyun 2020年06月08日 12:43:42'
 * @param {*} el id為app的Element元素
 * @param {*} vm vue實例
 * */
function Compile(el,vm) {
  // 將文檔碎片化
  this.fragment = document.createDocumentFragment();
  let child;
  while (child = el.firstChild) {
    this.fragment.appendChild(child);
  }
  // 遍歷所有節(jié)點及其子節(jié)點衣陶,掃描解析編譯,調用對應的指令渲染函數進行數據渲染闸氮,調用對應的指令更新函數進行綁定
  this.compileElement(this.fragment,vm);
  //處理完所有節(jié)點后剪况,重新把內容添加回去
  el.appendChild(this.fragment);
}

Compile.prototype = {
  compileElement: function(el,vm) {
    let _this = this;
    [].slice.call(el.childNodes).forEach(function(node) {
      var text = node.textContent;
      var reg = /\{\{(.*)\}\}/;    // 表達式文本
      // 如果是元素節(jié)點
      if (node.nodeType == 1) {
        for (let i = 0; i < node.attributes.length; i++) {
          let attr = node.attributes[i];
          if (attr.nodeName == 'v-model') { 
            let dataItemName = attr.nodeValue;
              node.addEventListener('input', function(e) {
              // 如果有v-model屬性,則監(jiān)聽它的input事件
               vm.data[dataItemName] = e.target.value; // 給相應的data屬性賦值蒲跨,進而觸發(fā)該屬性的set方法
              })
              new Watcher(vm, node, dataItemName) //在消息訂閱器中添加一個訂閱者
              node.value = vm.data[dataItemName]; //將data中的值賦予給該node
              node.removeAttribute('v-model')
            }
        }
      } else if (node.nodeType == 3 && reg.test(node.nodeValue)) {
        //若是文本節(jié)點
        var name = RegExp.$1; // 獲取匹配到的字符串
        name = name.trim();
        new Watcher(vm, node, name);
        node.nodeValue = vm.data[name];
      }
        // 遍歷編譯子節(jié)點
        if (node.childNodes && node.childNodes.length) {
          _this.compileElement(node,vm);
        }
     });
  }
}

動圖效果:

getter/setter方法攔截數據的不足

需要vm.$set/Vue.set和vm.items.splice(newLength)解決译断,具體參看官方說明

1.增刪對象時,是監(jiān)控不到的或悲。比如:data={name:"哪吒"},此時若再設置data.alias="魔丸",是監(jiān)控不到的孙咪。因為屬性的getter/setter方法是在observe初始化數據時遍歷已有屬性添加的,后面設置的alias沒有設置getter/setter隆箩,所以檢測不到變化该贾。同樣的,刪除對象屬性時捌臊,getter/setter會跟著屬性一起被刪除掉杨蛋,攔截不到變化。

需要vm.set/Vue.set和vm.delete/Vue.delete這樣的api來解決這個問題

2.getter/setter是針對對象的理澎,像數組的修改(如push(),pop(),shift())導致arr發(fā)生了變化逞力,同樣需要更新視圖,但是arr的getter/setter攔截不到變化(只有在賦值的時候才會調用setter糠爬,比如:arr=[1,2,3])寇荧。

對于這種情況,vue通過改寫Array的默認方法执隧,在調用這些方法的時候發(fā)布更新消息揩抡。一般無需關注。但是對于如下兩種情況:

  • 當你利用索引直接設置一個項時镀琉,例如:vm.items[indexOfItem] = newValue峦嗤。
  • 當你修改數組的長度時,例如:vm.items.length = newLength屋摔。

需要vm.$set/Vue.set和vm.items.splice(newLength)解決烁设,具體參看官方說明

3.每次給數據設置值的時候,都會調用setter函數钓试,這個時候就會發(fā)布屬性更新消息装黑,即使數據的值沒有變副瀑。從性能方便考慮我們肯定希望值沒有變化的時候,不更新模板恋谭。(像Angular這樣把批量操作延時到一次更新,一次做完所有數據變更糠睡,然后整體應用到界面上)
本篇筆記就這么多,我是錢多多箕别,一敲代碼頭就疼铜幽。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市串稀,隨后出現的幾起案子,更是在濱河造成了極大的恐慌狮杨,老刑警劉巖母截,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異橄教,居然都是意外死亡清寇,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門护蝶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來华烟,“玉大人,你說我怎么就攤上這事持灰】梗” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵堤魁,是天一觀的道長喂链。 經常有香客問我,道長妥泉,這世上最難降的妖魔是什么椭微? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮盲链,結果婚禮上蝇率,老公的妹妹穿的比我還像新娘。我一直安慰自己刽沾,他們只是感情好本慕,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著悠轩,像睡著了一般间狂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上火架,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天鉴象,我揣著相機與錄音忙菠,去河邊找鬼。 笑死纺弊,一個胖子當著我的面吹牛牛欢,可吹牛的內容都是我干的。 我是一名探鬼主播淆游,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼傍睹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了犹菱?” 一聲冷哼從身側響起拾稳,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腊脱,沒想到半個月后访得,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡陕凹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年悍抑,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杜耙。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡搜骡,死狀恐怖,靈堂內的尸體忽然破棺而出佑女,到底是詐尸還是另有隱情记靡,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布珊豹,位于F島的核電站簸呈,受9級特大地震影響,放射性物質發(fā)生泄漏店茶。R本人自食惡果不足惜蜕便,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贩幻。 院中可真熱鬧轿腺,春花似錦、人聲如沸丛楚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽趣些。三九已至仿荆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拢操。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工锦亦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人令境。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓杠园,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舔庶。 傳聞我的和親對象是個殘疾皇子抛蚁,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355