JS源码分析│简易mvvm库的设计实现

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

作者:刀哥(朱建)

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

1、需求整理与分析

需求:

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

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

分析:

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

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

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

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

2、实现思路

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

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

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

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

3、模块划分

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

4、数据监听模块observer

通过es5规范中的object.defineProperty方式实现对数据的监听。

5、实现思路

递归遍历data,将data下面所有属性都加上set,get方法,以实现对所有属性的拦截.

注意:对象可能含有数组属性,数组的内置有push,pop,splice等方法改变内部数据.

此时做法是改变数组的原型链,在原型链中增加一层自定义的push,pop,splice方法做拦截,这些方法里面加上我们自己的回调函数,然后在调用原生的push,pop,splice等方法。

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();

        }

    }

});
}

6、编译模块compiler

实现思路:

  • 将模版template上的dom遍历一遍,将其存入文档碎片frag

  • 遍历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);

  })

}
}

};

7、指令模块directive

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

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

update函数则具体定义所属指令如何渲染ui,如简单的vtext指令的update函数如下:

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

9、结构图


9、数据订阅模块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();

        }

    }

});
}

10、流程图

11、总结

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

原代码:https://github.com/laughing-pic-zhu/mvvm

想要深入了解的同学可以访问数澜社区,和大家一起讨论学习~

相关文章
|
3月前
|
JavaScript 前端开发 Python
用python执行js代码:PyExecJS库
文章讲述了如何使用PyExecJS库在Python环境中执行JavaScript代码,并提供了安装指南和示例代码。
162 1
用python执行js代码:PyExecJS库
|
1月前
|
移动开发 JavaScript 前端开发
一些处理浏览器兼容性问题的JavaScript库
这些库在处理浏览器兼容性问题方面都有着各自的特点和优势,可以根据具体的需求和项目情况选择合适的库来使用,从而提高代码的兼容性和稳定性,为用户提供更好的体验。同时,随着浏览器技术的不断发展,还需要持续关注和学习新的兼容性解决方案。
106 48
|
1月前
|
JavaScript 前端开发
Moment.js与其他处理时间戳格式差异的JavaScript库相比有什么优势?
Moment.js与其他处理时间戳格式差异的JavaScript库相比有什么优势?
|
1月前
|
CDN
如何在项目中使用Moment.js库?
如何在项目中使用Moment.js库?
|
1月前
|
JavaScript 前端开发
如何在不影响性能的前提下使用JavaScript库来实现复杂的动画效果?
如何在不影响性能的前提下使用JavaScript库来实现复杂的动画效果?
|
1月前
|
算法 开发者
Moment.js库是如何处理不同浏览器的时间戳格式差异的?
总的来说,Moment.js 通过一系列的技术手段和策略,有效地处理了不同浏览器的时间戳格式差异,为开发者提供了一个稳定、可靠且易于使用的时间处理工具。
37 1
|
1月前
|
JavaScript 前端开发 搜索推荐
Moment.js、Day.js、Miment,日期时间库怎么选?
【10月更文挑战第29天】如果你需要一个功能强大、插件丰富的日期时间库,并且对性能要求不是特别苛刻,Moment.js是一个不错的选择;如果你追求极致的轻量级和高性能,那么Day.js可能更适合你;而如果你有一些特定的日期时间处理需求,并且希望在性能和功能之间取得平衡,Miment也是可以考虑的。
|
1月前
|
存储 JavaScript 前端开发
decimal.js库的安装和使用方法
【10月更文挑战第24天】decimal.js 是一个非常实用的高精度计算库,通过合理的安装和使用,可以在 JavaScript 中实现精确的数值计算和处理。你可以根据具体的需求和项目情况,灵活运用该库来解决数字精度丢失的问题。
|
2月前
|
JavaScript 前端开发 开发者
jQuery:JavaScript库的瑰宝
jQuery:JavaScript库的瑰宝
58 3
|
3月前
|
JavaScript 前端开发
常用的 JavaScript 框架和库
常用的 JavaScript 框架和库
157 7