应用 JavaScript 数据绑定完成一个简朴的 MVVM 库

MVVM 是 Web 前端一种异常盛行的开辟情势,应用 MVVM 可以使我们的代码更专注于处置惩罚营业逻辑而不是去体贴 DOM 操纵。现在有名的 MVVM 框架有 vue, avalon , angular 等,这些框架各有所长,然则完成的头脑大抵上是雷同的:数据绑定 + 视图革新。出于猎奇和一颗情愿折腾的心,我本身也沿着这个方向写了一个最简朴的 MVVM 库 ( mvvm.js ),统共 2000 多行代码,指令的定名和用法与 vue 类似,在这里分享一下完成的道理以及我的代码组织思绪。

思绪整顿

MVVM 在概念上是真正将视图与数据逻辑星散的情势,ViewModel 是悉数情势的重点。要完成 ViewModel 就须要将数据模子(Model)和视图(View)关联起来,悉数完成思绪可以简朴的总结成 5 点:

  1. 完成一个 Compiler 对元素的每一个节点举行指令的扫描和提取;

  2. 完成一个 Parser 去剖析元素上的指令,可以把指令的企图经由历程某个革新函数更新到 dom 上(中心能够须要一个特地担任视图革新的模块)比方剖析节点 <p v-show="isShow"></p> 时先获得 Model 中 isShow 的值,再依据 isShow 变动 node.style.display 从而掌握元素的显现和隐蔽;

  3. 完成一个 Watcher 能将 Parser 中每条指令的革新函数和对应 Model 的字段联系起来;

  4. 完成一个 Observer 使得可以对对象的一切字段举行值的变化监测,一旦发作变化时可以拿到最新的值并触发关照回调;

  5. 应用 Observer 在 Watcher 中竖立一个对 Model 的监听 ,当 Model 中的一个值发作变化时,监听被触发,Watcher 拿到新值后调用在步骤 2 中关联的谁人革新函数,就可以完成数据变化的同时革新视图的目的。

效果示例

起首粗看下终究的运用示例,与其他 MVVM 框架的实例化迥然差别:

<div id="mobile-list">
    <h1 v-text="title"></h1>
    <ul>
        <li v-for="item in brands">
            <b v-text="item.name"></b>
            <span v-show="showRank">Rank: {{item.rank}}</span>
        </li>
    </ul>
</div>
var element = document.querySelector('#mobile-list');
var vm = new MVVM(element, {
    'title'   : 'Mobile List',
    'showRank': true,
    'brands'  : [
        {'name': 'Apple', 'rank': 1},
        {'name': 'Galaxy', 'rank': 2},
        {'name': 'OPPO', 'rank': 3}
    ]
});

vm.set('title', 'Top 3 Mobile Rank List'); // => <h1>Top 3 Mobile Rank List</h1>

模块分别

我把 MVVM 分成了五个模块去完成: 编译模块 Compiler 、剖析模块 Parser 、视图革新模块 Updater 、数据定阅模块 Watcher 和 数据监听模块 Observer 。流程可以简述为:Compiler 编译好指令后将指令信息交给剖析器 Parser 剖析,Parser 更新初始值并向 Watcher 定阅数据的变化,Observer 监测到数据的变化然后反馈给 Watcher ,Watcher 再将变化效果关照 Updater 找到对应的革新函数举行视图的革新。

上述流程如图所示:

《应用 JavaScript 数据绑定完成一个简朴的 MVVM 库》

下文就引见下这五个模块完成的基础道理(代码只贴重点部份,完全的完成请到我的 Github 翻阅)

1. 编译模块 Compiler

Compiler 的职责主如果对元素的每一个节点举行指令的扫描和提取。由于编译和剖析的历程会屡次遍历悉数节点树,所以为了提高编译效力在 MVVM 组织函数内部先将 element 转成一个文档碎片情势的副本 fragment 编译对象是这个文档碎片而不该该是目的元素,待悉数节点编译完成后再将文档碎片增加回到本来的实在节点中。

vm.complieElement 完成了对元素一切节点的扫描和指令提取:

vm.complieElement = function(fragment, root) {
    var node, childNodes = fragment.childNodes;
    // 扫描子节点
    for (var i = 0; i < childNodes.length; i++) {
        node = childNodes[i];
        if (this.hasDirective(node)) {
            this.$unCompileNodes.push(node);
        }
        // 递归扫描子节点的子节点
        if (node.childNodes.length) {
            this.complieElement(node, false);
        }
    }
    // 扫描完成,编译一切含有指令的节点
    if (root) {
        this.compileAllNodes();
    }
}

vm.compileAllNodes 要领将会对 this.$unCompileNodes 中的每一个节点举行编译(将指令信息交给 Parser ),编译完一个节点后就从缓存行列中移除它,同时搜检 this.$unCompileNodes.length 当 length === 0 时申明悉数编译完成,可以将文档碎片追加到实在节点上了。

2. 指令剖析模块 Parser

当编译器 Compiler 把每一个节点的指令提取出来后就可以给到剖析器剖析了。每一个指令都有差别的剖析要领,一切指令的剖析要领只需做好两件事:一是将数据值更新到视图上(初始状况),二是将革新函数定阅到 Model 的变化监测中。这里以剖析 v-text 为例形貌一个指令的大抵剖析要领:

