简单实现VUE双向数据绑定

前言

VUE是当下比较热门的一个前端框架,其显著特点就是双向数据绑定,即data更新view,view更新data,但其具体实现一直模模糊糊,这次就来搞个明明白白。

核心原理

其核心原理就是数据劫持,数据劫持是用Object.definePorperty()来实现的,看代码

let data = {}
let num = 0;
Object.defineProperty(data, 'num', {
    enumerable: true,
    configurable: true,
    get() {
        return num;
    },
    set(v) {
        num = v;
    }
});

每当获取data['num']的值时就会触发它的get方法,设置data['num']的值时就会触发它的set方法,这样就实现了数据劫持,一旦数据发生一些改变,就可以监听到,然后去实现一些自定义的功能,例如:更新view

第一步,确定初始化方式

就模仿VUE的初始化好了,首先我们写HTML

{{info}}

接着,对它进行初始化

    let ymvue = new YMVue({
        el: '#app',
        data: {
            info: 'hello world'
        },
        methods: {
            mounted() {
                console.log(this)
            },
            btnClick() {
                this.hello = 'Hello Vue'
            }
        }
    });

第二步,生成观察者

VUE用的是观察者模式,观察者的主要功能就是数据劫持,那我们定义一个观察者Guard,当我们初始化一个YMVue对象之后,就把它交给观察者Guard,然后遍历它的data集合,并对里面的数据进行劫持。

    function Guard(obj) {
        this.obj = obj;   // YMVue对象
        this.start(obj.data);
    }

    Guard.prototype = {
        start(data) {
            if (data && typeof data === 'object') {
                Object.keys(data).forEach((key) => {
                    this.addGuard(data, key, data[key]);
                });
            }
        },
        addGuard(data, key, val) {
            let self = this;
            this.start(data[key]);
            // let dep = new Dep();
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get() {
                    //if (Dep.target) {
                    //    dep.addSub(Dep.target);
                    //}
                    return val;
                },
                set(v) {
                    if (val === v) {
                        return;
                    }
                    val = v;
                    //dep.update();
                }
            })
        }
    };

上面代码中注释代码仅作暂时注释,待下面进行说明

第三步,生成订阅者

如果给观察者传入一个回调函数callback,那么在触发set方法后执行回调,好像是实现了所有功能。
但是,but,一个数据的更新可能会触发无数个方法,如果通过给观察者传入回调,那就必须对同一个数据进行多次劫持,那自然是不得行了,所以需要个订阅者,把数据更新后带来的连锁反应订阅到这个数据上去。

    function BindCallbackToGuard(obj, name, callback) {
        this.obj = obj;             // YMVue对象
        this.callback = callback;   // 回调函数
        this.name = name;           // 订阅的数据
        this.value = this.get();
    }

    BindCallbackToGuard.prototype = {
        excute() {
            let val = this.obj.data[this.name];
            let oldVal = this.value;
            if (val !== oldVal) {
                this.value = val;
                this.callback.call(this.obj, val, oldVal);
            }
        },
        get() {
            Dep.target = this;                    // 把自己设为订阅对象 
            let value = this.obj.data[this.name]; // 触发观察者的get函数
            Dep.target = null;                    
            return value;
        }
    };

第四步,创建一个订阅者容器

第二步和第三步的代码中都有一个Dep,那Dep到底是什么呢
Dep其实是一个订阅者容器,每当有一个订阅者订阅了自己,观察者就把它放进订阅者容器里面,然后当观察者监听到数据有变的时候,就去遍历订阅者容器,然后执行每个订阅者订阅的方法。

    function Dep() {
        this.subs = []
    }

    Dep.prototype = {
        addSub(sub) {
            this.subs.push(sub);
        },
        update() {
            this.subs.forEach((sub) => {   // sub是一个订阅者
                sub.excute();
            })
        }
    };

这时候,就可以取消第二步代码中的注释了

第五步,生成DOM解析器

