用Proxy/Object.defineProperty实现双向绑定

前言

上文我们讲了Proxy 与 Object.defineProperty的对比,Proxy 与 Object.defineProperty最典型的应用就是用于实现双向数据绑定。但实现双向数据绑定的方法不止于此。

  • 发布者-订阅者模式(backbone.js):一般通过sub, pub的方式实现数据和视图的绑定监听
  • 脏值检查(angular.js) :通过脏值检测的方式比对数据是否有变更,来决定是否更新视图
  • 数据劫持(vue.js):采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调

不过我们今天只讲一讲如何使用Proxy 与 Object.defineProperty来实现双向数据绑定。

双向数据绑定

简单来说双向数据绑定就是数据和UI建立双向的通信通道,可以通过数据来更新UI显示,也可以通过UI的操做来更新数据。下图可以很好的说明一切

实现思路

实现一个简单的双向数据绑定并不难,我们来看一个简单的例子
html:

<span id="box">
        <h1 id='text'></h1>
        <input type="text" id='input' oninput="inputChange(event)" />
        <button id="button" onclick="clickChange()">Click me</button>
    </span>
1
2
3
4
5

js:

<script>
        const input = document.getElementById('input');
        const text = document.getElementById('text');
        const button = document.getElementById('button');
        const data = {
            value: ''
        }
        function defineProperty(obj, attr) {
            let val
            Object.defineProperty(obj, attr, {
                set(newValue) {
                    console.log('set')
                    if (val === newValue) {
                        return;
                    }
                    val = newValue;
                    input.value = newValue;
                    text.innerHTML = newValue;
                },
                get() {
                    console.log('get');
                    return val
                }
            })
        }
        defineProperty(data, 'value')
        function inputChange(event) {
            data.value = event.target.value
        }

        function clickChange() {
            data.value = 'hello'
        }
    </script>
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

但是想要实现Vue的双向数据绑定并没有这么简单,我们知道Vue的双向数据绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,那么我们起码要做以下三个步骤:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
流程图如下:

实现Observer

使用 Object.defineProperty 定义一个Observer

function defineProperty(obj, key, value) {
            Observer(value); // 递归遍历所有子属性
            Object.defineProperty(obj, key, {
                enumerable: true, // 可枚举
                configurable: false, // 不能再define
                set(newValue) {
                    if (value === newValue) {
                        return;
                    }
                    value = newValue;
                    console.log(`set ${key}: ${newValue}`);
                },
                get() {
                    console.log(`get ${key}: ${value}`);
                    return value
                }
            })
        }

        function Observer(data) {
            if (!data || typeof data !== 'object') {  // 非对象即终止遍历
                return;
            }
            Object.keys(data).forEach(function (key) {
                defineReactive(data, key, data[key]);   // 监听所有对象属性
            });
        }
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

实现Dep

创建一个用来存储订阅者Watcher的订阅器,订阅器Dep主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数。

function Dep() {
            this.list = [];
        }
        Dep.prototype = {
            addSub: function (watcher) {
                this.list.push(watcher);
            },
            notify: function () {
                this.list.forEach(function (watcher) {
                    watcher.update();
                });
            }
        };
1
2
3
4
5
6
7
8
9
10
11
12
13

实现Watcher

既然实现了一个订阅器,那么就需要一个订阅者,订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中,
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发回调,更新视图

function Watcher(obj, key, cb) {
            this.cb = cb;
            this.obj = obj;
            this.key = key;
            // 此处为了触发属性的getter,从而在dep添加自己
            this.value = this.get();
        }
        Watcher.prototype = {
            update: function () {
                this.run();    // 属性值变化收到通知
            },
            run: function () {
                var value = this.get(); // 取到最新值
                var oldVal = this.value;
                if (value !== oldVal) {
                    this.value = value;
                    this.cb.call(this.obj, value, oldVal); // 执行Compile中绑定的回调,更新视图
                }
            },
            get: function () {
                Dep.target = this;    // 将当前订阅者指向自己
                var value = this.obj[this.key];    // 触发getter,添加自己到属性订阅器中
                Dep.target = null;    // 添加完毕,重置
                return value;
            }
        };
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

实现了订阅器和订阅者之后,需要将订阅器添加进入订阅者,将Observer改造以下植入订阅器。如果不好理解可以结合watcher一起看。

function defineProperty(obj, key, value) {
            Observer(value); // 递归遍历所有子属性
            var dep = new Dep();  // 生成一个Dep实例
            Object.defineProperty(obj, key, {
                enumerable: true, // 可枚举
                configurable: false, // 不能再define
                set(newValue) {
                    if (value === newValue) {
                        return;
                    }
                    value = newValue;
                    console.log(`set ${key}: ${newValue}`);
                    dep.notify(); // 如果数据变化,通知所有订阅者
                },
                get() {
                    if (Dep.target) {
                        dep.addSub(Dep.target); // 在这里添加一个订阅者,这里的Dep.target是指订阅器本身
                    }
                    console.log(`get ${key}: ${value}`);
                    return value
                }
            })
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

Observer改造完成后,已经具备了监听数据, 添加订阅器和数据变化通知订阅者的功能。接下来就是将watcher添加进入订阅者,模拟实现Compile,并进行数据初始化。

模拟实现Compile

我们这里不解析指令所以直接写出watcher,并添加进去订阅者

function inputChange(event) {
            data.value = event.target.value
        }

        function clickChange() {
            data.value = '你好 世界'
        }
        function renderInput(newValue) {
            if (input) {
                input.value = newValue
            }
        }

        function renderText(newValue) {
            if (text) {
                text.innerHTML = newValue
            }
        }
        new Watcher(data, 'value', renderInput)
        new Watcher(data, 'value', renderText)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

数据初始化

let data = {
            value: ''
        }
        Observer(data)
1
2
3
4

这样一个简单的基于 Object.defineProperty 的双向数据绑定就完成了。

Proxy

由于Object.defineProperty在数组监控方面的不足,我们可以采用Proxy,只需要修改Observer即可实现上面例子的功能

function Observer(target) {
            var dep = new Dep();  // 生成一个Dep实例
            let handler = {
                get: function (obj, name) {
                    console.log('get')
                    const prop = obj[name];
                    if (Dep.target) {
                        dep.addSub(Dep.target); // 在这里添加一个订阅者,这里的Dep.target是指订阅器本身
                    }
                    if (typeof prop === 'undefined') return;
                    if (!prop.isBindingProxy && typeof prop === 'object') {
                        obj[name] = new Proxy(prop, proxyHandler);
                    }
                    return obj[name]
                },
                set: function (obj, name, value) {
                    Reflect.set(target, name, value);
                    obj[name] = value
                    dep.notify(); // 如果数据变化,通知所有订阅者
                },
            };
            let p = new Proxy(target, handler);
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

参考链接

最后更新时间: 10/8/2019, 7:41:17 PM