资讯专栏INFORMATION COLUMN

简易mvvm库的设计实现

caoym / 1220人阅读

摘要:前言模式即模式简称单项双向数据绑定的实现,让前端开发者们从繁杂的事件中解脱出来,很方便的处理数据和之间的联动。本文将从的双向数据绑定入手剖析库设计的核心代码与思路。

前言

mvvm模式即model-view-viewmodel模式简称,单项/双向数据绑定的实现,让前端开发者们从繁杂的dom事件中解脱出来,很方便的处理数据和ui之间的联动。
本文将从vue的双向数据绑定入手,剖析mvvm库设计的核心代码与思路。

需求整理与分析 整理

数据一旦改变则更新数据对应的ui

ui改变则触发事件改变ui对应的数据

分析

通过dom节点的指令获取刷新函数,用来刷新指定的ui。

实现一个桥接的方法,让刷新函数和需要的数据关联起来

监听数据变化,数据改变后通过桥接方法调用刷新函数

ui改变触发对应的dom事件在改变特定的数据

实现思路

实现observer,重新定义data,为data上每个属性增加setter,getter以监听数据的变化

实现compile,扫描模版template,提取每个dom节点中的指令信息

实现directive,通过指令信息是实例化对应的directive实例,不同类型的directive拥有不同的刷新函数update

实现watcher,让observer的属性监听函数与directive的update函数做一一对应,以实现数据变化后更新视图

模块划分

MVVM目前划分为observer,compile,directive,watcher四个模块

数据监听模块observer

通过es5规范中的object.defineProperty方式实现对数据的监听
实现思路:
递归遍历data,将data下面所有属性都加上set,get方法,以实现对所有属性的拦截.
注意:对象可能含有数组属性,数组的内置有push,pop,splice等方法改变内部数据.
此时做法是改变数组的原型链,在原型链中增加一层自定义的push,pop,splice方法做拦截,这些方法里面加上我们自己的回调函数,然后在调用原生的push,pop,splice等方法.
具体可以看我上一篇文章js对象监听实现
observer.js代码

export function Observer(obj) {
    this.$observe = function(_obj) {
        var type = Object.prototype.toString.call(_obj);
        if (type == "[object Object]") {
            this.$observeObj(_obj);
        } else if (type == "[object Array]") {
            this.$cloneArray(_obj);
        }
    };

    this.$observeObj = function(obj) {
        var t = this;
        Object.keys(obj).forEach(function(prop) {
            var val = obj[prop];
            defineProperty(obj, prop, val);
            if (prop != "__observe__") {
                t.$observe(val);
            }
        });
    };

    this.$cloneArray = function(a_array) {
        var ORP = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
        var arrayProto = Array.prototype;
        var newProto = Object.create(arrayProto);
        ORP.forEach(function(prop) {
            Object.defineProperty(newProto, prop, {
                value: function(newVal) {
                    var dep = a_array.__observe__;
                    var re=arrayProto[prop].apply(a_array, arguments);
                    dep.notify();
                    return re;
                },
                enumerable: false,
                configurable: true,
                writable: true
            });
        });
        a_array.__proto__ = newProto;
    };

    this.$observe(obj, []);
}

var addObserve = function(val) {
    if (!val || typeof val != "object") {
        return;
    }
    var dep = new Dep();
    if (isArray(val)) {
        val.__observe__ = dep;
        return dep;
    }

}

export function defineProperty(obj, prop, val) {
    if (prop == "__observe__") {
        return;
    }
    val = val || obj[prop];
    var dep = new Dep();

    obj.__observe__ = dep;
    var childDep = addObserve(val);

    Object.defineProperty(obj, prop, {
        get: function() {
            var target = Dep.target;
            if (target) {
                dep.addSub(target);
                if (childDep) {
                    childDep.addSub(target);
                }
            }
            return val;
        },
        set: function(newVal) {
            if(newVal!=val){
                val = newVal;
                dep.notify();
            }
        }
    });
}
编译模块compiler

实现思路:
1.将模版template上的dom遍历一遍,将其存入文档碎片frag
2.遍历frag,通过attributes获取节点的属性信息,在通过正则表达式过滤属性信息,进而拿到元素节点和文档节点的指令信息

 var complieTemplate = function (nodes, model) {
      if ((nodes.nodeType == 1 || nodes.nodeType == 11) && !isScript(nodes)) {
        paserNode(model, nodes);
        if (nodes.hasChildNodes()) {
          nodes.childNodes.forEach(node=> {
            complieTemplate(node, model);
          })
        }
      }
    };
    
    var paserNode = function (model, node) {
  var attributes = node.attributes || [];
  var direct_array = [];
  var scope = {
    parentNode: node.parentNode,
    nextNode: node.nextElementSibling,
    el: node,
    model: model,
    direct_array: direct_array
  };

  attributes = toArray(attributes);
  var textContent = node.textContent;
  var attrs = [];
  var vfor;

  attributes.forEach(attr => {
    var name = attr.name;
    if (isDirective(name)) {
      if (name == "v-for") {
        vfor = attr;
      } else {
        attrs.push(attr);
      }
      removeAttribute(node, name);
    }
  });

  //bug  nodeType=3
  var textValue = stringParse(textContent);
  if (textValue) {
    attrs.push({
      name: "v-text",
      value: textValue
    });
    node.textContent = "";
  }

  if (vfor) {
    scope.attrs = attrs;
    attrs = [vfor];
  }

  attrs.forEach(function (attr) {
    var name = attr.name;
    var val = attr.value;
    var directiveType = "v" + /v-(w+)/.exec(name)[1];
    var Directive = directives[directiveType];
    if (Directive) {
      direct_array.push(new Directive(val, scope));
    }
  });
};

