# Vue如何實現響應式(Reactivity)
## 前言
前端框架Vue的基本思路和Angular、React並無二致,其核心就在於: 當數據變化時,自動去刷新頁面DOM,這使得我們能從繁瑣的DOM操作中解放出來,從而專心地去處理業務邏輯。
## 何謂響應式
當數據變化後會自動更新某個函示
映射到組件的實現就是,當數據變化後,會自動觸發組件重新渲染
響應式式vue.js組件化更新渲染的一個核心機制
#### 命令式編程實現響應式
```
let price = 5
let quantity = 2
let total = price * qunatity
console.log(total) // 10
quantity = 3
total = price * qunatity
console.log(total) // 15
```
很顯然這樣很笨,每次改quantity都要重新執行 total = price * qunatity
#### 用函數實現響應式
```
let product = {
price: 5,
quantity: 2
}
let total = 0
let dep = new Set() // to store our effects
let effect = () => { total = product.price * product.quantity }
function track(){ dep.add(effect) }
function trigger() { dep.forEach(effect => effect()) }
track() // 收集響應
effect() // 執行響應,否則一開始total還是0
console.log(total) // 10
product.quantity = 3
trigger() // 重新執行effect計算total
console.log(total) // 15
```
雖然每次修改quantity還是要執行一次trigger()才會重算total
不過至少接下來只要思考如何解決如何自動執行trigger這件事就好
假如若想要改變quantity就重新觸發trigger,限制一定要呼叫這個setState,就可以達到改值自動響應
```
let product = {
price: 5,
quantity: 2
}
let total = 0
let dep = new Set() // to store our effects
let effect = () => { total = product.price * product.quantity }
function track(){ dep.add(effect) }
function trigger() { dep.forEach(effect => effect()) }
track();
effect();
console.log(total) // 10
function setState(param) {
product = {
...product,
...param,
};
trigger();
}
setState({ quantity: 3 });
console.log(total); // 15
```
有沒有一種react的既視感,其實react就是差不多是這種概念來實現數據響應式
但vue是怎麼做到改變值(eg: product.quantity = 3 then trigger)就可以做到響應式的呢?
### vue 2 使用defineProprty 實現數據劫持
[MDN defineProperty文件](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
靜態方法 Object.defineProperty() 會直接對一個物件定義、或是修改現有的屬性。執行後會回傳定義完的物件。
```
var aValue = 1
var o = {a: aValue};
Object.defineProperty(o, "a", {
get: function () {
console.log('trigger o.a getter!')
return aValue;
},
set(value){
aValue = value
console.log('set o.a to '+ value)
}
});
o.a = 2 // "set o.a to 2"
o.b = 3 // 未對o.b definProperty實作setter,不會trigger setter
console.log(o.a) // "trigger o.a getter!"
console.log(o.b) // 未對o.b definProperty實作setter,不會trigger getter
```
缺點:
1. 無法對新增/刪除的屬性作監聽(vue 2 會為此提供api: $set/ $delete)
2. 初始化階段遞迴執行Object.defineProperty帶來的性能負擔
[code sanbox](https://jsfiddle.net/yamapi0103/5byrzfan/35/)
範例: 對nested Object/array 實作getter/setter, vue 源碼也是類似這樣去做攔截
```
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
const isArray = Array.isArray;
function defineReactive(data, key, value) {
observe(value); // 递归观察嵌套对象和数组
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
console.log(`get key: ${key} value: ${value}`);
return value;
},
set: function defineSet(newVal) {
console.log(`set key: ${key} value: ${newVal}`);
value = newVal;
},
});
}
class Observer {
constructor(data) {
if (isArray(data)) {
for (let i = 0; i < data.length; i++) {
defineReactive(data, i, data[i]);
}
} else if (isPlainObject(data)) {
this.walk(data);
}
}
walk(data) {
// 循環對象 對屬性依次劫持
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key]);
});
}
}
function observe(data) {
if (typeof data !== 'object' || data == null) return; // 只對對象劫持
return new Observer(data);
}
let nestedObj = {
a: {
b: 2,
},
c: [],
};
observe(nestedObj);
nestedObj.a.b; // "get key: b value: 2"
nestedObj.c.push({ d: 1 });
nestedObj.c[0].d; // getter not work
```
由於Object.defineProperty劫持的是對象的屬性,所以新增屬性時,需要重新遍歷對象,對其新增屬性再使用Object.defineProperty進行劫持。(要再執行一次observe(data))
所以,Object.defineProperty 只能監控現有變化的能力,所以vue2.x放棄了這個特性。
(使用vue為data中的數據新增屬性時,需要使用vm.$set才能確保新增的屬性也是響應式的。)
#### [補充]那 vue 2 如何用defineProperty 在新增數據時實現數據劫持?
定義在core/observer/array.js中, 下面是這部分源碼的分析。
vue 改寫了原生Array.prototype的方法(methodsToPatch 這7種),所以要用這幾種方法操作陣列才能實現響應式
[官網有提到](https://v2.vuejs.org/v2/guide/list#Mutation-Methods
):

arrayMethods 是對數組的方法進行重寫(包裝)
[code sandbox](https://jsfiddle.net/yamapi0103/e0j4vqh5/29/)
```
let oldArrayProto = Array.prototype;
// newArrayProto.__proto__ = oldArrayProto
let newArrayProto = Object.create(oldArrayProto); // 添加自己的數組方法,同時保留原有的方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']; // concat/ slice 不會改變原術組
methods.forEach((method) => {
newArrayProto[method] = function (...args) {
const result = oldArrayProto[method].apply(this, args); // 調用原來的方法
// 對數組新增的數據再次進行劫持
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
default:
break;
}
if (inserted) {
// 對新增內容再次進行觀測
ob.observeArray(inserted);
}
return result;
};
});
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
const isArray = Array.isArray;
function defineReactive(data, key, value) {
/*
if (isPlainObject(value) || isArray(value)) {
observe(value); // 递归观察嵌套对象和数组
} */
observe(value); // 递归观察嵌套对象和数组
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
console.log(`get key: ${key} value: ${value}`);
return value;
},
set: function defineSet(newVal) {
console.log(`set key: ${key} value: ${newVal}`);
value = newVal;
},
});
}
class Observer {
constructor(data) {
// 如果數據上有__ob__,說明這個屬性被觀測過了
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false,
});
if (isArray(data)) {
/* for (let i = 0; i < data.length; i++) {
defineReactive(data, i, data[i])
} */
// 重寫數組7個mutable的方法
data.__proto__ = newArrayProto;
this.observeArray(data);
} else {
this.walk(data);
}
}
walk(data) {
// 循環對象 對屬性依次劫持
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key]);
});
}
observeArray(data) {
data.forEach((item) => observe(item));
}
}
function observe(data) {
if (typeof data !== 'object' || data == null) return; // 只對對象劫持
if (data.__ob__ instanceof Observer) {
// 說明這個對象已被代理過了
return data.__ob__;
}
return new Observer(data);
}
let nestedArr = [1, 2, [2, 3], { d: 333 }];
let nestedObj = {
a: [],
};
observe(nestedObj);
nestedObj.a.push({ b: 'bbb' });
// 新增的屬性後續皆有被觸發
nestedObj.a[0].b; // "get key: a value: bbb"
nestedObj.a[0].b = 'ababab'; // "set key: b value: ababab"
```
vue 2 中偵測陣列變化並沒有使用defineProperty,因為修改索引情況不多,且會浪費性能,採用重寫陣列方法的變異方法實現
initData -> observe -> 對我們傳入的陣列進行原型鏈修改,後續調用的方法都是重寫後的方法 -> 對陣列中每個對象也再次進行代理
- arr[1] = 100 (X) // 不會進行畫面渲染
- arr[0].a =100 (O) // 陣列內部若還是物件,會進行畫面渲染,因為陣列中的對象會被observe
### [補充]vue 3 使用 Proxy 實現數據劫持
[MDN Proxy說明文件](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
1. Proxy 可以直接監聽整個物件
優點: 可以檢測到對象的任何修改,彌補defineProperty的不足
```
// 創建一个空的陣列
const myArray = [];
// 創建代理對象來監聽陣列
const arrayProxy = new Proxy(myArray, {
get(target, property, receiver) {
// 返回陣列原生方法
return target[property]
},
set(target, property, value, receiver) {
// 在這裡log 陣列變化
console.log(`Array property '${property}' was set to: ${value}`)
target[property] = value
return true;
}
});
// 添加元素到陣列
arrayProxy.push(1); // "Array property '0' was set to: 1
arrayProxy.push(2); // "Array property '1' was set to: 2"
// 輸出陣列
console.log(arrayProxy); // [1, 2]
```
Proxy則是直接代理物件,是可以攔截到物件的新增屬性的。
### 用觀察者模式實現
其核心思路是對傳入的數據,深度遍歷每個屬性並使用defineProperty實作get和set方法,使其具有響應式
做法:
- 調用getter時進行依賴收集(訂閱) =>dep.depend()
- 調用setter 時通知依賴更新(發布) => dep.notify()
每次頁面渲染就會重新收集

[源碼解析影片](https://www.bilibili.com/video/BV1P8411M7dM?p=8&vd_source=b7b95c88e0694fa186b56e535ce87b9d)
target可想成一個組件上的data的每個屬性都有各自的trigger函式,只要屬性變動會去執行對應的effect

class Dep 收集依賴
[code sandbox](https://jsfiddle.net/yamapi0103/0n7cg2s6/18/)
```
class Dep {
constructor(value) {
this.subs = new Set();
}
depend() {
if (activeEffect) {
console.log('before add subs');
this.subs.add(activeEffect);
}
}
notify() {
this.subs.forEach((effect) => effect());
}
}
var activeEffct = null;
function watchEffect(effect) {
activeEffect = effect;
effect();
activeEffect = null;
}
function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
const isArray = Array.isArray;
function defineReactive(data, key, value) {
observe(value); // 递归观察嵌套对象和数组
const dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
dep.depend();
console.log(`get key: ${key} value: ${value}`);
return value;
},
set: function defineSet(newVal) {
dep.notify();
console.log(`set key: ${key} value: ${newVal}`);
value = newVal;
},
});
}
class Observer {
constructor(data) {
if (isArray(data)) {
for (let i = 0; i < data.length; i++) {
defineReactive(data, i, data[i]);
}
} else if (isPlainObject(data)) {
this.walk(data);
}
}
walk(data) {
// 循環對象 對屬性依次劫持
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key]);
});
}
}
function observe(data) {
if (typeof data !== 'object' || data == null) return; // 只對對象劫持
return new Observer(data);
}
let nestedObj = {
a: 1,
c: [],
};
observe(nestedObj);
watchEffect(() => {
console.log(nestedObj.a);
console.log('數據變了 我要去更新畫面了!');
});
nestedObj.a = 2; // 執行watchEffect()
```
[code sandbox](https://codesandbox.io/s/vue-reactivity-test-cxz63x?file=/src/App.vue)
參考來源
- https://vue-course-doc.vercel.app/#_1-%E8%AF%BE%E7%A8%8B%E7%AE%80%E4%BB%8B
- https://front-chef.coderbridge.io/2021/02/27/vue2-vue3-reactivity/
- [defineProperty vs Proxy](https://juejin.cn/post/6844903965180575751)
- [解析vue源碼 上](https://juejin.cn/post/7105337345649901605#1)
- [解析vue源碼 下](https://juejin.cn/post/7104871637710798885)
https://www.gushiciku.cn/pl/g8v2/zh-tw