和尤雨溪一塊兒進階vuejavascript
如今來寫一個簡單的3.0的版本吧vue
你們都知道,2.0的響應式用的是Object.defineProperty
,結合發佈訂閱模式實現的,3.0已經用Proxy
改寫了java
Proxy是es6提供的新語法,Proxy 對象用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。react
語法:es6
const p = new Proxy(target, handler)
數組
target 要使用 Proxy 包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。
handler 一個一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理 p 的行爲。瀏覽器
handler的方法有不少, 感興趣的能夠移步到MDN,這裏重點介紹下面幾個bash
handler.has()
in 操做符的捕捉器。 handler.get() 屬性讀取操做的捕捉器。 handler.set() 屬性設置操做的捕捉器。 handler.deleteProperty() delete 操做符的捕捉器。 handler.ownKeys() Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 複製代碼
基於上面的知識,咱們來攔截一個對象屬性的取值,賦值和刪除網絡
// version1 const handler = { get(target, key, receiver) { console.log('get', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set', key, value) let res = Reflect.set(target, key, value, receiver) return res }, deleteProperty(target, key) { console.log('deleteProperty', key) Reflect.deleteProperty(target, key) } } // 測試部分 let obj = { name: 'hello', info: { age: 20 } } const proxy = new Proxy(obj, handler) // get name hello // hello console.log(proxy.name) // set name world proxy.name = 'world' // deleteProperty name delete proxy.name 我是08年出道的前端老鳥,想交流經驗能夠進個人扣扣裙 519293536 有問題我都會盡力幫你們
上面已經能夠攔截到對象屬性的取值,賦值和刪除了,咱們來看看新增一個屬性能否攔截
proxy.height = 20
// 打印 set height 20 複製代碼
成功攔截!! 咱們知道vue2.0新增data上不存在的屬性是不能夠響應的,須要手動調用$set
的,這就是Proxy
的優勢之一
如今來看看嵌套對象的攔截,咱們修改info屬性的age屬性
proxy.info.age = 30 // 打印 get info 複製代碼
只能夠攔截到info,不能夠攔截到info的age屬性,因此咱們要遞歸了,問題是在哪裏遞歸呢?
由於調用proxy.info.age會先觸發proxy.info的攔截,因此咱們能夠在get中攔截,若是proxy.info是對象的話,對象須要再被代理一次,咱們把代碼封裝一下,寫成遞歸的形式
function reactive(target) { return createReactiveObject(target) } function createReactiveObject(target) { // 遞歸結束條件 if(!isObject(target)) return target const handler = { get(target, key, receiver) { console.log('get', key) let res = Reflect.get(target, key, receiver) // res若是是對象,那麼須要繼續代理 return isObject(res) ? createReactiveObject(res): res }, set(target, key, value, receiver) { console.log('set', key, value) let res = Reflect.set(target, key, value, receiver) return res }, deleteProperty(target, key) { console.log('deleteProperty', key) Reflect.deleteProperty(target, key) } } return new Proxy(target, handler) } function isObject(obj) { return obj != null && typeof obj === 'object' } // 測試部分 let obj = { name: 'hello', info: { age: 20 } } const proxy = reactive(obj) proxy.info.age = 30 複製代碼
運行上面的代碼,打印結果
get info
set age 30 複製代碼
Bingo! 嵌套對象攔截到了
vue2.0用的是Object.defineProperty攔截對象的getter和setter,一次將對象遞歸到底, 3.0用Proxy,是惰性遞歸的,只有訪問到某個屬性,肯定了值是對象,咱們才繼續代理下去這個屬性值,所以性能更好
如今咱們來測試數組的方法,看看可否攔截到,以push方法爲例, 測試部分代碼以下
let arr = [1, 2, 3] const proxy = reactive(arr) proxy.push(4) 複製代碼
打印結果
get push
get length
set 3 4 set length 4 複製代碼
和預期有點不太同樣,調用數組的push方法,不只攔截到了push, 還攔截到了length屬性,set被調用了兩次,在set中咱們是要更新視圖的,咱們作了一次push操做,卻觸發了兩次更新,顯然是不合理的,因此咱們這裏須要修改咱們的handler的set函數,區分一下是新增屬性仍是修改屬性,只有這兩種狀況才須要更新視圖
set函數修改以下
set(target, key, value, receiver) {
console.log('set', key, value) let oldValue = target[key] let res = Reflect.set(target, key, value, receiver) let hadKey = target.hasOwnProperty(key) if(!hadKey) { // console.log('新增屬性', key) // 更新視圖 }else if(oldValue !== value) { // console.log('修改屬性', key) // 更新視圖 } return res } 複製代碼
至此,咱們對象操做的攔截咱們基本已經完成了,可是還有一個小問題, 咱們來看看下面的操做
let obj = { some: 'hell' } let proxy = reactive(obj) let proxy1 = reactive(obj) let proxy2 = reactive(obj) let proxy3 = reactive(obj) let p1 = reactive(proxy) let p2 = reactive(proxy) let p3 = reactive(proxy) 複製代碼
咱們這樣寫,就會一直調用reactive代理對象,因此咱們須要構造兩個hash表來存儲代理結果,避免重複代理
function reactive(target) { return createReactiveObject(target) } let toProxyMap = new WeakMap() let toRawMap = new WeakMap() function createReactiveObject(target) { let dep = new Dep() if(!isObject(target)) return target // reactive(obj) // reactive(obj) // reactive(obj) // target已經代理過了,直接返回,不須要再代理了 if(toProxyMap.has(target)) return toProxyMap.get(target) // 防止代理對象再被代理 // reactive(proxy) // reactive(proxy) // reactive(proxy) if(toRawMap.has(target)) return target const handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver) // 遞歸代理 return isObject(res) ? reactive(res) : res }, // 必需要有返回值,不然數組的push等方法報錯 set(target, key, val, receiver) { let hadKey = hasOwn(target, key) let oldVal = target[key] let res = Reflect.set(target, key, val,receiver) if(!hadKey) { // console.log('新增屬性', key) } else if(oldVal !== val) { // console.log('修改屬性', key) } return res }, deleteProperty(target, key) { Reflect.deleteProperty(target, key) } } let observed = new Proxy(target, handler) toProxyMap.set(target, observed) toRawMap.set(observed, target) return observed } function isObject(obj) { return obj != null && typeof obj === 'object' } function hasOwn(obj, key) { return obj.hasOwnProperty(key) } 複製代碼
接下來就是修改數據,觸發視圖更新,也就是實現發佈訂閱,這一部分和2.0的實現部分同樣,也是在get中收集依賴,在set中觸發依賴
完整代碼以下
class Dep { constructor() { this.subscribers = new Set(); // 保證依賴不重複添加 } // 追加訂閱者 depend() { if(activeUpdate) { // activeUpdate註冊爲訂閱者 this.subscribers.add(activeUpdate) } } // 運行全部的訂閱者更新方法 notify() { this.subscribers.forEach(sub => { sub(); }) } } let activeUpdate function reactive(target) { return createReactiveObject(target) } let toProxyMap = new WeakMap() let toRawMap = new WeakMap() function createReactiveObject(target) { let dep = new Dep() if(!isObject(target)) return target // reactive(obj) // reactive(obj) // reactive(obj) // target已經代理過了,直接返回,不須要再代理了 if(toProxyMap.has(target)) return toProxyMap.get(target) // 防止代理對象再被代理 // reactive(proxy) // reactive(proxy) // reactive(proxy) if(toRawMap.has(target)) return target const handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver) // 收集依賴 if(activeUpdate) { dep.depend() } // 遞歸代理 return isObject(res) ? reactive(res) : res }, // 必需要有返回值,不然數組的push等方法報錯 set(target, key, val, receiver) { let hadKey = hasOwn(target, key) let oldVal = target[key] let res = Reflect.set(target, key, val,receiver) if(!hadKey) { // console.log('新增屬性', key) dep.notify() } else if(oldVal !== val) { // console.log('修改屬性', key) dep.notify() } return res }, deleteProperty(target, key) { Reflect.deleteProperty(target, key) } } let observed = new Proxy(target, handler) toProxyMap.set(target, observed) toRawMap.set(observed, target) return observed } function isObject(obj) { return obj != null && typeof obj === 'object' } function hasOwn(obj, key) { return obj.hasOwnProperty(key) } function autoRun(update) { function wrapperUpdate() { activeUpdate = wrapperUpdate update() // wrapperUpdate, 閉包 activeUpdate = null; } wrapperUpdate(); } let obj = {name: 'hello', arr: [1, 2,3]} let proxy = reactive(obj) // 響應式 autoRun(() => { console.log(proxy.name) })
我是08年出道的前端老鳥,想交流經驗能夠進個人扣扣裙 519293536 有問題我都會盡力幫你們
proxy.name = 'xxx' // 修改proxy.name, 自動執行autoRun的回調函數,打印新值 複製代碼
最後總結下vue2.0和3.0響應式的實現的優缺點:
- 性能 : 2.0用
Object.defineProperty
攔截對象的屬性的修改,在getter中收集依賴,在setter中觸發依賴更新,一次將對象遞歸到底攔截,性能較差, 3.0用Proxy
攔截對象,惰性遞歸,性能好 Proxy
能夠攔截數組的方法,Object.defineProperty
沒法攔截數組的push
,unshift
,shift
,pop
,slice
,splice
等方法(2.0內部重寫了這些方法,實現了攔截), proxy能夠攔截攔截對象的新增屬性,Object.defineProperty
不能夠(開發者須要手動調用$set
)- 兼容性 :
Object.defineProperty
支持ie8+,Proxy
的兼容性差,ie瀏覽器不支持本文的文字及圖片來源於網絡加上本身的想法,僅供學習、交流使用,不具備任何商業用途,版權歸原做者全部,若有問題請及時聯繫咱們以做處理