[ES6]探究數據綁定之Proxy

 

知識儲備

Proxy 方式實現數據綁定中涉及到 Proxy、Reflect、Set、Map 和 WeakMap,這些都是 ES6 的新特性。javascript

Proxy

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種:

  • get(target, propKey, receiver)
  • set(target, propKey, value, receiver)
  • has(target, propKey)
  • deleteProperty(target, propKey)
  • ownKeys(target)
  • getOwnPropertyDescriptor(target, propKey)
  • defineProperty(target, propKey, propDesc)
  • preventExtensions(target)
  • getPrototypeOf(target)
  • isExtensible(target)
  • setPrototypeOf(target, proto)
  • apply(target, object, args)
  • construct(target, args)

👉 更詳細介紹參考:
MDN·Proxy
Proxy

Reflect

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

Set

  • 相似 Array 數組
  • Set 容許你存儲任何類型的惟一值,不管是原始值或者是對象引用
  • Set 成員的值都是惟一的,沒有重複值

WeakSet

  • 相似 Set,也是不重複元素的集合
  • WeakSet 對象中只能存放對象值, 不能存放原始值, 而 Set 對象均可以
  • WeakSet 對象中存儲的對象值都是被弱引用的, 若是沒有其餘的變量或屬性引用這個對象值, 則這個對象值會被當成垃圾回收掉. 正由於這樣, WeakSet 對象是沒法被枚舉的, 沒有辦法拿到它包含的全部元素

Map

  • 相似 Object 對象,保存鍵值對
  • Map 任何值(對象或者原始值) 均可以做爲一個鍵(key)或一個值(value),而 Object 對象的 key 鍵值只能是字符串

WeakMap

  • 相似 Map,也是一組鍵值對的集合
  • WeakMap 對象中的鍵是弱引用的。鍵必須是對象,值能夠是任意值
  • 因爲這樣的弱引用,只要所引用的對象的其餘引用都被清除,垃圾回收機制就會釋放該對象所佔用的內存。也就是說,一旦再也不須要,WeakMap 裏面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用,原理同 WeakSet
  • WeakMap 的 key 是非枚舉的

Proxy 實現數據綁定

先上完整代碼 👉

 

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
  • 待執行監聽函數集合 Set:var queuedObservers = new Set()
  • 監聽對象集合 WeakMap:var observers = new WeakMap() 鍵值爲監聽對象
  • 對象屬性監聽函數集合 Set:監聽了對象屬性的監聽函數,都保存到對象屬性監聽函數集合 Set 中,方便在對象屬性發生變動時,執行監聽函數
  • 攔截方法 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) 組合的對象屬性監聽函數集 Set
  • 當前監聽函數 print 被添加到對象屬性監聽函數集 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) 組合的對象屬性監聽函數集 Set
  • 對象屬性監聽函數集 Set 中的監聽函數(包括 print)入待執行監聽函數集合,準備執行
  • 再次執行 print

高級主題

動態 observable tree

 

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 實現數據綁定

相關文章
相關標籤/搜索