双向数据绑定

采用数据劫持结合发布者订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,数据变动时发布消息给订阅者,触发相应函数的回调。

解释

MVVM双向数据绑定,数据变化更新试图,视图变化更新数据。

视图变化更新数据可通过事件监听来实现,而关键在于数据变化更新视图。

如果要实现双向数据绑定,需要四个东东:

  • Observer数据监听器:对对象的所有属性进行监听,拿到最新的数据,通知给订阅者;
    • Observer中通过Object.defineProperty方法,监听对象的所有属性,会new一个新的消息订阅器Dep,在消息订阅器中有订阅者数组,专门用来存储订阅者。当属性值被获取的时候,会添加订阅者进去。而当属性值被修改以后,会调用订阅器中的函数,来通知所有的订阅者。
  • Dep订阅器:用来收集订阅者,管理订阅者和监听器。
    • 会遍历订阅者数组,调用订阅者的更新函数。
  • Compiler编译器:对子元素节点的指令进行扫描和解析,根据模板指令替换数据,初始化视图以及绑定相应的回调函数;
    • 编译器有两个作用个,一个是解析模板指令,替换模板数据,初始化视图;另一个是将模板指令所在的模板节点绑定更新函数,并且初始化相应的订阅器
  • Watcher订阅者:作为ObserverCompile的桥梁,订阅属性变动的通知,执行指令绑定的回调函数,更新视图。
    • 接收三个参数,vue实例,属性名,更新函数。在compiler初始化的时候,根据属性名获取旧的属性值。在被Observer通知调用订阅者的更新函数时,会先获取下最新的属性值,如果旧值与新值不等的话,调用更新函数。
  • MVVM入口:整合三者

流程图如下:

MVVM.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function MVVM(options){
this.$options = options || {};
var data = this._data = this.$options.data;
var me = this;
// 数据代理
// 实现vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function(key){
me._proxyData(keys);
});
// 代理计算属性
this._initComputed();
observe(data,this);
this.$compile = new Compile(options.el || document.body,this)
}
MVVM.prototype = {
$watch: function(key,cb,options){
new Watcher(this,key,cb)
}
}

Observer.js

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
function Observer(data){
Object.keys(data).forEach(function(){
defineReactive(data,key,data[key]);
});
}
function defineReactive(data,key,val){
var dep = new Dep();
var childObj = observe(val);

Object.defineProperty(data,key,{
enumerable:true,
configurable:false,
get:function(){
if(Dep.target){
dep.depend();
}
return val;
},
set:function(newVal){
if(newVal === val){
return;
}
val = newVal;
childObj = observe(newVal);
dep.notify();
}
});
}

两种实现双向绑定的机制

Object.defineProperty

虽然核心思想均是采用数据劫持和观察者模式,但是数据劫持的方式可以有Object.definePropertyproxy,首先是为什么会采用这两种方式?

  • OD是通过劫持的对象以及遍历对象的所有属性实现的,首先在性能上遍落后,而且如果属性值是数组的话,则没办法遍历。因为在vue中,只有八个方法可实现对数组变化的响应:push pop shift unshift splice sort reverse,单纯的下标取数法是响应不了的。

  • 又有一种OD实现的双向绑定:

    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
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    let uid = 0;
    // 消息管理器,存储订阅者并发布消息
    class Dep {
    constructor(){
    // 设置id,用于区分watcher和只改变属性值后新产生的Watcher
    this.id = uid++;
    // 存储订阅者的数组
    this.subs = [];
    }

    // 触发target上的watcher中的addDep方法,参数为dep的实例本身
    depend(){
    // 把消息管理器传到Watcher那里,然后存储
    Dep.target.addDep(this);
    }
    // 添加订阅者
    addSub(sub){
    this.subs.push(sub)
    }
    notify(){
    // 循环的方式通知所有的订阅者(Watcher),触发订阅者的响应逻辑处理
    this.subs.forEach(sub => sub.update)
    }
    }
    // 为Dep类设置一个静态属性,默认为null,工作时指向当前的Watcher
    Dep.target = null;

    //Observer 监听者,监听属性值的变化
    class Observer {
    constructor(value){
    this.value = value;
    this.walk(value);
    }
    // 遍历属性值并监听
    walk(value){
    Object.keys(value).forEach(key => this.convert(key,value[key]));
    }
    convert(key,value){
    defineReactive(this.value,key,val);
    }
    }
    function defineReactive(obj, key,val){
    // 实例化一个消息管理器
    const dep = new Dep();
    // 给当前属性的值添加监听
    let childOb = observe(val);
    Object.defineProperty(obj, key, {
    enmuerable: true,
    configurable: true,
    get: () => {
    // 如果Dep类存在target属性,则将其添加到dep实例的subs数组中
    // target指向一个Watcher实例,每个Watcher都是一个订阅者
    // Watcher实例在实例化的过程中
    if (Dep.target) {
    dep.depend();
    }
    return val;
    },
    set: newVal => {
    if (val === newVal) return;
    val = newVal;
    // 对新值进行监听;
    childOb = observe(newVal);
    dep.notify();
    }
    })
    }
    function observe(value){
    if (!value || typeof value !== 'object') {
    return;
    }
    return new Observer(value);
    }


    //订阅者 Watcher
    class Watcher{
    constructor(vm, expOrFn, cb){
    this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者,采用对象是避免重复的key
    this.vm = vm; // 被订阅的数据一定来自于当前的Vue实例
    this.cb = cb; // 当数据更新时想要做的事情
    this.expOrFn = expOrFn; // 被订阅的数据
    this.val = this.get(); // 维护更新之前的数据
    }
    // 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用
    update(){
    this.run();
    }
    addDep(dep){
    // 如果在depIds的hash中没有当前的id,可以判断是新watcher,因此可以添加到dep的数组中存储
    // 此判断是避免同id的Watcher被多次存储
    if (!this.depIds.hasOwnProperty(dep.id)) {
    dep.addSub(this);
    // 把消息管理器放到[]中
    this.depIds[dep.id] = dep;
    }
    }
    run(){
    const val = this.get();
    console.log(val);
    if (val != this.val) {
    this.val = val;
    // 触发当前需要执行的操作,并且传新的参数
    this.cb.call(this.vm,val)
    }
    }
    get(){
    // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
    Dep.target = this;
    const val = this.vm._data[this.expOrFn];
    // 置空。用于下一个Watcher使用
    Dep.target = null;
    return val;
    }
    }

Proxy

  • Proxy直接监听对象,而不是遍历属性。

  • 可以检测到数组的变化

  • 返回的新的对象,而不是像OD那样,遍历属性,并且直接修改该属性,比较安全。

  • 不过因为支持度没有那么好,要在vue3.0上才真正的应用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 极简版
    const input = document.getElementById('input')
    const p = document.getElementById('p')
    const obj = {}

    const newObj = new Proxy(obj, {
    get: function(target, key, receiver) {
    console.log(`getting ${key}!`)
    return Reflect.get(target, key, receiver);
    },
    set: function(target, key, receiver) {
    console.log(target, key, receiver)
    if(key === 'text'){
    input.value = value;
    p.innerHTML = value;
    }
    return Reflect.set(target, key, receiver)
    }
    })

自我理解

根据https://juejin.im/post/5d421bcf6fb9a06af23853f1#heading-9此篇文章,理解为如下: