分析Vue双向数据绑定原理以及简单实现MVVM

前言

随着Vue的火热发展,越来越多的程序员并不满足于对框架的使用,更多地追求其内在的原理,就像不能沉沦于美丽的外表,更应该追求灵魂的高度。

正文

好了,废话不多说,接下来我们将通过俩方面开展我们对外在的追求,哦不,内在的追求。

1 了解vue双向数据绑定原理

2 了解原理后,对有趣的灵魂进行一波塑造,简单实现一个MVVM框架

Vue实现双向数据绑定的做法

vue.js 是采用数据劫持结合观察者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给观察者,触发相应的监听回调。

塑造有趣的灵魂

好了,我们了解了vue双向数据后,接下来就是实现一个简单的MVVM框架,既然我们知道vue是通过数据劫持结合观察者模式实现的,那我们可以通过:

  • 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知观察者

  • 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

  • 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

  • 4、实现一个MVVM入口类,对数据进行劫持的入口

1 实现一个MVVM入口类 先实现入口类,统一入口,进而通过获取的数据进行数据劫持和指令解析

class mVue {
    constructor (options) {
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        if (this.$el) {
            // 1. 实现了一个数据观察者
            new Observer(this.$data);
            // 2. 实现一个指令解析器
            new Compile(this.$el, this);
        }
    }
}
复制代码

2 实现一个数据监听器Observer类

实例化Observer类,通过递归调用observe方法,获取data的每一个对象数据的值,并通过Object.defineProperty对数据的get/set进行劫持,在 defineReactive 方法中通过闭包的方式创建数据依赖器Dep,每个属性维护一个Dep,记录自己的观察者(即watcher),notify通知每个观察者执行相应的update方法,更新视图

那么问题来了? Observer类要如何去记录自己的观察者watcher呢?

很简单,维护一个数组subs,用来收集观察者watcher,数据变动触发notify,再调用观察者的update方法

class Observer{
    constructor (data) {
        this.observe(data);
    }
    observe (data) {
        /** 
         {
            persion: {
                name: 'fanke'
                fav:{
                    a: 'ball'
                }
            }
         }
         */
        if(data && typeof data === 'object') {
            // 对data进行数据劫持
            Object.keys(data).forEach( key => {
                this.defineReactive(data, key, data[key])
            })
        }
    }
    defineReactive (data, key, value) {
        const _this = this;
        // 递归遍历
        this.observe(value);
        const dep = new Dep(); // 数据依赖器 每个属性维护一个Dep,记录自己的观察者(即watcher),notify通知每个观察者执行相应的update方法,更新视图
        Object.defineProperty(data, key, {
            enumerable: true,    // 可枚举
            configurable: false, // 不能再define
            get() {
                // 数据变化时,往Dep中去添加观察者
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set (newValue) {
                _this.observe(newValue);
                if(newValue !== value) {
                    value = newValue;
                    // 通知数据依赖器dep,进而dep去通知观察者做出更新
                    dep.notify();
                }
            }
        })
    }
}
复制代码
class Dep {
    // 收集 + 通知
    constructor () {
        this.subs = [];
    }
    // 收集观察者
    addSub (watcher) {
        this.subs.push(watcher);
    }
    // 通知观察者去更新
    notify () {
        console.log('通知观察者', this.subs)
        this.subs.forEach( w => w.update());
    }
}
复制代码

3 实现一个Watcher Watcher观察者作为Observer和Compile之间通信的桥梁,主要做的事情是:

a、在自身实例化时往属性观察器(dep)里面添加自己

b、自身必须有一个update()方法

c、待属性变动dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调函数cb,则功成身退

// 观察者
class Watcher {
    constructor (vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 获取旧值
        this.oldVal = this.getOldVal();
    }
    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) {
            this.cb(newVal)
        }
    }
}
复制代码

4 实现一个指令解析器Compile compile主要做的事情是解析模板指令(例如v-html,v-text),将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的观察者,一旦数据有变动,收到通知,进而更新视图

class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 获取文档碎片对象,放入内存中会减少页面的回流和重绘
        const fragment = this.node2Fragment(this.el);
        // console.log(fragment)

        // 编译模板
        this.compile(fragment);

        // 追加子元素到根元素上
        this.el.appendChild(fragment);
    }
    compile (fragment) {
        // 1 获取子节点
        const childNodes = fragment.childNodes;
        [...childNodes].forEach( child => {
            // console.log(child);
            if (this.isElementNode(child)) {
                // 是元素节点
                // 编译元素节点
                // console.log('元素节点', child)
                this.compileElement(child)
            }else {
                // 文本节点
                // console.log('文本节点', child)
                this.compileText(child)
            }

            if (child.childNodes && child.childNodes.length) {
                this.compile(child);
            }
        })
    }
    compileElement (node) {
        // <div v-text="msg"></div>
        const attributes = node.attributes;
        // console.log(attributes); 
        [...attributes].forEach(attr => {
            const {name, value} = attr; // name=v-text | value = msg || name=@click value=handleClick
            // console.log(name);
            if (this.isDirective(name)) { // 是指令 v-text v-model v-html v-on:click
                const [, directive] = name.split("-"); // text, model, html, on:click
                const [dirName, eventName] = directive.split(":"); // dirName -> text, model, html, on
                // 更新数据 数据驱动视图
                compileUtil[dirName](node, value, this.vm, eventName);
                // 删除有指令的标签上的属性
                node.removeAttribute('v-' + directive)
            }else if(this.isEventDirective(name)){ // 判断是否是@等监听事件
                const [, eventName] = name.split("@"); // eventDirective = click
                compileUtil['on'](node, value, this.vm, eventName);
                // 删除有指令的标签上的属性
                node.removeAttribute('@' + eventName)
            }
        })
    }
    compileText (node) {
        // 编译 {{}}
        const content = node.textContent;
        // console.log("content", content);
        if(/\{\{(.+?)\}\}/.test(content)){
            // console.log(content);
            compileUtil['text'](node, content, this.vm);
        }
    }
    // 判断是否是Vue的指令
    isDirective (attrName) {
        return attrName.startsWith("v-")
    }
    // 判断指令是否是事件 
    isEventDirective (attrName) {
        return attrName.startsWith("@")
    }
    // 判断是否是节点
    isElementNode (node) {
        return node.nodeType === 1;
    }
    node2Fragment (el) {
        // 创建文档碎片对象
        const f = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            f.append(firstChild);
        }
        return f;
    }
}
复制代码

总结

好了,这样我们就大概塑造了一个有趣的灵魂了, 这里我们主要对双向数据绑定原理的实现,但对于模板的更新只是简单的进行DOM操作,实际在在Vue源码中是通过虚拟VDOM + diff算法 结合更新队列去实现的,小伙伴有兴趣的话,可以深入Vue源码查看

这时候可能会有一些同学会有一些有趣的问题,例如:

1 发布订阅模式为何和观察者模式如此相似?

这里简单说一下俩者的区别:

a 观察者模式里,只有两个角色 —— 观察者 + 被观察者,发布订阅模式里存在着第三个中介

b 观察者和被观察者,是松耦合的关系。 发布者和订阅者,则完全不存在耦合

2 如何查看完整的代码? 请猛戳这里

猜你喜欢

转载自juejin.im/post/7075569849178521608