Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现一个Vue双向绑定 #14

Open
rico-c opened this issue Dec 16, 2018 · 0 comments
Open

实现一个Vue双向绑定 #14

rico-c opened this issue Dec 16, 2018 · 0 comments
Labels

Comments

@rico-c
Copy link
Owner

rico-c commented Dec 16, 2018

根据Vue官方文档图片,Vue的双向绑定原理如下:

下面上代码:

首先是HTML代码

    <div id="app">
        <div>
            <input type="text" v-model="testData">
            <p>{{ testData }}</p>
        </div>
    </div>

下面是Vue.js的实现:

    var app = new Vue({ 
        el: '#app', 
        data: {
            testData: 
        }
    })

    function Vue(options = {}) {  
        this.$options = options; 
        this.$el = document.querySelector(options.el); // 获取DOM
        this._data = options.data; // 获取data
        this._watcherTpl = {}; // watcher池
        this._observer(this._data); // 重写data的set和get以双向绑定
        this._compile(this.$el); // 传入dom,编译模板
    };

    // 重写data的get和set
    Vue.prototype._observer = function (obj) {
        var _this = this;
        Object.keys(obj).forEach(key => { 
            _this._watcherTpl[key] = { // 每个数据的订阅池()
                _directives: []
            };
            var value = obj[key]; 
            var watcherTpl = _this._watcherTpl[key]; 
            Object.defineProperty(_this._data, key, { // 重写数据的set get
                configurable: true,  // 可以删除
                enumerable: true, // 可以遍历
                get() {
                    return value; // 获取值的时候,直接返回
                },
                set(newVal) { // 改变值的时候 触发set
                    if (value !== newVal) {
                        value = newVal;
                        watcherTpl._directives.forEach((item) => { // 遍历订阅池 
                            item.update();
                            // 遍历所有订阅的地方(v-model+v-bind+{{}}) 触发this._compile()中发布的订阅Watcher 更新视图  
                        });
                    }
                }
            })
        });
    }

    // 模板编译
    Vue.prototype._compile = function (el) {
        var _this = this, nodes = el.children; // 获取app的dom
        for (var i = 0, len = nodes.length; i < len; i++) { // 遍历dom节点
            var node = nodes[i];
            if (node.children.length) {
                _this._compile(node);  // 递归深度遍历dom树
            }

            // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件    
            if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
                node.addEventListener('input', (function (key) {
                    var attVal = node.getAttribute('v-model'); // 获取v-model绑定的值
                    _this._watcherTpl[attVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
                        node,
                        _this,
                        attVal,
                        'value'
                    ));
                    return function () {
                        _this._data[attVal] = nodes[key].value;  // input值改变的时候 将新值赋给数据 触发set=>set触发watch 更新视图
                    }
                })(i));
            }

            var reg = /\{\{\s*([^}]+\S)\s*\}\}/g, txt = node.textContent;   // 正则匹配{{}}
            if (reg.test(txt)) {
                node.textContent = txt.replace(reg, (matched, placeholder) => {
                    // matched匹配的文本节点包括{{}}, placeholder 是{{}}中间的属性名
                    var getName = _this._watcherTpl; // 所有绑定watch的数据
                    getName = getName[placeholder];  // 获取对应watch 数据的值
                    if (!getName._directives) { // 没有事件池 创建事件池
                        getName._directives = [];
                    }
                    getName._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
                        node,
                        _this,
                        placeholder,
                        'innerHTML'
                    ));

                    return placeholder.split('.').reduce((val, key) => {
                        return _this._data[key]; // 获取数据的值 触发get 返回当前值 
                    }, _this.$el);
                });
            }
        }
    }

    // new Watcher() 为this._compile()发布订阅+ 在this._observer()中set(赋值)的时候更新视图
    function Watcher(el, vm, val, attr) {
        this.el = el; // 指令对应的DOM元素
        this.vm = vm; // myVue实例
        this.val = val; // 指令对应的值 
        this.attr = attr; // dom获取值,如value获取input的值 / innerHTML获取dom的值
        this.update(); // 更新视图
    }
    Watcher.prototype.update = function () {
        this.el[this.attr] = this.vm._data[this.val]; // 获取data的最新值 赋值给dom 更新视图
    }
@rico-c rico-c added the 笔记 label Dec 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant