MVVM实现原理**(五)**—— 连接视图与数据【Watcher】

188 阅读5分钟

mvvm定义:MVVM是Model-View-ViewModel的简写。即模型-视图-视图模型。【模型】指的是后端传递的数据。【视图】指的是所看到的页面。【视图模型】mvvm模式的核心,它是连接view和model的桥梁。

【视图模型】它有两个方向:一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。

这两个方向都实现的,我们称之为数据的双向绑定。

总结:在MVVM的框架下视图和模型是不能直接通信的。它们通过ViewModel来通信,ViewModel通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定。并且MVVM中的View 和 ViewModel可以互相通信。MVVM流程图如下:

Watcher 订阅者,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。Dep 消息订阅器,内部维护了一个数组,用来收集订阅者(Watcher),数据变动触发notify 函数,再调用订阅者的 update 方法。执行流程如下:

从图中可以看出,当执行 new Vue() 时,Vue 就进入了初始化阶段,
一方面:创建观察者Observe。 Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能。 此时在getter方法中,Wather 会将自己添加到消息订阅器中(Dep)
另一方面:创建指令解析器Compile。 Vue 的指令编译器Compile 对元素节点的指令进行解析,初始化视图并订阅Watcher 来更新视图此时会创建Watcher(订阅者),Watcher会有更新视图的方法update
当数据发生变化时: Observer 中的 setter 方法被触发,setter 会立即调用订阅器的notify方法——Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。
因为VUE使用Object.defineProperty方法来做数据绑定,而这个方法又无法通过兼容性处理,所以Vue 不支持 IE8 以及更低版本浏览器。另外,查看vue原代码,发现在vue初始化实例时, 有一个proxy代理方法,它的作用就是遍历data中的属性,把它代理到vm的实例上,这也就是我们可以这样调用属性:vm.aaa等于vm.data.aaa。

总结的话:

data通过Observer转换成了getter/setter的形式来追踪变化
当外界通过Watcher读取数据的,会触发getter从而将watcher添加到依赖中
当数据变化时,会触发setter从而向Dep中的依赖(watcher)发送通知
watcher接收通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户 的某个回调函数等

function Vue(options = {}) {
    this.$options = options;//将所有属性挂载在options
 
    var data = this._data = this.$options.data;
    // 一方面: Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;
    observe(data)
 
    //观察完之后,this代理了this._data  数据代理
    for (let key in data) {
        Object.defineProperty(this, key, {
            enumerable: true,
            get() {
                return this._data[key];//获取到新值
            },
            set(newVal) {
                this._data[key] = newVal//这里触发观察者【再次执行Observe(data)中的set方法】
            }
        })
    }
    //另一方面: Vue 的指令编译器Compile 对元素节点的指令进行解析,初始化视图,并订阅Watcher 来更新视图, 此时Wather 会将自己添加到消息订阅器中(Dep),初始化完毕。  
    new Compile(options.el, this)
}
 
 
//模板编译器
function Compile(el, vm) {
    //el代表替换的范围
    vm.$el = document.querySelector(el);
 
    /*获取到元素以后,把里面的子元素都拿到,需要移动到内存中操作,所以需要创建文档碎片*/
    //创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
    let fragment = document.createDocumentFragment();
 
    //把app里面的内容都放到内存中,此时页面上为空白
    while (child = vm.$el.firstChild) {
        fragment.appendChild(child)
    }
 
    //递归 替换虚拟节点中的 {{}}
    replace(fragment)
    function replace(fragment) {
        Array.from(fragment.childNodes).forEach((node) => {
            //循环每一层
            let text = node.textContent;
            let reg = /\{\{(.*)\}\}/
 
            //nodeType为3时,是文本内容
            if (node.nodeType === 3 && reg.test(text)) {
                //console.log(RegExp.$1) // a.a vm.b
                let arr = RegExp.$1.split('.') //[a,a]
                let val = vm;
                arr.forEach((k) => {
                    val = val[k]
                })
 
                new Watcher(vm, RegExp.$1, function(newVal){//函数里需要接收一个新的值
                    node.textContent = text.replace(/\{\{(.*)\}\}/, newVal)
                })
 
                //替换逻辑
                node.textContent = text.replace(/\{\{(.*)\}\}/, val)
                console.log(node.textContent)
            }
            if (node.childNodes) {
                replace(node)
            }
        })
    }
 
    //把内存中的文档碎片放回到页面上,此时页面上的东西显示回来
    vm.$el.appendChild(fragment)
 
}
 
 
 
//vm.$options
function Observe(data) {//这里写我们的主要逻辑
    let dep = new Dep()
    for (let key in data) {//把data属性通过object.defineProperty的方式定义
 
        let val = data[key]
 
        observe(val)//递归 使data的中的每个属性都被Object.defineProperty处理
 
        Object.defineProperty(data, key, {
            enumerable: true,
            get() {
                
                //把watcher 放入到 订阅者中
                Dep.target && dep.addSub(Dep.target) //[Dep.target即为watcher]
 
                return val;
            },
            set(newVal) {//更改值得时候
                if (newVal === val) {//设置的值和以前是一样的东西
                    return;
                }
                val = newVal;//如果以后在获取值得时候,将刚才设置的值丢回去
                observe(newVal)
 
                //发布
                dep.notify() // 让所有的watcher的update方法执行
            }
        })
    }
}
function observe(data) {
    if (typeof data !== 'object') {
        return
    }
    return new Observe(data)
}
 
 
 
//发布订阅模式  先有订阅再有发布
 
//有一个方法,可以订阅一些事件 这些事件会放在数组里 [fn1,fn2,fn3]
 
 
function Dep() {
    this.subs = [];
}
 
//订阅的实现
Dep.prototype.addSub = function (sub) {
    this.subs.push(sub)
}
 
//发布的实现     绑定的方法,如[fn1,fn2,fn3] 都有一个update属性
Dep.prototype.notify = function () {
    this.subs.forEach(sub => sub.update())
}
 
//Watcher
function Watcher(vm, exp, fn) {
    this.fn = fn
    this.vm = vm
    this.exp = exp
 
    //添加到订阅中
    Dep.target = this  //获取当前的 watcher实例  添加到订阅中
    let val = vm
    let arr = exp.split('.')
    arr.forEach((k) => { // this.a.a  回调用 get方法
        val = val[k]
    })
    Dep.target = null
}
Watcher.prototype.update = function () {
    let val = this.vm
    let arr = this.exp.split('.')
    arr.forEach((k) => { // this.a.a  回调用 get方法
        val = val[k]
    })
    this.fn(val) //
}