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); // 重新呼叫計算方法