美文网首页
从 0.5 开始造轮子 仿 vue 的 mvvm(一)

从 0.5 开始造轮子 仿 vue 的 mvvm(一)

作者: WEB_Jorie | 来源:发表于2018-07-06 14:02 被阅读0次

    ----欢迎查看我的博客----

    从 0.5 开始造轮子

      这系列文章主要以学习为主,讲述了如何从 0.5 开始 造一个轮子,为什么是0.5因为我查了很多资料,参考了很多。至于为什么第一个是 vue 可能是参考资料比较多,在一个目前在公司的技术栈是 vue ,于是先搁置了以前的技术栈, react 。后面空闲了准备捡起react ,开始 造轮子,虽然之前造过,但是 感觉有点 low,后面再说吧。。。

    核心 -- 可爱的数据数据劫持

      数据劫持怎么理解,其实很简单。相信写过 java 的应该很容易理解。其实就是javabeen, 对对象的属性添加 set,get,操作。在js里面可以通过 Object.defineProperty 来劫持对象属性的setter和getter操作,当然 es6 里和 vue 里目前已经替换成了 Proxy ,之后我们也会替换掉 。数据劫持“种下”一个钩子,当数据发生变化触发set函数做一些操作,get时候又会触发一个钩子。
    具体看个例子吧:

    let obj = {
        name: 'mvvm'
    };
    let testname = 'vue';
    
    Object.defineProperty(obj, 'name', {
        // 1. value: '七里香',
        configurable: true,     // 2. 可以配置对象,删除属性
        // writable: true,         // 3. 可以修改对象
        enumerable: true,        // 4. 可以枚举
        // ☆ get,set设置时不能设置writable和value,它们代替了二者且是互斥的
        get() {     // 5. 获取obj.name的时候就会调用get方法
            return testname;
        },
        set(val) {      // 6. 将修改的值重新赋给name
            testname = val;   
        }
    });
    
    console.log(obj);
    /*
    {
        name: 'vue',
        set:function(val){},
        get:function(){}
    }
    */
    

    开始造轮子

    要实现mvvm的双向绑定,就必须要实现以下几点:

    1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者也就是我们说的数据劫持

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

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

    4、mvvm入口函数,实例,整合以上三者

    这篇文章中找了个图:

    image

    入口函数,轮子的开始

    看看实现过程:

        this.$options = options; // 配置挂载
        this.$el = document.querySelector(options.el); // 获取dom
        this._data = options.data;//数据挂载
        this._watcherTpl = {};//watcher池 发布订阅
        this._observer(this._data); //数据劫持
        // this._compile(dom)
        this._compile(this.$el);//渲染
    

    Observer用来数据劫持

      给数据添加 getter, setter, 并且在setter时候做一些事情,当然这里没有做深度劫持。下个章节加上。这里注意一下value,这里我们是使用 let 定义的,如果这里换成 var,就会导致对象的value被最后一个值覆盖。具体情况 百度一下 let 和 var 在循环中的区别就明白了。后续将替换为 Proxy
    查看Observer部分实现:

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

    指令解析器Compile

    由于这是个最简单的版本,所以我们暂时只考虑 v-model 和 v-bind 在 input 和 textarea 下的情况。其他情况我们后期迭代处理。
    实现情况:

    // 模板编译
    myVue.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) {
                    //attVal = data的值
                    var attVal = node.getAttribute('v-model'); // 获取绑定的data
                    //找到对应的发布订阅池
                    _this._watcherTpl[attVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
                        node,
                        _this,
                        attVal,
                        'value'
                    ));
                    return function () {
                        //触发set nodes[i].value;
                        _this._data[attVal] = nodes[key].value;  // input值改变的时候 将新值赋给数据 触发set=>set触发watch 更新视图
                    }
                })(i));
            }
    
            if (node.hasAttribute('v-bind')) { // v-bind指令
                var attrVal = node.getAttribute('v-bind'); // 绑定的data
                _this._watcherTpl[attrVal]._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
                    node,
                    _this,
                    attrVal,
                    'innerHTML'
                ))
            }
    
            var reg = /\{\{\s*([^}]+\S)\s*\}\}/g,
                txt = node.textContent;   // 正则匹配{{}}
            if (reg.test(txt)) {
                node.textContent = txt.replace(reg, (matched, attVal) => {
                    // matched匹配的文本节点包括{{}}, attVal 是{{}}中间的属性名
                    var getName = _this._watcherTpl[attVal]; // 所有绑定watch的数据
                    if (!getName._directives) { // 没有事件池 创建事件池
                        getName._directives = [];
                    }
                    getName._directives.push(new Watcher( // 将dom替换成属性的数据并发布订阅 在set的时候更新数据
                        node,
                        _this,
                        attVal,
                        'innerHTML'
                    ));
    
                    return _this._data[attVal];
                    // return attVal.split('.').reduce((val, key) => {
                    //     return _this._data[key]; // 获取数据的值 触发get 返回当前值
                    // }, _this.$el);
                });
            }
        }
    };
    

    实现Watcher

    也就是做为 Compile 和 Observer 的连接器,将dom和数据劫持联系起来。作为一个中间件。说白了就是根据一些条件更改真实 dom 的 attr。

    // new Watcher() 为this._compile()发布订阅+ 在this._observer()中set(赋值)的时候更新视图
    function Watcher(el, vm, val, attr) {
        this.el = el; // 指令对应的DOM元素
        this.vm = vm; // myVue实例
        this.val = val; // data
        this.attr = attr; // 真实dom的属性
        this.update(); // 填入数组
    }
    Watcher.prototype.update = function () {
        //dom.value = this.mvvm._data[data]
        //调用get
        this.el[this.attr] = this.vm._data[this.val]; // 获取data的最新值 赋值给dom 更新视图
    };
    

    这几段代码虽然很短可是可以多揣摩一下。总体下来其实就这些东西。

    结语

       其实核心思想大概就是这么3个模块,能实现一个小的mvvm,本文章的完整代码见:

    github完整代码

    在线例子,需要墙

    下一章 将替换我们的劫持对象 Object.defineProperty 为 Proxy

    相关文章

      网友评论

          本文标题:从 0.5 开始造轮子 仿 vue 的 mvvm(一)

          本文链接:https://www.haomeiwen.com/subject/lnnvuftx.html