vue2.0與3.0響應式原理機制
vue2.0響應式原理 – defineProperty
這個原理老生常談了,就是攔截對象
,給對象的屬性增加set
和 get
方法,因為核心是defineProperty
所以還需要對數組的方法進行攔截
一、變化追蹤
- 把一個普通 JavaScript 對象傳給 Vue 實例的
data
選項,Vue 將遍歷此對象所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。 - Object.defineProperty 是僅 ES5 支援,且無法 shim 的特性,這也就是為什麼 Vue 不支援 IE8 以及更低版本瀏覽器的原因。
- 用戶看不到 getter/setter,但是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。
- 每個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程中把屬性記錄為依賴,之後當依賴項的
setter
被調用時,會通知watcher
重新計算,從而致使它關聯的組件得以更新。
原理:在初次渲染的過程中就會調用對象屬性的getter函數,然後getter函數通知wather對象將之聲明為依賴,依賴之後,如果對象屬性發生了變化,那麼就會調用settter函數來通知watcher,watcher就會在重新渲染組件,以此來完成更新。
二、變化檢測問題
Vue 不能檢測到對象屬性的添加或刪除。由於 Vue 會在初始化實例時對屬性執行 getter/setter
轉化過程,所以屬性必須在 data
對象上存在才能讓 Vue 轉換它,這樣才能讓它是響應的。
var vm = new Vue({ el: '#app', data:{ a:1, k: {} } }) // `vm.a` 是響應的
vm.b = 2 // `vm.b` 是非響應的
對於已經創建的實例,Vue 不允許動態添加根級別的響應式 property。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套對象添加響應式 property。
Vue.set(vm.someObject, 'b', 2)
//您還可以使用 vm.$set 實例方法,這也是全局 Vue.set 方法的別名:
this.$set(this.someObject,'b',2)
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
Vue 不能檢測以下數組的變動:
- 當你利用索引直接設置一個數組項時,例如:
vm.items[indexOfItem] = newValue
- 當你修改數組的長度時,例如:
vm.items.length = newLength
// Vue.set Vue.set(vm.items, indexOfItem, newValue) // Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue) vm.$set(vm.items, indexOfItem, newValue) vm.items.splice(newLength)
三、聲明響應式屬性
由於 Vue 不允許動態添加根級響應式屬性,所以你必須在初始化實例前聲明根級響應式屬性,可以為一個空值
如果你在 data 選項中未聲明 message
,Vue 將警告你渲染函數在試圖訪問的屬性不存在。
var vm = new Vue({ data: { // 聲明 message 為一個空值字元串 message: '' }, template: '<div>{{ message }}</div>' }) // 之後設置 `message` vm.message = 'Hello!'
四、非同步更新隊列
Vue 在更新 DOM 時是非同步執行的。
只要觀察到數據變化,Vue 將開啟一個隊列,並緩衝在同一事件循環中發生的所有數據改變。如果同一個 watcher 被多次觸發,只會一次推入到隊列中。
Vue 在內部嘗試對非同步隊列使用原生的 Promise.then
和 MutationObserver
,如果執行環境不支援,會採用 setTimeout(fn, 0)
代替。
五、攔截
Object.defineProperty缺點
- 無法監聽數組的變化
- 需要深度遍歷,浪費記憶體
對對象進行攔截
function observer(target){ // 如果不是對象數據類型直接返回即可 if(typeof target !== 'object'){ return target } // 重新定義key for(let key in target){ defineReactive(target,key,target[key]) } } //更新 function update(){ console.log('update view') } function defineReactive(obj,key,value){ // 校驗----對象嵌套對象,遞歸劫持 observer(value); Object.defineProperty(obj,key,{ get(){ // 在get 方法中收集依賴 return value }, set(newVal){ if(newVal !== value){ observer(value); update(); // 在set方法中觸發更新 } } }) } let obj = {name:'youxuan'} observer(obj); obj.name = 'webyouxuan';
數組方法劫持
let oldProtoMehtods = Array.prototype; let proto = Object.create(oldProtoMehtods); ['push','pop','shift','unshift'].forEach(method=>{ Object.defineProperty(proto,method,{ get(){ update(); oldProtoMehtods[method].call(this,...arguments) } }) })
function observer(target){ if(typeof target !== 'object'){ return target } // 如果不是對象數據類型直接返回即可 if(Array.isArray(target)){ Object.setPrototypeOf(target,proto); // 給數組中的每一項進行observr for(let i = 0 ; i < target.length;i++){ observer(target[i]) } return }; // 重新定義key for(let key in target){ defineReactive(target,key,target[key]) } }
Vue3.0數據響應機制 – Proxy
首先熟練一下ES6中的 Proxy、Reflect 及 ES6中為我們提供的 Map、Set兩種數據結構。
Proxy
用於創建一個對象的代理,從而實現基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數調用等)。
語法:const p = new Proxy(target, handler)
參數
target:
要使用Proxy
包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。handler:
一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理p
的行為。
const handler = { get: function(obj, prop) { return prop in obj ? obj[prop] : 37; } }; const p = new Proxy({}, handler); p.a = 1; p.b = undefined; console.log(p.a, p.b); // 1, undefined console.log('c' in p, p.c); // false, 37
Reflect
是一個內置的對象,它提供攔截 JavaScript 操作的方法
1、Reflect.deleteProperty(target, propertyKey)
作為函數的delete
操作符,相當於執行 delete target[name]
。
2、Reflect.set(target, propertyKey, value[, receiver])
將值分配給屬性的函數。返回一個Boolean
,如果更新成功,則返回true
。
3、Reflect.get(target, propertyKey[, receiver])
獲取對象身上某個屬性的值,類似於 target[name]。
先應用再說原理:
let p = Vue.reactive({name:'youxuan'}); Vue.effect(()=>{ // effect方法會立即被觸發 console.log(p.name); }) p.name = 'webyouxuan';; // 修改屬性後會再次觸發effect方法
一、reactive方法實現
通過proxy 自定義獲取、增加、刪除等行為
1) 對象操作
// 1、聲明響應式對象 function reactive(target) { return createReactiveObject(target); } // 是否是對象類型 function isObject(target) { return typeof target === 'object' && target !== null; } // 2、創建 function createReactiveObject(target) { // 判斷target是不是對象,不是對象不必繼續 if (!isObject(target)) { return target; } const handlers = { get(target, key, receiver) { // 取值 console.log('獲取') let res = Reflect.get(target, key, receiver); return res; }, set(target, key, value, receiver) { // 更改 、 新增屬性 console.log('設置') let result = Reflect.set(target, key, value, receiver); return result; }, deleteProperty(target, key) { // 刪除屬性 console.log('刪除') const result = Reflect.deleteProperty(target, key); return result; } } // 開始代理 observed = new Proxy(target, handlers); return observed; } let p = reactive({ name: 'youxuan' }); console.log(p.name); // 獲取 p.name = 'webyouxuan'; // 設置 delete p.name; // 刪除
深層代理
由於我們只代理了第一層對象,所以對age
對象進行更改是不會觸發set方法的,但是卻觸發了get
方法,這是由於 p.age
會造成 get
操作
let p = reactive({ name: "123", age: { num: 10 } }); p.age.num = 11
get改進方案
這裡我們將p.age
取到的對象再次進行代理,這樣在去更改值即可觸發set
方法
get(target, key, receiver) { // 取值 console.log("獲取"); let res = Reflect.get(target, key, receiver);
// 懶代理,只有當取值時再次做代理
return isObject(res)? reactive(res) : res; }
2)數組操作
Proxy
默認可以支援數組,包括數組的長度變化以及索引值的變化
let p = reactive([1,2,3,4]); p.push(5);
會觸發兩次set
方法,第一次更新的是數組中的第4
項,第二次更新的是數組的length
set(target, key, value, receiver) { // 更改、新增屬性 let oldValue = target[key]; // 獲取上次的值 let hadKey = hasOwn(target,key); // 看這個屬性是否存在 let result = Reflect.set(target, key, value, receiver); if(!hadKey){ // 新增屬性 console.log('更新 添加') }else if(oldValue !== value){ // 修改存在的屬性 console.log('更新 修改') } // 當調用push 方法第一次修改時數組長度已經發生變化 // 如果這次的值和上次的值一樣則不觸發更新 return result; }
解決重複使用reactive情況
// 情況1.多次代理同一個對象 let arr = [1,2,3,4]; let p = reactive(arr); reactive(arr); // 情況2.將代理後的結果繼續代理 let p = reactive([1,2,3,4]); reactive(p);
通過hash表
的方式來解決重複代理的情況
const toProxy = new WeakMap(); // 存放被代理過的對象 const toRaw = new WeakMap(); // 存放已經代理過的對象 function reactive(target) { // 創建響應式對象 return createReactiveObject(target); } function isObject(target) { return typeof target === "object" && target !== null; } function hasOwn(target,key){ return target.hasOwnProperty(key); } function createReactiveObject(target) { if (!isObject(target)) { return target; } let observed = toProxy.get(target); if(observed){ // 判斷是否被代理過 return observed; } if(toRaw.has(target)){ // 判斷是否要重複代理 return target; } const handlers = { get(target, key, receiver) { // 取值 console.log("獲取"); let res = Reflect.get(target, key, receiver); return isObject(res) ? reactive(res) : res; }, set(target, key, value, receiver) { let oldValue = target[key]; let hadKey = hasOwn(target,key); let result = Reflect.set(target, key, value, receiver); if(!hadKey){ console.log('更新 添加') }else if(oldValue !== value){ console.log('更新 修改') } return result; }, deleteProperty(target, key) { console.log("刪除"); const result = Reflect.deleteProperty(target, key); return result; } }; // 開始代理 observed = new Proxy(target, handlers); toProxy.set(target,observed); toRaw.set(observed,target); // 做映射表 return observed; }
二、effect實現
effect意思是副作用,此方法默認會先執行一次。如果數據變化後會再次觸發此回調函數。
let user= {name:'大鵬'} let p = reactive(user); effect(()=>{ console.log(p.name); // 大鵬 })
實現方法
function effect(fn) { const effect = createReactiveEffect(fn); // 創建響應式的effect effect(); // 先執行一次 return effect; } const activeReactiveEffectStack = []; // 存放響應式effect function createReactiveEffect(fn) { const effect = function() { // 響應式的effect return run(effect, fn); }; return effect; } function run(effect, fn) { try { activeReactiveEffectStack.push(effect); return fn(); // 先讓fn執行,執行時會觸發get方法,可以將effect存入對應的key屬性 } finally { activeReactiveEffectStack.pop(effect); } }
當調用fn()
時可能會觸發get
方法,此時會觸發track
const targetMap = new WeakMap(); function track(target,type,key){ // 查看是否有effect const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1]; if(effect){ let depsMap = targetMap.get(target); if(!depsMap){ // 不存在map targetMap.set(target,depsMap = new Map()); } let dep = depsMap.get(target); if(!dep){ // 不存在set depsMap.set(key,(dep = new Set())); } if(!dep.has(effect)){ dep.add(effect); // 將effect添加到依賴中 } } }
當更新屬性時會觸發trigger
執行,找到對應的存儲集合拿出effect
依次執行、
function trigger(target,type,key){ const depsMap = targetMap.get(target); if(!depsMap){ return } let effects = depsMap.get(key); if(effects){ effects.forEach(effect=>{ effect(); }) } }
我們發現如下問題
新增了值,effect
方法並未重新執行,因為push
中修改length
已經被我們屏蔽掉了觸發trigger
方法,所以當新增項時應該手動觸發length
屬性所對應的依賴。
let school = [1,2,3]; let p = reactive(school); effect(()=>{ console.log(p.length); }) p.push(100);
解決
function trigger(target, type, key) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let effects = depsMap.get(key); if (effects) { effects.forEach(effect => { effect(); }); } // 處理如果當前類型是增加屬性,如果用到數組的length的effect應該也會被執行 if (type === "add") { let effects = depsMap.get("length"); if (effects) { effects.forEach(effect => { effect(); }); } } }
三、ref實現
ref可以將原始數據類型也轉換成響應式數據,需要通過.value
屬性進行獲取值
function convert(val) { return isObject(val) ? reactive(val) : val; } function ref(raw) { raw = convert(raw); const v = { _isRef:true, // 標識是ref類型 get value() { track(v, "get", ""); return raw; }, set value(newVal) { raw = newVal; trigger(v,'set',''); } }; return v; }
問題又來了我們再編寫個案例
這樣做的話豈不是每次都要多來一個.value
,這樣太難用了
let r = ref(1); let c = reactive({ a:r }); console.log(c.a.value);
解決
在get
方法中判斷如果獲取的是ref
的值,就將此值的value
直接返回即可
let res = Reflect.get(target, key, receiver); if(res._isRef){ return res.value }
四、computed實現
computed
實現也是基於 effect
來實現的,特點是computed
中的函數不會立即執行,多次取值是有快取機制的
let a = reactive({name:'youxuan'}); let c = computed(()=>{ console.log('執行次數') return a.name +'webyouxuan'; }) // 不取不執行,取n次只執行一次 console.log(c.value); console.log(c.value); function computed(getter){ let dirty = true; const runner = effect(getter,{ // 標識這個effect是懶執行 lazy:true, // 懶執行 scheduler:()=>{ // 當依賴的屬性變化了,調用此方法,而不是重新執行effect dirty = true; } }); let value; return { _isRef:true, get value(){ if(dirty){ value = runner(); // 執行runner會繼續收集依賴 dirty = false; } return value; } } }
修改effect
方法
function effect(fn,options) { let effect = createReactiveEffect(fn,options); if(!options.lazy){ // 如果是lazy 則不立即執行 effect(); } return effect; } function createReactiveEffect(fn,options) { const effect = function() { return run(effect, fn); }; effect.scheduler = options.scheduler; return effect; }
在trigger
時判斷
deps.forEach(effect => { if(effect.scheduler){ // 如果有scheduler 說明不需要執行effect effect.scheduler(); // 將dirty設置為true,下次獲取值時重新執行runner方法 }else{ effect(); // 否則就是effect 正常執行即可 } }); let a = reactive({name:'youxuan'}); let c = computed(()=>{ console.log('執行次數') return a.name +'webyouxuan'; }) // 不取不執行,取n次只執行一次 console.log(c.value); a.name = 'zf10'; // 更改值 不會觸發重新計算,但是會將dirty變成true console.log(c.value); // 重新調用計算方法