響應性(Reactivity)是一種容許咱們以聲明式的方式去適應變化的編程範例。html
通俗來講,就是數據變化了,相應的視圖會更新(從新渲染)。前端
ES5 的 Object.defineProperty()
方法支持在一個對象 obj 上定義一個新屬性 prop,或者修改一個對象的現有屬性 prop,並返回此對象。react
Object.defineProperty(obj, prop, descriptor)
git
屬性的 getter 函數,默認爲 undefined。當訪問該屬性時,會調用此函數。執行時不傳入任何參數,可是會傳入 this 對象(因爲繼承關係,這裏的this並不必定是定義該屬性的對象)。該函數的返回值會被用做屬性的值。es6
屬性的 setter 函數,默認爲 undefined。當屬性值被修改時,會調用此函數。該方法接受一個參數(也就是被賦予的新值),會傳入賦值時的 this 對象。github
遍歷數據 data 的全部屬性,經過 Object.defineProperty()
攔截並改寫(自定義)數據的屬性的 getter & setter 函數,從而在訪問對象屬性和設置 / 修改對象屬性的時候可以執行自定義的回調函數:在 getter 中進行依賴收集操做(track,訪問過該屬性的節點、組件、函數……都會被收集爲依賴 watcher),在 setter 中進行視圖更新操做(trigger,通知前面收集到的依賴觸發執行 & 視圖從新渲染)。編程
// 重寫數組的原型方法
let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype);
['push', 'pop', 'unshift', 'shift', 'sort', 'reverse', 'splice'].forEach(
(method) => {
proto[method] = function () {
updateView();
oldArrayPrototype[method].call(this, ...arguments);
};
}
);
// 監聽數據變化
function observer(target) {
if (typeof target !== 'object' || target == null) {
// 不是對象,沒法更改屬性值,直接返回
return target;
}
// 數組,重寫原型方法
if (Array.isArray(target)) {
target.__proto__ = proto;
}
// 循環對象,從新定義屬性的 getter & setter
for (let key in target) {
defineReactive(target, key, target[key]);
}
}
// 定義響應式
function defineReactive(obj, key, val) {
observer(val);
Object.defineProperty(obj, key, {
get() {
// 在這裏進行依賴收集(略)
return val;
},
set(newVal) {
if (newVal !== val) {
observer(newVal);
// 在這裏進行依賴觸發(略)
updateView();
val = newVal;
}
},
});
}
function updateView() {
console.log('視圖更新');
}
複製代碼
// 表示對象的屬性是否能夠被枚舉,如可否經過 for-in 循環返回該屬性。
enumberable: false
// 表示對象的屬性是否能夠被刪除,以及除 value 和 writable 特性(鍵值)外的其餘特性(如 get、set)是否能夠被修改。
configurable: false
// 表示對象的屬性值是否能夠被改變
writable: true
複製代碼
Object.defineProperty()
監測到。Object.defineProperty()
不能監測到數組長度的變化,於是不會觸發視圖更新。但 Vue2 沒有支持,官方回覆是由於性能問題,在性能和用戶體驗之間作了取捨。👇
Object.defineProperty()
須要指定對象具體的屬性名才能對其 getter 和 setter 進行攔截。this.$set
,使新增的屬性也擁有響應式的效果。可是須要判斷到底什麼狀況下須要用 $set,何時能夠直接觸發響應式。Proxy 是一個包含另外一個對象或函數並容許你對其進行攔截的對象。 Proxy - JavaScript | MDN
ES6 的 Proxy 對象用於建立一個對象的代理,從而實現對其基本操做的攔截 & 自定義。
const p = new Proxy(target, handler)
handler.get()
:屬性讀取操做。handler.set()
:屬性設置操做。handler.deleteProperty()
:屬性 delete 操做。handler.has()
:屬性 in 操做符。handler.ownKeys()
:Object.getOwnPropertyNames
方法和 Object.getOwnPropertySymbols
方法的捕捉器。Reflect 對象與 Proxy 對象同樣,也是 ES6 爲了操做對象而提供的新 API。相比 Object 對象主要有以下特色 / 優點:
Object.defineProperty
),放到 Reflect 對象上。現階段,某些方法同時在 Object 和 Reflect 對象上部署,將來的新方法將只部署在 Reflect 對象上。也就是說,從 Reflect 對象上能夠拿到語言內部的方法。Object.defineProperty(obj, name, desc)
在沒法定義屬性時,會拋出一個錯誤;而 Reflect.defineProperty(obj, name, desc)
則會返回 false。name in obj
和 delete obj[name]
,而對應的 Reflect.has(obj, name)
和 Reflect.deleteProperty(obj, name)
都是函數式操做。WeakMap 對象
是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值能夠是任意的。原生的 WeakMap 持有的是每一個鍵對象的「弱引用」,這意味着在沒有其餘引用存在時垃圾回收能正確進行。原生 WeakMap 的結構是特殊且有效的,其用於映射的 key 只有在其沒有被回收時纔是有效的。
WeakMap 鍵名所指向的對象,不計入垃圾回收機制,有助於防止內存泄漏。因此WeakMap 能夠實現往對象上添加數據,又不會干擾垃圾回收機制。
用 Proxy 代理數據,建立響應式對象,攔截其 getter 和 setter 函數;依賴該數據 / 屬性的方法(稱爲反作用 effect)默認會先執行一次,觸發所依賴屬性的 get 方法,在 getter 函數中進行依賴收集(track,把當前屬性與當前的 effect 創建聯繫,即映射表);當屬性變化時,會觸發其 set 方法,在 setter 函數中進行更新(trigger,依次觸發映射表中依賴當前屬性的 effect)。
把數據變爲響應式,遍歷 & 自定義對象全部屬性的 getter & setter 函數,返回 proxy 對象。
// 判斷是不是對象
function isObject(val) {
return typeof val === 'object' && val !== null;
}
// 1.響應式的核心方法
function reactive(target) {
// 建立響應式對象
return createReactiveObject(target);
}
let toProxy = new WeakMap(); //弱引用映射表,es6;放的是 「原對象:代理後的對象」
// 防止被代理過的對象再次被代理
let toRaw = new WeakMap(); // 「代理後的對象:原對象」
// 判斷當前對象有無某屬性
function hasOwn(target, key) {
return target.hasOwnProperty(key);
}
// 建立響應式對象
function createReactiveObject(target) {
if (!isObject(target)) {
// 不是對象,直接返回
return target;
}
let proxy = toProxy.get(target);
if (proxy) {
// 若是 target 已經有相應的代理後的對象,直接返回以前代理過的結果便可
return proxy;
}
if (toRaw.has(target)) {// 判斷 target 是否已是 reactive 對象
// target 已是代理後的對象了,則無需再次代理
return target;
}
const baseHandler = {
// reflect 優勢:不會報錯 & 會有返回值;之後會替代 Object
get(target, key, receiver) {
// target:原對象, key:屬性, receiver:當前的代理對象 proxy(target 被代理後的對象)
console.log('獲取');
let res = Reflect.get(target, key, receiver);
// res 是當前獲取到的值
return isObject(res) ? reactive(res) : res; // 按需實現遞歸
},
set(target, key, value, receiver) {
// 識別是 修改屬性 or 新增屬性
let hadKey = hasOwn(target, key); //判斷這個屬性之前有沒有
let oldValue = target[key];
let res = Reflect.set(target, key, value, receiver);
if (!hadKey) {
console.log('新增屬性');
console.log('設置');
} else if (value !== oldValue) {
// 屏蔽無心義的修改(即修改先後值相同)
console.log('修改屬性');
console.log('設置');
}
return res;
},
deleteProperty(target, key) {
console.log('刪除');
let res = Reflect.deleteProperty(target, key);
return res;
},
};
// 建立觀察者
let observer = new Proxy(target, baseHandler); // es6
toProxy.set(target, observer);
toRaw.set(observer, target);
return observer;
}
複製代碼
// reactive 中函數 createReactiveObject 的 baseHandler 修改以下
const baseHandler = {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
// 收集依賴(把屬性 & 對應的 effect 創建聯繫),即 訂閱【把當前的 key 與 effect 對應起來】
track(target, key); // 若是目標上的 key 變化了,從新讓數組中的 effect 執行便可
return isObject(res) ? reactive(res) : res; // 按需實現遞歸
},
set(target, key, value, receiver) {
let hadKey = hasOwn(target, key);
let oldValue = target[key];
let res = Reflect.set(target, key, value, receiver);
if (!hadKey) {
trigger(target, 'add', key);
} else if (value !== oldValue) {
trigger(target, 'edit', key);
}
return res;
},
deleteProperty(target, key) {
console.log('刪除');
let res = Reflect.deleteProperty(target, key);
return res;
},
};
複製代碼
// 2.依賴收集(發佈訂閱)
// 取值會觸發 get,get 觸發 track(track 裏存映射表,最外層是個 WeakMap);設置值時觸發 set,set 觸發 trigger,取出 effect 執行,更新視圖
// 棧:先進後出
let activeEffectStacks = []; // 保存 reactiveEffect
// 依賴的數據結構應該以下
// {
// target: {
// key: [fn, fn, fn,...] // 一個屬性可能對應多個反作用(即有多個 effect 都依賴這個屬性)【應去重,因此用 Set 數據結構】
// }
// }
let targetSMap = new WeakMap(); // 集合 和 hash 表
function track(target, key) {
//若這個 target 中的 key 變化了,就執行棧中的方法
let effect = activeEffectStacks[activeEffectStacks.length - 1];
if (effect) {
// 有對應關係,才建立關聯【如下爲動態建立依賴關係】
let depsMap = targetSMap.get(target);
if (!depsMap) {
// 首次沒有,設置一個並設默認值
targetSMap.set(target, (depsMap = new Map()));
}
// 取對象的 key 對應的反作用數組
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
if (!deps.has(effect)) {
deps.add(effect);
}
}
}
function trigger(target, type, key) {
let depsMap = targetSMap.get(target);
if (depsMap) {
// 有才須要觸發
let deps = depsMap.get(key);
if (deps) {
// 將當前 key 對應的 effect 依次執行
deps.forEach((effect) => effect());
}
}
}
// 響應式——反作用
function effect(fn) {
// 須要把 fn 這個函數 變成 響應式的函數
let reactiveEffect = createReactiveEffect(fn);
// 反作用 默認會先執行一次
reactiveEffect();
}
function createReactiveEffect(fn) {
let reactiveEffect = function () {
// 建立的響應式的 effect
return run(reactiveEffect, fn); // 2個目的:一、執行 fn;二、把這個 reactiveEffect 存到棧中
};
return reactiveEffect;
}
// 運行 fn & 把 effect 存起來
function run(effect, fn) {
try {
activeEffectStacks.push(effect);
fn(); // 和 vue2 同樣,利用 js 的單線程
} finally {
// 即便前面報錯,這裏也會執行
activeEffectStacks.pop();
}
}
複製代碼
// 若是傳入 ref 的是一個對象,將調用 reactive 方法進行深層響應轉換。
const convert = (raw) => (isObject(raw) ? reactive(raw) : raw);
function ref(raw) {
raw = convert(raw);
const v = {
_isRef: true,
get value() {
track(v, '');
return raw;
},
set value(newValue) {
raw = convert(newValue);
trigger(v, '');
},
};
return v;
}
複製代碼
function effect(fn, options = {}) {
const effect = createReactiveEffect(fn, options);
if (!options.lazy) {
effect();
}
return effect;
}
function createReactiveEffect(fn, options) {
const effect = function () {
return run(effect, fn);
};
effect.scheduler = options.scheduler;
return effect;
}
function computed(getterOrOptions) {
const getter = isFunction(getterOrOptions)
? getterOrOptions
: getterOrOptions.get;
const setter = isFunction(getterOrOptions) ? () => {} : getterOrOptions.set;
let value;
let dirty = true;
let v;
const runner = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true;
trigger(v, '');
},
});
v = {
_isRef: true,
get value() {
if (dirty) {
value = runner();
dirty = false;
}
track(v, '');
return value;
},
set value(newValue) {
setter(newValue);
},
};
return v;
}
複製代碼
API | 特性 | 適用場景 |
---|---|---|
reactive | - 接收一個普通對象而後返回該普通對象的響應式代理。 - 響應式轉換是「深層的」:會影響對象內部全部嵌套的屬性。返回的代理對象 不等於 原始對象。建議僅使用代理對象而避免依賴原始對象。 |
只能用於代理非基本數據類型 object。 |
toRefs | 能夠將一個響應型對象(reactive object) 轉化爲普通對象(plain object),同時又把該對象中的每個屬性轉化成對應的響應式屬性(ref)。 | 保留被解構的響應式對象(reactive object)的響應式特性(reactivity)【響應式對象被解構後會丟失響應性】,e.g. ...toRefs(data) |
ref | - 接受一個參數值並返回一個響應式且可改變的 ref 對象。ref 對象擁有一個指向內部值的單一 property.value。 - 若是傳入 ref 的是一個對象,將調用 reactive 方法進行深層響應轉換。 - 使用 ref api 時,數據變成了對象,值就是 value 屬性的值,若是數據自己就是對象,依然會多一層 value 結構,而 reactive 沒有這些反作用。 |
- 通常用於給 js 基本數據類型添加響應性(也支持非基本類型的 object) - 基本數據類型共 7 個,只能使用 ref:String,Number,BigInt,Boolean,Symbol,Null,Undefined |
watch | - 監聽特定的 data 源,並在單獨的回調函數中定義反作用。默認狀況下,它也是惰性的——即,回調僅在監聽源發生更改時調用。 - options: -- immediate:表示是否在第一次渲染的時候執行這個函數。 -- deep:若是咱們監聽一個對象,是否要看這個對象裏面屬性的變化。 |
- 監聽:若是某一 / 多個屬性變化,就去執行回調函數。 - 惰性地執行反作用。 - 更具體地說明應觸發偵聽器從新運行的狀態;在數據變化的回調中執行異步操做或者開銷很大的時候使用。 - 訪問偵聽狀態的先前值和當前值。 |
computed | - 使用 getter 函數,併爲從 getter 返回的值返回一個不變的響應式 ref 對象,不能直接對 computed 返回值的 value 屬性賦值。 - 也可使用具備 get 和 set 函數的對象來建立可寫的 ref 對象。 |
- 計算屬性,用於須要監聽一 / 多個值而且生成一個新的屬性時。 - 會根據依賴自動緩存,若是依賴不變,這個值就不會從新計算。 |
兼容性,IE 11 及如下版本不兼容 ES6 的 Proxy
- More Detail