敲黑板劃重點令花,這是考點。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.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這樣把批量操作延時到一次更新,一次做完所有數據變更糠睡,然后整體應用到界面上)
本篇筆記就這么多,我是錢多多箕别,一敲代碼頭就疼铜幽。