文章首發於我的博客html
Object.defineProperty()
實現數據響應Proxy
什麼是代理呢,能夠理解爲在對象以前設置一個「攔截」,當該對象被訪問的時候,都必須通過這層攔截。意味着你能夠在這層攔截中進行各類操做。好比你能夠在這層攔截中對原對象進行處理,返回你想返回的數據結構。前端
ES6 原生提供 Proxy 構造函數,MDN上的解釋爲:Proxy 對象用於定義基本操做的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)。vue
咱們先來看看怎麼使用。react
const p = new Proxy(target, handler); 複製代碼
target
: 所要攔截的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)handler
:一個對象,定義要攔截的行爲const p = new Proxy({}, { get(target, propKey) { return '哈哈,你被我攔截了'; } }); console.log(p.name); // 哈哈,你被我攔截了 複製代碼
注意Proxy是用來操做對象的。代理的目的是爲了拓展對象的能力。git
再看一個例子 咱們能夠實現一個功能:不容許外部修改對象的name屬性。es6
const p = new Proxy({}, { set(target, propKey, value) { if (propKey === 'name') { throw new TypeError('name屬性不容許修改'); } // 不是 name 屬性,直接保存 target[propKey] = value; } }); p.name = 'proxy'; // TypeError: name屬性不容許修改 p.a = 111; console.log(p.a); // 111 複製代碼
babel是用來轉換語法的,像新增的API(好比Array.from, Array.prototype.includes )咱們須要安裝額外的包來進行支持,好比 core-js/stable 和 regenerator-runtime/runtime (PS:babel 7.x 以後@babel/polyfill已不推薦使用),而後還有一些API(String#normalize、Proxy、fetch等)
core-js
中是暫時沒有提供 polyfill,具體的可查看官方文檔 core-js#missing-polyfills。github
Proxy
支持的攔截操做一共 13 種,詳細的能夠查看 MDN。面試
遞歸遍歷data中的數據,使用 Object.defineProperty()劫持 getter和setter,在getter中作數據依賴收集處理,在setter中 監聽數據的變化,並通知訂閱當前數據的地方。 部分源碼 src/core/observer/index.js#L156-L193, 版本爲 2.6.11 以下npm
let childOb = !shallow && observe(val) // 對 data中的數據進行深度遍歷,給對象的每一個屬性添加響應式 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { // 進行依賴收集 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { // 是數組則須要對每個成員都進行依賴收集,若是數組的成員仍是數組,則遞歸。 dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } // 新的值須要從新進行observe,保證數據響應式 childOb = !shallow && observe(newVal) // 將數據變化通知全部的觀察者 dep.notify() } }) 複製代碼
這麼作有什麼問題呢?數組
newProperty
,當前新加的這個屬性並無加入vue檢測數據更新的機制(由於是在初始化以後添加的)。vue.$set
是能讓vue知道你添加了屬性, 它會給你作處理,$set
內部也是經過調用Object.defineProperty()
去處理的以數組爲例說明(PS: 數據的實時響應是指頁面的渲染內容,而不是值vm.items自己的數據):
<ul id="example">
<li v-for="item in items">
{{ item }}
</li>
</ul>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
const vm = new Vue({
el: '#example',
data: {
items: ['a', 'b', 'c']
},
})
// 直接使用下標修改數據不是實時響應
setTimeout(() => {
vm.items[1] = 'x';
vm.items[3] = 'd';
console.log(vm.items);
// 此時打印結果爲 ['a', 'x', 'c', 'd'],但頁面內容沒有更新
}, 500);
// 使用 $set 修改數據是實時響應
setTimeout(() => {
vm.$set(vm.items, 1, 'x1')
vm.$set(vm.items, 3, 'd1')
console.log(vm.items);
// 此時打印結果爲 ['a', 'x1', 'c', 'd1'],頁面內容更新
}, 1000);
複製代碼
能夠點擊直接查看代碼 codepen
vue3.0還未正式發佈,不過vue-next 的相關代碼已經開源出來了,目前處於Alpha版本。
爲何使用 Proxy 能夠解決上面的問題呢?主要是由於Proxy是攔截對象,對對象
進行一個"攔截",外界對該對象的訪問,都必須先經過這層攔截。不管訪問對象的什麼屬性,以前定義的仍是新增的,它都會走到攔截中,
下面分別用Object.defineProperty()
和 Proxy
實現一個簡單的數據響應
使用Object.defineProperty()
實現:
class Observer { constructor(data) { // 遍歷參數data的屬性,給添加到this上 for(let key of Object.keys(data)) { if(typeof data[key] === 'object') { data[key] = new Observer(data[key]); } Object.defineProperty(this, key, { enumerable: true, configurable: true, get() { console.log('你訪問了' + key); return data[key]; // 中括號法能夠用變量做爲屬性名,而點方法不能夠; }, set(newVal) { console.log('你設置了' + key); console.log('新的' + key + '=' + newVal); if(newVal === data[key]) { return; } data[key] = newVal; } }) } } } const obj = { name: 'app', age: '18', a: { b: 1, c: 2, }, } const app = new Observer(obj); app.age = 20; console.log(app.age); app.newPropKey = '新屬性'; console.log(app.newPropKey); 複製代碼
上面代碼的執行結果爲
// 修改 obj原有的屬性 age的輸出 你設置了age 新的age=20 你訪問了age 20 // 設置新屬性的輸出 新屬性 複製代碼
能夠看到,給對象新增一個屬性,內部並無監聽到,新增的屬性須要手動再次使用Object.defineProperty()
進行監聽。 這就是爲何 vue 2.x
中 檢測不到對象屬性的添加和刪除的緣由,內部提供的$set
就是經過調用Object.defineProperty()
去處理的。
下面咱們使用 Proxy
替代 Object.defineProperty()
實現
const obj = { name: 'app', age: '18', a: { b: 1, c: 2, }, } const p = new Proxy(obj, { get(target, propKey, receiver) { console.log('你訪問了' + propKey); return Reflect.get(target, propKey, receiver); }, set(target, propKey, value, receiver) { console.log('你設置了' + propKey); console.log('新的' + propKey + '=' + value); Reflect.set(target, propKey, value, receiver); } }); p.age = '20'; console.log(p.age); p.newPropKey = '新屬性'; console.log(p.newPropKey); p.a.d = '這是obj中a的屬性'; console.log(p.a.d); 複製代碼
能夠看到下面輸出
// 修改原對象的age屬性 你設置了age 新的age=20 你訪問了age 20 // 設置新的屬性 你設置了newPropKey 新的newPropKey=新屬性 你訪問了newPropKey 新屬性 // 給obj的a屬性(是個對象)設置屬性d 你訪問了a 你訪問了a 這是obj中a的屬性 // 備註:若是對象的屬性是對象,須要返回一個新的Proxy // 稍後會補充一下, 你們也能夠先本身考慮一下, 歡迎討論 複製代碼
PS: 補充一個使用 Proxy處理多層級對象的例子:How to create a Deep Proxy?
能夠看到,新增的屬性,並不須要從新添加響應式處理,由於 Proxy
是對對象的操做,只要你訪問對象,就會走到 Proxy
的邏輯中。
Reflect(ES6引入) 是一個內置的對象,它提供攔截 JavaScript 操做的方法。將Object對象一些明顯屬於語言內部方法(好比
Object.defineProperty()
)放到Reflect
對象上。修改某些Object方法的返回結果,讓其變得更合理。讓Object操做都變成函數行爲。具體內容查看MDN
除了即將發佈的 vue 3.0
以外,還有哪些庫使用了Proxy
呢?
都是使用到了對對象進行讀寫攔截,在讀寫中作一些額外的判斷和操做。
Proxy
是用來操做對象的,Object.defineProperty()
是用來操做對象的屬性的。vue2.x
使用 Object.defineProperty()
實現數據的響應式,可是因爲 Object.defineProperty()
是對對象屬性的操做,因此須要對對象進行深度遍歷去對屬性進行操做。vue3.0
用 Proxy
是對對象進行攔截操做,不管是對對象作什麼樣的操做都會走到 Proxy 的處理邏輯中vue3.0
、dobjs/dob
、immer
等庫目前都使用到了 Proxy
,對對象進行讀寫攔截,作一些額外的處理。最近發起了一個100天前端進階計劃,主要是深挖每一個知識點背後的原理,歡迎關注 微信公衆號「牧碼的星星」,咱們一塊兒學習,打卡100天。