上面我们已经把功能都搞定了,现在就需要把那些特殊的HTML跟这些代码联系到一起,这时候就需要一个DOM解析器,其中对DOM元素的操作我们用到了文档碎片Fragment

    function Analysis(containerId, obj) {
        this.obj = obj;    // YMVue对象
        this.dom = document.querySelector(containerId);
        this.fragment = null;
        this.init();
    }

    Analysis.prototype = {
        init() {
            if (this.dom) {
                this.fragment = this.switchToFragment(this.dom);     // 将NODE节点转换为文档碎片
                this.analysisElement(this.fragment);            // 开始解析
                this.dom.appendChild(this.fragment);            // 用文档碎片替换node节点
            } else {
                console.log('Dom 元素不存在');
            }
        },
        switchToFragment() {
            let fragment = document.createDocumentFragment();
            let child = this.dom.firstChild;
            while (child) {
                fragment.append(child);
                child = this.dom.firstChild;
            }
            return fragment;
        },
        analysisElement(dom) {
            let childNodes = dom.childNodes;
            [].slice.call(childNodes).forEach((node) => {
                let reg = /\{\{(.*)\}\}/;
                let text = node.textContent;

                if (this.isElementNode(node)) {      // 元素节点
                    this.analysisAttr(node);
                } else if (this.isTextNode(node) && reg.test(text)) {  // 文本节点
                    this.bindText(node, reg.exec(text)[1]);
                }

                if (node.childNodes && node.childNodes.length) {
                    this.analysisElement(node);
                }
            });
        },
        analysisAttr(node) {
            let nodeAttrs = node.attributes;
            Array.prototype.forEach.call(nodeAttrs, (attr) => {
                let name = attr.name;          // 属性名称
                if (this.isCommand(name)) {    // 属性名以'v-'开头
                    let value = attr.value;    // 属性值
                    let command = name.substring(2);
                    if (this.isEventCommand(command)) {  // 事件指令,比如v-on:click
                        this.bindEvent(node, value, command);
                    } else { // v-model 指令
                        this.bindModel(node, value)
                    }
                    node.removeAttribute(name);  // 移除后,页面上不显示
                }
            })
        },

        bindText(node, name) {
            let text = this.obj[name];
            node.textContent = text || '';
            // 添加一个订阅者
            new BindCallbackToGuard(this.obj, name, function (v) {
                node.textContent = v;
            });

        },
        bindEvent(node, name, command) {
            let eventType = command.split(':')[1];
            let method = this.obj.methods && this.obj.methods[name];
            if (eventType && method) {
                node.addEventListener(eventType, method.bind(this.obj), false);
            }
        },
        bindModel(node, name) {
            let self = this;
            let text = this.obj[name];
            node.value = text || '';
            
            // 添加一个订阅者
            new BindCallbackToGuard(this.obj, name, function (v) {
                node.value = v;
            });
            // 监听input事件,当它的value改变时,同时更新其绑定值
            node.addEventListener('input', function (e) {
                let newVal = e.target.value;
                if (text === newVal) {
                    return;
                }
                self.obj[name] = newVal;
                text = newVal;
            }, false);
        },
        isCommand(attr) {
            return attr.indexOf('v-') === 0;
        },
        isEventCommand(dir) {
            return dir.indexOf('on:') === 0;
        },
        isElementNode(node) {
            return node.nodeType === 1;
        },
        isTextNode(node) {
            return node.nodeType === 3;
        }
    };

第六步,定义初始化类

现在所有的功能基本完成,就差一个初始类YMVue

    function YMVue(opts) {
        this.data = opts.data;
        this.methods = opts.methods || {};

        Object.keys(this.data).forEach((key) => {
            this.proxy(key);      // 设置代理
        });
        new Guard(this);           // 初始化观察者
        new Analysis(opts.el, this);     // dom解析
        this.methods.mounted && this.methods.mounted.call(this);  // 初始化完成后,执行mounted方法
    }

    YMVue.prototype = {
        proxy(key) {  
            let self = this;
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return self.data[key];
                },
                set(v) {
                    self.data[key] = v;
                }
            })
        }
    };

初始化类有一个代理proxy,它有什么用呢?
因为VUE改变的数据的操作是this.info='xxxx',但是this指向的是VUE对象,info又在data集合里,所以应该是this.data.info='xxx',那取消中间的data就需要用到代理。

结束

OK,大功告成,虽然功能距离真正的VUE还差的远,但是简单的数据双向绑定就这么实现了,写成一个插件,随便在哪都可以用起来。
完整代码在这

你可能感兴趣的:(简单实现VUE双向数据绑定)