响应式原理(MVVM)

一、如何理解MVVM模式

mvvm模式字面可理解为数据-视图-数据驱动,本质上就是MVC模式在前端的体现,而vue正是运用这种模式,看vue源码可了解到,vue核心即采用数据劫持结合发布者-订阅者模式,通过ES5中Object.defineProperty()的特性来劫持各个属性的setter,getter,在数据变动时发消息给订阅者,触发对应watcher的回调,以致于view更新的效果。这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

二、总体实现思路

在这里插入图片描述
借助上图来理解会更清晰,首先Mvue类接收一个参数对象作为初始输入,接收data,一方面在Observer观察者中,运用Object.defineProperty()方法把data数据对象里属性全部转成setter、getter方法,并初始化各自的订阅器Dep(用来存放watcher);另一方面创建Compile指令解析器,作用是对每个元素节点进行解析,替换模板数据,调用updater更新view(同时也是生成 虚拟dom 到真实dom的过程),与此同时对data的每个属性绑定一个Watcher订阅者,并将各个Watcher push到对应的Dep订阅器中; 当data中的属性被改变时,触发setter,并在对应的Dep容器中调用Dep.notify()通知订阅者Watcher,订阅者收到消息后,将要更新的watcher依赖push到更新队列中,等在下一个事件循环中刷新队列执行自身的update方法,来触发render进而更新view;而对于双向绑定,无非是运用input表单的addEventListener事件,来更新data以至于达到更新view的目的。

三、上代码

  • index.js
        let vm = new MVVM({
            el: '#app',
            data: {
                person: {
                    name: '大神',
                    age: 18,
                    fav: 'film'
                },
                msg: 'msg的内容',
                htmlStr: '<h3>这是v-html</h3>'
            }
  • MVVM实例
class MVVM {
    constructor(options) {
        // 初始元素与数据通过options对象绑定
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        // 通过Compiler对象对模版进行编译,例如{{}}插值、v-text、v-html、v-model等Vue语法
        if (this.$el) {
            // 1. 创建观察者
            new Observer(this.$data);
            // 2. 编译模版
            new Compiler(this.$el, this);
            // 3. 通过数据代理实现 this.person.name = '海贼王——路飞'功能,而不是this.$data.person.name = '海贼王——路飞'
            this.proxyData(this.$data);
        }
    }
    //用vm代理vm.$data
    proxyData(data) {
        for (let key in data) {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(newVal) {
                    data[key] = newVal;
                }
            })
        }
    }
}

  • 编译HTML模版对象
class Compiler {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 1. 将预编译的元素节点放入文档碎片对象中,避免DOM频繁的回流与重绘,提高渲染性能  ---vue中采用虚拟dom
        const fragments = this.node2fragments(this.el);
        // 2. 编译模版
        this.compile(fragments);
        // 3. 追加子元素到根元素
        this.el.appendChild(fragments);
    }
    compile(fragments) {
        // 1.获取子节点
        const childNodes = fragments.childNodes;
        // 2.递归循环编译
        [...childNodes].forEach(child => {
            // 如果是元素节点
            if (this.isElementNode(child)) {
                this.compileElement(child);
            } else {
                // 文本节点
                this.compileText(child);
            }
            //递归遍历
            if (child.childNodes && child.childNodes.length) {
                this.compile(child);
            }
        })
    }
    compileElement(node) {
        let attributes = node.attributes;
        // 对于每个属性进行遍历编译
        // attributes是类数组,因此需要先转数组
        [...attributes].forEach(attr => {
            let { name, value } = attr; // v-text="msg"  v-html=htmlStr  type="text"  v-model="msg"
            if (this.isDirector(name)) { // v-text  v-html  v-mode  v-bind  v-on:click v-bind:href=''
                let [, directive] = name.split('-');
                let [compileKey, detailStr] = directive.split(':');
                // 更新数据,数据驱动视图
                compileUtil[compileKey](node, value, this.vm, detailStr);
                // 删除有指令的标签属性 v-text v-html等,普通的value等原生html标签不必删除
                node.removeAttribute('v-' + directive);
            }
        })
    }
    compileText(node) {
        // 编译文本中的{{person.name}}--{{person.age}}
        const content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
            compileUtil['text'](node, content, this.vm);
        }
    }
rector(attrName) {
        // 判断是否为Vue特性标签
        return attrName.startsWith('v-');
    }
    node2fragments(el) {
        // 创建文档碎片对象
        const f = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            f.appendChild(firstChild);
        }
        return f;
    }
    isElementNode(node) {
        // 元素节点的nodeType属性为 1
        return node.nodeType === 1;
    }
}

