文中代码是从vue2.6源码中摘抄

Vue实现数据双向绑定的原理是基于数据劫持结合发布-订阅者模式实现的,通过Object.defineProperty()来劫持各个属性,并在数据变动时发布消息给订阅者,触发相应的监听回调,更新视图

Vue要实现数据的双向绑定,就必须要具备以下几点
1:Observer数据监听器:对数据对象的所有属性进行监听,如有变动则获取最新值并通知订阅者更新视图
2:Compile指令解析器:对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,并绑定相应的更新视图函数
3:Dep事件调度中心:如当Observer数据监听器监听的数据有变动,则通过Dep通知订阅者Watcher
4:Watcher订阅者:连接Observer和Compile的桥梁,能够订阅并接收每个属性变动的通知,执行每个指令绑定的回调函数,进而更新视图

如图

android p v 双向绑定 实现双向绑定_MVVM

Observer数据监听器

将需要观察的数据对象进行递归遍历,通过Obeject.defineProperty()来监听每个属性的变动,当给某个属性赋值时,就会触发set方法,那么就能监听到数据变化了,部分源码

function observe(value: any, asRootData: ?boolean): Observer | void {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    //....省略
    ob = new Observer(value)
    return ob
}

class Observer {
    value: any;
    constructor(value: any) {
        this.value = value
        this.vmCount = 0
        this.walk(value)
        //....省略
    }

    walk(obj: Object) {
        //....省略
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
}

function defineReactive(
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {
    //....省略
    const dep = new Dep()//事件调用中心,存放订阅者Watcher
    observe(val)
    Object.defineProperty(obj, key, {
        get: function reactiveGetter() {
            //....省略
            if (Dep.target) {
                dep.depend()//往事件队列中添加订阅者Watcher
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            //如果值相等就直接返回 不更新视图
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            //....省略
            val = newVal //赋新值
            //通知所有订阅者
            dep.notify()
        }
    })
}

Observer主要作用

1:对需要观察的数据对象进行递归遍历,为每个属性添加get、set方法
2:往Dep订阅器中添加订阅者Watcher
3:当数据变化时,通知所有订阅者更新数据

创建数据监听器后,通过for循环遍历给每个属性 并通过Object.defineProperty给每个属性添加get、set方法

在get方法中,除了返回我们所要的值以外,还往Dep事件队列中添加Watcher订阅者,用于当数据变化时通知订阅者

在set方法中,对比新旧值,如若不同,则通过Dep的notify方法通知订阅者,进而更新视图

Dep事件调度中心

Dep主要作用
1:维护一个数组,用于存放订阅者Watcher(在Watcher自身实例化时,通过一系列调用,往订阅器Dep中添加自己)
2:当数据变化时,能够调用自身notify方法,从而触发所有订阅者Watcher

class Dep {
    static target: ?Watcher;
    subs: Array<Watcher>;

    //....省略
    constructor() {
        //内部维护一个数组,用于存放订阅者Watcher
        this.subs = []
    }

    addSub(sub: Watcher) {
        this.subs.push(sub)
    }

    depend() {
        if (Dep.target) {  //Dep.target就是订阅者Watcher
            //调用Watcher的addDep方法,然后在回调到Dep的addSub方法从而添加订阅者
            Dep.target.addDep(this)
        }
    }

    notify() {
        for (let i = 0, l = subs.length; i < l; i++) {
            //通知所有订阅者
            subs[i].update()
        }
    }
}

Dep内部维护着一个数组,通过depend往数组中添加订阅者Watcher,当属性有变化时会调用属性的set方法,进而调用Dep的notify方法,在notify内部通过循环的方式调用订阅者Watcher的update方法

Watcher订阅者

Watcher主要作用
1:在自身实例化时,通过一系列调用,往订阅器Dep中添加自己
2:当数据变化时,通过一系列调用,能够调用自身update方法,从而触发Compile中绑定的回调,更新视图

class Watcher {

    constructor(
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
    ) {
        this.vm = vm
        //回调函数,用于更新视图
        this.cb = cb
        //调用自己的get方法,目的是触发属性的get方法
        this.value = this.lazy
            ? undefined
            : this.get()
    }

    get() {
        //把当前对象赋值给Dep.target
        Dep.target = this
        //....省略
        //触发属性的get方法,从而在Dep中添加自己
        value = this.getter.call(vm, vm)
        //设置Dep.target为null
        Dep.target = null
        return value
    }

    //Dep对象会调用此方法
    addDep(dep: Dep) {
        if (!this.depIds.has(id)) {
            dep.addSub(this) //添加Watcher到Dep中
        }
    }

    //属性变化调用Dep的notify方法,进而调用此方法
    update() {
        this.run()
    }

    run() {
        const value = this.get()
        const oldValue = this.value
        if (value !== oldVal) {
            this.value = value;
            //属性值变化,调用回调函数,更新视图
            this.cb.call(this.vm, value, oldValue)
        }
    }
}

//Observer中代码
Object.defineProperty(obj, key, {
    get: function reactiveGetter() {
        //....省略
        if (Dep.target) {
            dep.depend()//往事件队列中添加订阅者Watcher
        }
        return value
    }
}

实例化Watcher的时候,调用其内部的get方法,此方法首先把当前对象赋值给Dep.target,然后调用监听属性的get方法,进而往调用中心Dep中添加订阅者,从而当属性值有变化时,就会通知Dep调度中心,进而通知Watcher,而Watcher又会通过回调函数,通知视图更新

Compile指令解析器

因vue2.6源码这部分还没看太懂,所以直接转载了

Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者(Watcher),一旦数据有变动,收到通知,更新视图

如图所示:

android p v 双向绑定 实现双向绑定_android p v 双向绑定_02

因为遍历解析的过程有多次操作DOM节点,为提高性能和效率,会先将根节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实DOM节点中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对弈的指令更新函数进行绑定

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

通过new Watcher()添加回调来接收数据变化的通知,以此来达到数据的双向绑定

观察者模式和发布订阅模式的区别

android p v 双向绑定 实现双向绑定_MVVM_03


从图中可以看出两者的区别

1:发布订阅模式多了一个事件调度中心

2:观察者模式中被观察者和观察者有依赖关系:

3:发布订阅模式因中间有事件调度中心,所以没有依赖关系

4:观察者模式多应用于同步事件通知上

5:发布订阅模式因中间有事件调度中心,多应用于异步事件通知上