• [ES6]探究数据绑定之Proxy


    知识储备

    Proxy 方式实现数据绑定中涉及到 Proxy、Reflect、Set、Map 和 WeakMap,这些都是 ES6 的新特性。

    Proxy

    Proxy 对象代理,在目标对象之前架设一层拦截,外部对目标对象的操作,都会通过这层拦截,我们可以定制拦截行为,每一个被代理的拦截行为都对应一个处理函数。

    
     

    1

    
     

    let p = new Proxy(target, handler);

    
     

    1

    2

    3

    4

    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 实现数据绑定

  • 相关阅读:
    19. 删除链表的倒数第 N 个结点
    相交链表
    环形链表2
    环形链表
    K8s 网络通讯
    Hutool-二维码生成
    Hutool-加解密
    Hutool-解析JSON
    Hutool-读取配置文件中的配置
    Hutool-操作图片
  • 原文地址:https://www.cnblogs.com/xosg/p/10257774.html
Copyright © 2020-2023  润新知