博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
简易mvvm库的设计实现
阅读量:6434 次
发布时间:2019-06-23

本文共 6328 字,大约阅读时间需要 21 分钟。

前言

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等方法.
具体可以看我上一篇文章
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库功能绝不止于此,本文只是略举个人认为的核心代码。

如果思路和实现上的问题,也请各位斧正,谢谢阅读!

转载地址:http://lmhga.baihongyu.com/

你可能感兴趣的文章
Maven搭建简单的SS项目
查看>>
#我要上首页# 新版博客首页来了,做明星博主还会远吗?
查看>>
PHP缓存技术
查看>>
关于SOCKET资源堆栈
查看>>
笔记 百度搜索
查看>>
Kebernetes 学习总结(9)认证-授权-RBAC
查看>>
控制台 - 网络管理之华为交换机 S系列端口限速
查看>>
天下会 - 搜索实战系列之视频
查看>>
修改windows远程登录端口
查看>>
ccflow表结构与运行机制(二次开发必读)
查看>>
mysql数据库引擎调优
查看>>
101 Tips to MySQL Tuning and Optimization
查看>>
对mysql explain讲的比较清楚的
查看>>
上币至iamToken
查看>>
我的友情链接
查看>>
破解sina新浪邮箱密码
查看>>
linux为启动菜单加密码
查看>>
MySQL5.5编译方式安装实战
查看>>
细谈Ehcache页面缓存的使用
查看>>
每天一个linux命令(3):pwd命令
查看>>