parser.parseVText = function(node, model) {
    // 获得 Model 中定义的初始值 
    var text = this.$model[model];
    // 更新节点的文本
    node.textContent = text;
    // 对应的革新函数:
    // updater.updateNodeTextContent(node, text);
    
    // 在 watcher 中定阅 model 的变化
    watcher.watch(model, function(last, old) {
        node.textContent = last;
        // updater.updateNodeTextContent(node, text);
    });
}

3. 数据定阅模块 Watcher

上个例子,Watcher 供应了一个 watch 要领来对数据变化举行定阅,一个参数是模子字段 model 另一个是回调函数,回调函数是要经由历程 Observer 来触发的,参数传入新值 last 和 旧值 old , Watcher 拿到新值后就可以找到 model 对应的回调(革新函数)举行更新视图了。model 和 革新函数是一对多的关联,即一个 model 可以有恣意多个处置惩罚它的回调函数(革新函数),比方:v-text="title"v-html="title" 两个指令共用一个数据模子字段。

增加数据定阅 watcher.watch 完成体式格局为:

watcher.watch = function(field, callback, context) {
    var callbacks = this.$watchCallbacks;

    if (!Object.hasOwnProperty.call(this.$model, field)) {
        console.warn('The field: ' + field + ' does not exist in model!');
        return;
    }

    // 竖立缓存回调函数的数组
    if (!callbacks[field]) {
        callbacks[field] = [];
    }
    // 缓存回调函数
    callbacks[field].push([callback, context]);
}

当数据模子的 field 字段发作转变时,Watcher 就会触发缓存数组中定阅了 field 的一切回调。

4. 数据监听模块 Observer

Observer 是悉数 mvvm 完成的中心基础,看过有一篇文章说 O.o (Object.observe) 将会引爆数据绑定反动,给前端带来庞大影响力,不过很可惜,ES7 草案已将 O.o 给烧毁了!现在也没有浏览器支撑!所幸的是另有 Object.defineProperty 经由历程阻拦对象属性的存取形貌符(get 和 set) 可以模仿一个简朴的 Observer :

// 阻拦 object 的 prop 属性的 get 和 set 要领
Object.defineProperty(object, prop, {
    get: function() {
        return this.getValue(object, prop);
    },

    set: function(newValue) {
        var oldValue = this.getValue(object, prop);
        if (newValue !== oldValue) {
            this.setValue(object, newValue, prop);
            // 触发变化回调
            this.triggerChange(prop, newValue, oldValue);
        }
    }
});

然后另有个题目就是数组操纵 ( push, shift 等) 该怎样监测?一切的 MVVM 框架都是经由历程重写该数组的原型来完成的:

observer.rewriteArrayMethods = function(array) {
    var self = this;
    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);
    var methods = 'push|pop|shift|unshift|splice|sort|reverse'.split('|');
    
    methods.forEach(function(method) {
        Object.defineProperty(arrayMethods, method, function() {
            var i = arguments.length;
            var original = arrayProto[method];
            
            var args = new Array(i);
            while (i--) {
                args[i] = arguments[i];
            }
            
            var result = original.apply(this, args);

            // 触发还调
            self.triggerChange(this, method);

            return result;
        });
    });
    
    array.__proto__ = arrayMethods;
}

这个完成体式格局是从 vue 中参考来的,觉得用的很妙,不过数组的 length 属性是不可以被监听到的,所以在 MVVM 中应防止操纵 array.length

5. 视图革新模块 Updater

Updater 在五个模块中是最简朴的,只须要担任每一个指令对应的革新函数即可。其他四个模块经由一系列的折腾,把末了的效果交给到 Updater 举行视图或许事宜的更新,比方 v-text 的革新函数为:

updater.updateNodeTextContent = function(node, text) {
    node.textContent = text;
}

v-bind:style 的革新函数:

updater.updateNodeStyle = function(node, propperty, value) {
    node.style[propperty] = value;
}

双向数据绑定的完成

表单元素的双向数据绑定是 MVVM 的一个最大特性之一:

《应用 JavaScript 数据绑定完成一个简朴的 MVVM 库》

实在这个奇异的功用完成道理也很简朴,要做的只要两件事:一是数据变化的时刻更新表单值,二是反过来表单值变化的时刻更新数据,如许数据的值就和表单的值绑在了一同。

数据变化更新表单值 应用前面说的 Watcher 模块很轻易就可以做到:

watcher.watch(model, function(last, old) {
    input.value = last;
});

表单变化更新数据 只须要及时监听表单的值得变化事宜并更新数据模子对应字段即可:

var model = this.$model;
input.addEventListenr('change', function() {
    model[field] = this.value;
});

其他表单 radio, checkbox 和 select 都是一样的道理。

以上,悉数流程以及每一个模块的基础完成思绪都讲完了,言语表达能力不太好,若有说的不对写的不好的处所,愿望人人可以批评指正!

结语

折腾这个简朴的 mvvm.js 是由于本来本身的框架项目顶用的是 vue.js 然则只是用到了它的指令系统,一大堆功用只用到四分之一摆布,就想着只是完成 data-binding 和 view-refresh 就够了,效果没找如许的 javascript 库,所以我本身就造了这么一个轮子。

虽然说功用和稳定性远不如 vue 等盛行 MVVM 框架,代码完成能够也比较粗拙,然则经由历程造这个轮子照样增长了许多学问的 ~ 提高在于折腾嘛!

现在我的 mvvm.js 只是完成了最本的功用,今后我会继承完美、硬朗它,若有兴致迎接一同讨论和革新~

源代码传送门: https://github.com/tangbc/sugar

    原文作者:唐朝的唐
    原文地址: https://segmentfault.com/a/1190000004847657
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