阅读 143

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

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

从 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

评论