从零实现MVVM模式的Web前端框架的雏形

3,155 阅读7分钟

TNTWeb - 全称腾讯新闻前端团队,组内小伙伴在Web前端、NodeJS开发、UI设计、移动APP等大前端领域都有所实践和积累。

目前团队主要支持腾讯新闻各业务的前端开发,业务开发之余也积累沉淀了一些前端基础设施,赋能业务提效和产品创新。

团队倡导开源共建,拥有各种技术大牛,团队Github地址:github.com/tnfe

本文作者ancai,项目地址:github.com/ancai/nvm

image.png

一、前言

本文是自己在之前的业余时间做过的一个小项目的实践总结,主要是介绍关于现代Web前端框架的设计实现思路,参考了Vue2.x版本的设计思想。

二、MVVM模式

web开发的程序员对于MVC的设计思想应该并不陌生,网上相关的文章也有很多。M(model)-V(view)-C(Controller)代表了一种程序组织架构的模式,实现了数据、视图和逻辑的有效解耦。M,表示数据状态,V代表视图界面,Controller用于处理数据和视图之间的交互逻辑。后端的web框架(如Java中的springMVC、Struts2;nodejs中的express、koa等)依然沿用此设计模式至今。 不过在前端领域,涉及到用户界面的编程,MVC的架构模式现在已经不是主流了。究其原因,主要有几方面:

  • 业务逻辑和UI混合在一起
  • 不利于扩展和复用
  • 无法验证测试
  • 增加数据的复杂性

image.png

后来微软提出了MVVM的架构模式,其中和MVC的区别之处是VM取代了C,即ViewModel(视图模型)替换了控制器。主要应用在用户界面编程的领域,提供了双向的数据绑定功能。View的变化会自动反映在ViewModel上,通过ViewModel来通知模型,数据模型Model变化时同样通过VM来关联到视图。MVVM架构模式的出现给前端的开发带来了新的生机,使得前端开发更容易编写单元测试。同时借助于ViewModel的中间桥梁,开发人员不用再关注一些DOM操作的交互细节,程序更简洁和优雅,结合组件化的编程设计思想,前端模块更易复用和拓展。 ViewModel,一般由框架来实现,主要是提供了双向数据绑定功能。 更多关于的比较推荐两篇文章:什么是MVVM框架?MVC vs MVVM: Key Differences with Examples

三、流程架构

项目里面涉及到对模板、视图、数据、指令以及依赖的管理。

image.png 从大的方面看,框架主要完成了模板和数据之间的双向绑定。使用数据劫持的相关技术,对数据的变化的进行追踪,同时引入发布订阅模式完成对属性的依赖管理。另一方面则涉及模板的解析,提取模板中的指令和表达式,关联到数据,来初始化视图。模板和数据通过watcher衔接起来,这样数据的变化直接映射到视图;同时需要监听视图上的输入相关的事件,这样当输入变化时直接映射到数据模型。

image.png 从工程处理的角度上看,可以分成四个步骤:初始化、数据监测、模板编译和框架功能。初始化的过程主要是获得调用方传递的配置信息,模型数据和模板的预处理,某些关键属性代理到上层。数据监测的环节,涉及对普通JavaScript对象的劫持处理,针对数组的增强处理,通过一个发布-订阅模式完成对数据属性的依赖注入,同时关联到watcher。模板编译环节,解析一个类似HTML的模板,提取里面的变量表达式和相关指令,通过watcher关联到数据模型。

image.png 上面给出了项目中的核心类图,Observer实现对数据的劫持,Dep相当于是一个发布-订阅器同时关联到Watcher和Observer,Compile负责解析模板,进一步提取获得Directives类同时关联到Parser和Watcher,Parser用于解析指令生产视图UI,Watcher使用到一个Batcher来批量处理更新变化。

image.png 项目代码的目录结构比较简单,src目录下总共有14个代码文件,总代码行数在千行左右,理解起来不是很困难。index.js为入口文件,模板编译相关的处理逻辑均放在compile文件夹下,observer.js文件放置数据劫持相关的处理逻辑,value.js里面是字符串类型的属性表达式转化为对象数据的处理逻辑,dep.js实现了一个简单的发布-订阅模式来关联数据状态的变化以及watcher实例,watcher.js里面有详细的注释说明在哪个环节被调用。另外,整个项目使用webpack简单构建,打包后的体积也很小。

四、数据劫持实现

image.png 利用ES5中提供的object.defineProperty 这一API,重新定义get和set属性访问器实现对数据的劫持。主要是对普通对象和数组的增强处理,监测数据的变化,通知依赖。在首次访问属性时注入依赖,通常发生在首次初始化模板时,获得状态属性的表达式,在此时注入状态属性的依赖,这里的依赖就是watcher实例;当数据发生变化时,通过dep的发布-订阅模式通知watcher实例,进而更新视图状态。

五、模板编译实现

image.png 模板的编译,没有使用虚拟DOM结点,直接从原始的html模板入手,遍历字符串模板,收集里面的指令。同时解析表达式,获得对模型数据的依赖注入,在首次加载时,会关联到watcher上。里面针对不同类型的指令,对应不同的解析处理器,分为普通指令、事件指令、双向绑定指令、if/for指令等。其中涉及到if/for的指令,需要创建新的执行上下文,关联子级的环境数据。

六、关键代码实现

depwatcher

上面两张图时关于依赖管理器和watcher的简单实现,依赖管理器可以视作一个发布-订阅模式的设计,即通过Dep类 watcher订阅了模型数据,当模型数据变化时,watcher就知道了。有面图时watcher的简单处理逻辑,当模版编译解析出表达式时,就会创建watcher的实例,同时访问模型数据的值时,将该新建的watcher实例注入进去。就这样,watcher实例充当了模版编译和模型数据的桥梁。

七、总结

本项目没有使用到virtual node(虚拟DOM结点),直接由字符串模板编译成了视图部分。其中稍微有点难理解的部分:属性表达式的提取解析、依赖的注入、if和for指令的处理、watcher的实现。 作为一个能力较弱的程序员,还无法像前端技术大牛那样创造出一个成熟的前端框架。通过本文的简单阐述,希望能帮助到有相同困惑的小伙伴加深对MVVM以及前端框架原理的理解,大牛请自动忽略。本项目的完整源码参见最后部分,给出了详细的示例。

八、代码示例

源码

示例

九、团队

TNTWeb - 腾讯新闻前端团队,TNTWeb致力于行业前沿技术探索和团队成员个人能力提升。为前端开发人员整理出了小程序以及web前端技术领域的最新优质内容,每周更新✨,欢迎star,github地址:github.com/tnfe/TNT-We…

logo.png