Vue響應式原理
利用Object.defineProperty()劫持data數據,裏面每個屬性都定義了get和set方法 監聽屬性的變化,
依賴收集:每個屬性也都有個數組 保存着誰(視圖watcher)依賴了它,當獲取屬性觸發get時,收集了依賴,
依賴更新:當屬性變化觸發set函數時,通知依賴,通知視圖(watcher),視圖(watcher)開始更新。
一、 Object.defineProperty()
Object.defineProperty():在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
get 值是一個函數,當屬性被訪問時,會觸發 get 函數
set 值同樣是一個函數,當屬性被賦值時,會觸發 set 函數
Vue 是怎麼知道數據被調用或改變的?
Vue對data對象中的每個屬性進行了get、set監聽,當屬性被調用時觸發get方法,當屬性被改變時觸發set方法,所以Vue 能知道數據被調用或改變。
代碼如下:
class KVue {
constructor(options) {
this.$options = options;
if(this.$options == undefined) {
return;
}
this.$data = this.$options.data; //把data賦值給this,在創建的實例中才能獲取到data
this.observe(this.$data);
}
// 操作data:如果data是對象,則對屬性進行遍歷
observe(value) {
if(!value || typeof value !== 'object') {
return;
}
Object.keys(value).forEach((key) => {
this.defineReactive(value, key, value[key]);
})
}
// 操作屬性:用Object.defineProperty()監聽屬性
defineReactive(obj, key, val) {
this.observe(val);
Object.defineProperty(obj, key, {
get () {
return val;
},
set (newValue) {
if(val === newValue) {
return;
}
val = newValue;
console.log(val);
}
})
}
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="defineProperty.js"></script>
<script type="text/babel">
let kvue = new KVue({
data: {
name: 'fndjf',
list: {
bar: 'bar'
}
}
})
console.log(kvue)
console.log(kvue.$data.name)
kvue.$data.name = '1dgs';
console.log(kvue.$data.name)
console.log('--------------------');
console.log(kvue.$data.list.bar);
kvue.$data.list.bar = 'dfmd';
console.log(kvue.$data.list);
let kvue1 = new KVue();
</script>
</body>
</html>
二、依賴收集、更新
依賴收集:data中每個屬性都會有一個數組作爲依賴收集器,當頁面使用某個屬性(get)時,頁面的watcher就會被放到對應屬性的依賴收集器中。
依賴更新:通知屬性依賴收集器中的watcher進行更新
代碼如下:
class KVue {
constructor(options) {
this.$options = options;
if(this.$options == undefined) {
return;
}
this.$data = this.$options.data; //把data賦值給this,在創建的實例中才能獲取到data
this.observe(this.$data);
// 模擬watcher創建
new Watcher(); //已將實例賦值給Dep.target
this.$data.name;// 將Dep.target插入name屬性中的dep
new Watcher();//已將實例賦值給Dep.target,覆蓋了之前的Dep.target
this.$data.list.bar; //將Dep.target插入list.bar屬性中的dep
}
// 操作data
observe(value) {
if(!value || typeof value !== 'object') {
return;
}
Object.keys(value).forEach((key) => {
this.defineReactive(value, key, value[key]);
})
}
// 操作key
defineReactive(obj, key, val) {
// 遞歸監聽data屬性中的屬性
this.observe(val);
// 初始化一個屬性中的依賴
const dep = new Dep();
Object.defineProperty(obj, key, {
get () {
// Dep.target是調用該屬性的頁面的Watcher,添加到該屬性的依賴中
Dep.target && dep.addDep(Dep.target);
return val;
},
set (newValue) {
if(val === newValue) {
return;
}
val = newValue;
console.log(val);
// 通知依賴更新
dep.notify();
}
})
}
}
// Dep:依賴
class Dep {
constructor() {
// 存放若干依賴(watcher)
this.$deps = [];
}
// 添加依賴
addDep(dep) {
this.$deps.push(dep);
}
// 通知依賴數組中的每個watcher更新
notify() {
this.$deps.forEach(dep => dep.update(dep));
}
}
// Watcher:觀察者,做具體更新
class Watcher {
constructor() {
// 將當前watcher實例賦值給Dep的靜態屬性,即後一個watcher實例會覆蓋前一個watcher實例
Dep.target = this;
}
update(dep) {
console.log(dep);
console.log('屬性更新了');
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="defineProperty.js"></script>
<script type="text/babel">
let kvue = new KVue({
data: {
name: 'fndjf',
list: {
bar: 'bar'
}
}
})
console.log(kvue)
console.log(kvue.$data.name)
kvue.$data.name = '1dgs';
console.log(kvue.$data.name)
console.log('--------------------');
console.log(kvue.$data.list.bar);
kvue.$data.list.bar = 'dfmd';
console.log(kvue.$data.list);
</script>
</body>
</html>
三、總結
- data中每個屬性都有個數組作爲依賴收集器,收集使用該屬性的頁面的watcher
- Object.defineProperty() - get ,用於 依賴收集
- Object.defineProperty() - set,用於 依賴更新
- watcher 可用於視圖更新
四、另一寫法
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
// 1.實現整體架構(Vue類、Watcher類),用到了訂閱發佈者設計模式
// 2.實現MVVM中的M到V,把數據綁定到視圖
// 3.實現V-M,當文本框輸入文本時,觸發更新模型中的數據,同時更新相應的視圖
class Vue {
constructor(options) {
this.$data = options.data; //獲取數據
this.$el = document.querySelector(options.el); //獲取元素#app
this._directive = {}; //存放訂閱者信息
this.Observer(this.$data);
this.Compile(this.$el);
}
// 劫持數據:
Observer(data) {
for(let key in data) {
this._directive[key] = [];
let val = data[key];
let watcher = this._directive[key];
console.log(watcher);
Object.defineProperty(data, key, {
get() {
return val;
},
set(newVal) {
if(val !== newVal) {
val = newVal;
// 更新視圖
watcher.forEach(element => {
element.update();
});
}
}
})
}
}
// 解析指令:收集依賴、更新視圖、訂閱
Compile(el) {
let node = el.children;
console.log(node);
for(let i = 0; i < node.length; i++) {
// let node = node[i];
if(node[i].children.length) {
this.Compile(node[i]);
}
if(node[i].hasAttribute('v-text')) {
let attrVal = node[i].getAttribute('v-text'); // myText myBox
this._directive[attrVal].push(new Watcher(node[i], this, attrVal, "innerHTML"));
console.log(this._directive);
}
if(node[i].hasAttribute('v-model')) {
let attrVal = node[i].getAttribute('v-model'); // myText myBox
this._directive[attrVal].push(new Watcher(node[i], this, attrVal, "innerHTML"));
// 文本框出現變化時更新模型
node[i].addEventListener('input', ()=>{
this.$data[attrVal] = node[i].value;
})
}
}
}
}
// 訂閱者
class Watcher { //主要功能是更新視圖
constructor(el, vm, exp, attr) {
this.el = el;
this.vm = vm;
this.exp = exp;
this.attr = attr;
this.update();
}
update() {
this.el[this.attr] = this.vm.$data[this.exp];
// div.innerHTML = vm.$data.myText
};
}
</script>
</head>
<body>
<div id="app">
<div>
<div v-text="myText"></div>
<div v-text="myBox"></div>
<input type="text" v-model="myText">
<input type="text" v-model="myBox">
</div>
</div>
<script>
const app = new Vue({
el:'#app',
data: {
myText: 'textaaaaaa',
myBox: 'myBoxaaaaa'
}
})
</script>
</body>
</html>