// 编译模版具体执行
const compileUtil = {
    getValue(expr, vm) {
        // 处理 person.name 这种对象类型,取出真正的value
        return expr.split('.').reduce((data, currentVal) => {
            return data[currentVal];
        }, vm.$data)
    },
    setVal(expr, vm, inputValue) {
        expr.split('.').reduce((data, currentVal) => {
            data[currentVal] = inputValue;
        }, vm.$data)
    },
    getContent(expr, vm) {
        // {{person.name}}--{{person.age}}
        // 防止修改person.name使得所有值全部被替换
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(args[1], vm);
        });
    },
    text(node, expr, vm) {
        let value;
        if (expr.indexOf('{{') !== -1) {
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
                // text的 Watcher应在此绑定,
                // Watcher的构造函数的 getOldVal()方法需要接受数据或者对象,而{{person.name}}不能接收
                new Watcher(args[1], vm, () => {
                   //更新view
                    this.updater.textUpdater(node, this.getContent(expr, vm));
                });
                return this.getValue(args[1], vm);
            });
        } else {
            value = this.getValue(expr, vm);
        }
        this.updater.textUpdater(node, value);
    },
    html(node, expr, vm) {
        let value = this.getValue(expr, vm);
        // html对应的 Watcher
        new Watcher(expr, vm, (newVal) => {
            this.updater.htmlUpdater(node, newVal);
        })
        this.updater.htmlUpdater(node, value);
    },
    model(node, expr, vm) {
        const value = this.getValue(expr, vm);
        // v-model绑定对应的 Watcher, 数据驱动视图
        new Watcher(expr, vm, (newVal) => {
            this.updater.modelUpdater(node, newVal);
        });
        // 视图 => 数据 => 视图
        node.addEventListener('input', (e) => {
            this.setVal(expr, vm, e.target.value);
        })
        this.updater.modelUpdater(node, value);
    },
    // 视图更新函数
    updater: {
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value) {
            node.innerHTML = value;
        },
        modelUpdater(node, value) {
            node.value = value;
        }
    }

}
  • 定义观察者
class Observer {
    constructor(data) {
        this.observe(data);
    }
    // data是一个对象,可能嵌套其它对象,需要采用递归遍历的方式进行观察者绑定
    observe(data) {
        if (data && typeof data === 'object') {
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key]);
            })
        }
    }
    // 通过 object.defineProperty方法对对象属性进行劫持
    defineReactive(obj, key, value) {
        // 递归观察
        this.observe(value);
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            get() {
                // 首次取data属性时拦截,收集依赖,Dep.target为对应的Watcher,并往Dev中添加订阅者
                Dep.target && dep.addWatcher(Dep.target);
                return value;
            },
            // 采用箭头函数在定义时绑定this的定义域
            set: (newVal) => { //数据更新即触发
                if (value === newVal) return;
                this.observe(newVal);
                value = newVal;
                //  通知对应watcher数据发生改变
                dep.notify();
            }
        })
    }
}

  • 如何实现一个 watcher
// 订阅者
class Watcher {
    // 通过回调函数实现更新的数据通知到视图
    constructor(expr, vm, cb) {
        this.expr = expr;
        this.vm = vm;
        this.cb = cb;
        this.oldVal = this.getOldVal();    // 获取旧数据
    }
  // 在初始化new Watcher时触发,即在利用getValue()获取数据调用getter()方法时立即把当前订阅者挂载,收集到Dep容器中
    getOldVal() {
        Dep.target = this;
        const oldVal = compileUtil.getValue(this.expr, this.vm);
        // 挂载完毕需要注销,防止重复挂载
        Dep.target = null;
        return oldVal;
    }
    // 通过回调函数更新数据
    update() {
        const newVal = compileUtil.getValue(this.expr, this.vm);
        if (newVal !== this.oldVal) {
       //若在vue中,这个回调将调用render函数,会返回一个newVnode,进而调用patch(oldVnode,newVnode)函数来更新dom
            this.cb(newVal);
        }
    }
}
  • Dep实例
// Dep类 容器  收集watcher对象,并在数据变化时通知watcher
class Dep {
    constructor() {
        this.watcherCollector = [];
    }
    // 添加watcher
    addWatcher(watcher) {
        this.watcherCollector.push(watcher);
    }
    // 数据变化时通知watcher更新
    notify() {
        this.watcherCollector.forEach(w => w.update());
    }
}
四、总结

以上便是对mvvm设计模式及vue的双向绑定原理做的概要解析,主要还是对 Watcher, Observer , Dep 的关系做梳理,
Observer, 观察者,用来观察数据源变化.
Dep, 观察者和订阅者是典型的 一对多 的关系,所以这里设计了一个依赖中心,来管理某个观察者和所有这个观察者对应的订阅者的关系, 消息调度和依赖管理都靠它。
Watcher, 订阅者,当某个观察者观察到数据发生变化的时候,这个变化经过消息调度中心,最终会传递到所有该观察者对应的订阅者身上,然后这些订阅者分别执行自身的业务回调即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值