var isDirective = function (attr) {
  return /v-(w+)/.test(attr)
};

var isScript = function isScript(el) {
  return el.tagName === "SCRIPT" && (
      !el.hasAttribute("type") ||
      el.getAttribute("type") === "text/javascript"
    )
}
指令模块directive

指令信息如:v-text,v-for,v-model等。

每种指令信息需要的初始化动作以及指令的刷新函数update都可能不一样,所以我们把它抽象出来多带带做一个模块。当然也有公用的如公共属性,统一的watcher实例化,unbind.

update函数则具体定义所属指令如何渲染ui

如简单的vtext指令的update函数如下:

vt.update = function (textContent) {
    this.el.textContent = textContent;
};
结构图

数据订阅模块watcher

watcher的功能是让directive和observer模块关联起来。
初始化的时候做两件事:

将directive模块的update函数当参数传入,并将其存入自身update属性中

调用getValue,从而获取对象data的特定属性值,进而触发一次之前在observer定义的属性函数的getter方法。

由于在defineProperty函数中定义的dep变量在setter和getter函数里有引用,使dep变量处于闭包状态没有释放,此时在getter方法中通过判断Depend.target的存在,来获取订阅者watcher,通过发布者dep储存起来。
数据的每个属性都有一个唯一的的dep变量,记录着所有订阅者watcher的信息,一旦属性有变化,调用setter函数的时候触发dep.notify(),通知所有已订阅的watcher,进而执行所有与该属性关联的刷新函数,最后更新指定的ui。

watcher 初始化部分代码:

  Depend.target = this;
  this.value = this.getValue();
  Depend.target = null;

observer.js 属性定义代码:

export function defineProperty(obj, prop, val) {
    if (prop == "__observe__") {
        return;
    }
    val = val || obj[prop];
    var dep = new Dep();

    obj.__observe__ = dep;
    var childDep = addObserve(val);

    Object.defineProperty(obj, prop, {
        get: function() {
            var target = Dep.target;
            if (target) {
                dep.addSub(target);
                if (childDep) {
                    childDep.addSub(target);
                }
            }
            return val;
        },
        set: function(newVal) {
            if(newVal!=val){
                val = newVal;
                dep.notify();
            }
        }
    });
}
流程图

简单的流程图如下:

效果图

代码地址 总结

本文基本对mvvm库的需求整理,拆分,以及对拆分模块的逐一实现来达到整体双向绑定功能的实现,当然目前市场上的mvvm库功能绝不止于此,本文只是略举个人认为的核心代码。
如果思路和实现上的问题,也请各位斧正,谢谢阅读!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/86525.html

相关文章

  • web前端mvc库实现

    摘要:如函数通过名,找到对应的数组,并触发所有数组内回调函数。核心代码如下效果图源码前端实现小节整篇文章基本是围绕着如下点,的观察者模式的实现展开,期间的销毁则取消与之有关联对象的关系,如销毁时,注销掉与之关联的的回调函数。 web前端mvc库实现 前言 随着前端应用日趋复杂,如今如angular,vue的mvvm框架,基于virtual dom的react等前端库基本成为了各个公司的首选。...

    Kylin_Mountain 评论0 收藏0
  • 【教学向】再加150行代码教你实现一个低配版的web component库(1) —设计

    摘要:为的内置一个方法,用法和原生的事件机制一毛一样。 前言 上两篇Mvvm教程的热度超出我的预期,很多码友留言表扬同时希望我继续出下一篇教程,当时我也半开玩笑说只要点赞超10就兑现承诺,没想到还真破了10,所以就有了今天的文章。 准备工作 熟读 【教学向】150行代码教你实现一个低配版的MVVM库(1)- 原理篇【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇 本篇是在...

    Clect 评论0 收藏0
  • 【教学向】150行代码教你实现一个低配版的MVVM库(2)- 代码篇

    摘要:也放出地址,上面有完整工程以及在线演示地址相关阅读教学向行代码教你实现一个低配版的库原理篇教学向行代码教你实现一个低配版的库代码篇教学向再加行代码教你实现一个低配版的库设计篇教学向再加行代码教你实现一个低配版的库原理篇 书接上一篇: 150行代码教你实现一个低配版的MVVM库(1)- 原理篇 写在前面 为了便于分模块,和阅读,我使用了Typescript来进行coding,总行数是正好...

    loonggg 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<