Proxy 方式實現數據綁定中涉及到 Proxy、Reflect、Set、Map 和 WeakMap,這些都是 ES6 的新特性。javascript
Proxy 對象代理,在目標對象以前架設一層攔截,外部對目標對象的操做,都會經過這層攔截,咱們能夠定製攔截行爲,每個被代理的攔截行爲都對應一個處理函數。html
1java |
let p = new Proxy(target, handler);git |
1es6 2github 3數組 4app 5異步 6函數 7 8 9 |
var handler = { get: (target, name, recevier) => { return 'proxy' } } var p = new Proxy({}, handler) p.a = 1 console.log(p.a, p.c) // -> proxy proxy |
Proxy 構造函數接收兩個參數:
在這個例子中,目標對象是一個空對象,配置對象中有一個 get
函數,用來攔截外部對目標對象屬性的訪問,能夠看到,get
函數始終返回 proxy
。
Proxy 支持攔截的操做一共有13種:
Reflect 對象同 Proxy 對象同樣,也是 ES6 爲了操做對象而提供的新特性。
Reflect 對象的方法與 Proxy 對象的方法一一對應,只要是 Proxy 對象的方法,就能在 Reflect 對象上找到對應的靜態方法(Reflect 對象沒有構造函數,不能使用 new 建立實例)。這就讓 Proxy 對象能夠方便地調用對應的 Reflect 方法,完成默認行爲,做爲修改行爲的基礎。也就是說,無論 Proxy 怎麼修改默認行爲,你總能夠在 Reflect 上獲取默認行爲。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var handler = { get: (target, name, recevier) => { console.log('get: ', name) Reflect.get(target, name) }, set: (target, name, value, recevier) => { console.log('set: ', name) Reflect.get(target, name, value) } } var p = new Proxy({}, handler) p.a = 1 console.log(p.a, p.c) |
代碼執行結果,輸出:
1 2 3 |
set: a get: a get: c |
上面代碼中,Proxy 攔截目標對象的 get
和 set
方法,在其中定製攔截行爲,最後採用 Reflect.get
和 Reflect.set
分別完成目標對象默認的屬性獲取和設置行爲。
👉 更詳細介紹參考:
MDN·Reflect
Reflect
Set
WeakSet
Map
WeakMap
先上完整代碼 👉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
// 監聽對象集合 var observers = new WeakMap() // 待執行監聽函數集合,Set 能夠避免重複 var queuedObservers = new Set() // 當前監聽函數 var currentObserver function observable(obj) { observers.set(obj, new Map()) return new Proxy(obj, { get, set }) } function get(target, key, receiver) { // get 方法默認行爲 const result = Reflect.get(target, key, receiver) // 當前監聽函數中,監聽使用了該屬性, // 那麼把該 監聽函數 存放到該屬性對應的 對象屬性監聽函數集合 Set if (currentObserver) { registerObserver(target, key, currentObserver) } return result } function registerObserver(target, key, observer) { let observersForKey = observers.get(target).get(key) // 爲每個對象屬性都建立一個 Set 集合,存放監聽了該屬性的監聽函數 if (!observersForKey) { observersForKey = new Set() observers.get(target).set(key, observersForKey) } observersForKey.add(observer) } function set(target, key, value, receiver) { const observersForKey = observers.get(target).get(key) // 修改對象屬性,即對象屬性值發生變動時, // 判斷 對象屬性監聽函數集合 Set 是否存在,將其中的全部監聽函數都添加到 待執行監聽函數集合 if (observersForKey) { observersForKey.forEach(queueObserver) } // set 方法默認行爲 return Reflect.set(target, key, value, receiver) } function observe(fn) { queueObserver(fn) } // 將監聽函數添加到 待執行監聽函數集合 Set 中 // 若是 待執行監聽函數集合 Set 爲空,那麼在添加後當即執行 function queueObserver(observer) { if (queuedObservers.size === 0) { // 異步執行 Promise.resolve().then(runObservers) } queuedObservers.add(observer) } // 執行 待執行監聽函數集合 Set 中的監聽函數 // 執行完畢後,進行清理工做 function runObservers() { try { queuedObservers.forEach((observer) => { currentObserver = observer observer() }) } finally { currentObserver = undefined queuedObservers.clear() } } |
對外暴露的 observable(obj)
和 observe(fn)
方法兩者分別用於建立 observable 監聽對象和 observer 監聽回調函數。當 observable 監聽對象發生屬性變化時,observer 函數將自動執行。
測試用例:
1 2 3 4 5 6 7 8 9 |
var obj = {name: 'John', age: 20} // observable object var person = observable(obj) function print () { console.log(`監聽屬性發生變化:${person.name}, ${person.age}`) } // observer function observe(print) |
關於 observable(obj)
和 observe(fn)
:observable(obj)
方法中,經過 ES6 Proxy 爲目標對象 obj
建立代理,攔截 get
和 set
操做
currentObserver
var queuedObservers = new Set()
var observers = new WeakMap()
鍵值爲監聽對象get
:使用 obj.property
獲取對象屬性,即會被攔截方法 get
攔截get
中的註釋set
:使用 obj.property = value
設置對象屬性,即會被攔截方法 set
攔截set
中的註釋observe(fn)
方法中,添加對象屬性監聽函數
監聽函數中使用 obj.property
獲取對象屬性,即代表監聽函數監聽了該屬性,那麼就會觸發攔截方法 get
中對監聽屬性的邏輯處理,爲其建立對象屬性監聽函數集合 Set,並將當前的監聽函數添加進其中
observable
方法建立代理對象 person
observe
方法設置監聽函數,此時待執行監聽函數集合 Set 爲空,監聽函數添加到 Set 中後執行待執行監聽函數集合 Set 中的監聽函數runObservers
方法中當前監聽函數 currentObserver
被設爲 print
print
開始執行print
內部檢索到 person.name
person
上觸發攔截方法 get
observers.get(person).get('name')
檢索到 (person, name)
組合的對象屬性監聽函數集 Setprint
被添加到對象屬性監聽函數集 Set 中person.age
,同理,執行前面在 print
內部檢索到 person.name
的流程${person.name}, ${person.age}
打印出來;print
函數執行結束;currentObserver
變爲 undefined
當調用 person.age = 22
修改對象屬性時:
person
上觸發攔截方法 set
observers.get(person).get('age')
檢索到 (person, age)
組合的對象屬性監聽函數集 Setprint
)入待執行監聽函數集合,準備執行print
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var obj = { name: 'John', age: 20, teacher: { name: 'Tom', age: 30 }} // observable object var person = observable(obj) function print () { console.log(`監聽屬性發生變化:${person.teacher.name}, ${person.teacher.age}`) } // observer function observe(print) setTimeout(() => {person.teacher.name = 'Jack'}) |
到目前爲止,單層對象的數據綁定監聽是正常工做的。可是在這個例子中,咱們監聽的對象值又是對象,這個時候監聽就失效了,咱們須要將:
1 |
observable({data: {name: 'John'}}) |
替換成
1 |
observable({data: observable({name: 'John'})}) |
這樣就能正常運行了 😋
顯然,這樣使用不方便,能夠作攔截方法 get
中修改一下,在返回值是對象時,對返回值對象也調用 observable(obj)
爲其建立監聽對象。
1 2 3 4 5 6 7 8 9 10 11 12 |
function get(target, key, receiver) { const result = Reflect.get(target, key, receiver) if (currentObserver) { registerObserver(target, key, currentObserver) if (typeof result === 'object') { const observableResult = observable(result) Reflect.set(target, key, observableResult, receiver) return observableResult } } return result } |
對於 Proxy 攔截操做也能夠在原型鏈中被繼承,例如:
1 2 3 4 5 6 7 8 9 |
let proto = new Proxy({}, { get(target, propertyKey, receiver) { console.log('GET ' + propertyKey); return Reflect.get(target, propertyKey, receiver); } }); let obj = Object.create(proto); obj.foo // "GET foo" |
上面代碼中,攔截操做 get
定義在原型對象上面,因此若是讀取 obj
對象屬性時,攔截會生效。
同理,經過 Proxy 實現的數據綁定也能與原型繼承搭配工做,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const parent = observable({greeting: 'Hello'}) const child = observable({subject: 'World!'}) Object.setPrototypeOf(child, parent) function print () { console.log(`${child.greeting} ${child.subject}`) } // 控制檯打印出 'Hello World!' observe(print) // 控制檯打印出 'Hello There!' setTimeout(() => child.subject = 'There!') // 控制檯打印出 'Hey There!' setTimeout(() => parent.greeting = 'Hey', 100) // 控制檯打印出 'Look There!' setTimeout(() => child.greeting = 'Look', 200) |
本文中經過簡單的代碼展現了 Proxy 實現數據綁定,更加完整的實現,參考:nx-js/observer-util
Writing a JavaScript Framework - Data Binding with ES6 Proxies
使用 ES6 Proxy 實現數據綁定