从 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容