一个只有十行的精简MVVM框架
本文来自网易云社区。
前言
MVVM模式相信做前端的人都不陌生,去网上搜MVVM,会出现一大堆关于MVVM模式的博文,但是这些博文大多都只是用图片和文字来进行抽象的概念讲解,对于刚接触MVVM模式的新手来说,这些概念虽然能够读懂,但是也很难做到理解透彻。因此,我写了这篇文章。
这篇文章旨在通过代码的形式让大家更好的理解MVVM模式,相信大多数人读了这篇文章之后再去看其他诸如regular、vue等基于MVVM模式框架的源码,会容易很多。
如果你对MVVM模式已经很熟悉并且也已经研读过并深刻理解了当下主流的前端框架,可以忽略下面的内容。如果你没有一点JavaScript基础,也请先去学习下再来阅读读此文。
引子
来张图来镇压此文:
MVVM
是Model-View-ViewModel
的缩写。简单的讲,它将View
与Model
层分隔开,利用ViewModel
层将Model
层的数据经过一定的处理变成适用于View
层的数据结构并传送到View
层渲染界面,同时View
层的视图更新也会告知ViewModel
层,然后ViewModel
层再更新Model
层的数据。
我们用一段学生信息的代码作为引子,然后一步步再重构成MVVM模式的样子。
编写类似下面结构的学生信息:
Name: Jessica Bre
Height: 1.8m
Weight: 70kg
用常规的js代码是这样的:
- const student = {
- 'first-name': 'Jessica',
- 'last-name': 'Bre',
- 'height': 180,
- 'weight': 70,
- }
- const root = document.createElement('ul')
- const nameLi = document.createElement('li')
- const nameLabel = document.createElement('span')
- nameLabel.textContent = 'Name: '
- const name_ = document.createElement('span')
- name_.textContent = student['first-name'] + ' ' + student['last-name']
- nameLi.appendChild(nameLabel)
- nameLi.appendChild(name_)
- const heightLi = document.createElement('li')
- const heightLabel = document.createElement('span')
- heightLabel.textContent = 'Height: '
- const height = document.createElement('span')
- height.textContent = '' + student['height'] / 100 + 'm'
- heightLi.appendChild(heightLabel)
- heightLi.appendChild(height)
- const weightLi = document.createElement('li')
- const weightLabel = document.createElement('span')
- weightLabel.textContent = 'Weight: '
- const weight = document.createElement('span')
- weight.textContent = '' + student['weight'] + 'kg'
- weightLi.appendChild(weightLabel)
- weightLi.appendChild(weight)
- root.appendChild(nameLi)
- root.appendChild(heightLi)
- root.appendChild(weightLi)
- document.body.appendChild(root)
好长的一堆代码呀!别急,下面我们一步步优化!
DRY一下如何
程序设计中最广泛接受的规则之一就是“DRY”: "Do not Repeat Yourself"。很显然,上面的一段代码有很多重复的部分,不仅与这个准则相违背,而且给人一种不舒服的感觉。是时候做下处理,来让这段学生信息更"Drier"。
可以发现,代码里写了很多遍document.createElement
来创建节点,但是由于列表项都是相似的结构,所以我们没有必要一遍一遍的写。因此,进行如下封装:
- const createListItem = function (label, content) {
- const li = document.createElement('li')
- const labelSpan = document.createElement('span')
- labelSpan.textContent = label
- const contentSpan = document.createElement('span')
- contentSpan.textContent = content
- li.appendChild(labelSpan)
- li.appendChild(contentSpan)
- return li
- }
经过这步转化之后,整个学生信息应用就变成了这样:
- const student = {
- 'first-name': 'Jessica',
- 'last-name': 'Bre',
- 'height': 180,
- 'weight': 70,
- }
- const createListItem = function (label, content) {
- const li = document.createElement('li')
- const labelSpan = document.createElement('span')
- labelSpan.textContent = label
- const contentSpan = document.createElement('span')
- contentSpan.textContent = content
- li.appendChild(labelSpan)
- li.appendChild(contentSpan)
- return li
- }
- const root = document.createElement('ul')
- const nameLi = createListItem('Name: ', student['first-name'] + ' ' + student['last-name'])
- const heightLi = createListItem('Height: ', student['height'] / 100 + 'm')
- const weightLi = createListItem('Weight: ', student['weight'] + 'kg')
- root.appendChild(nameLi)
- root.appendChild(heightLi)
- root.appendChild(weightLi)
- document.body.appendChild(root)
是不是变得更短了,也更易读了?即使你不看createListItem
函数的实现,光看const nameLi = createListItem('Name: ', student['first-name'] + ' ' + student['last-name'])
也能大致明白这段代码时干什么的。
但是上面的代码封装的还不够,因为每次创建一个列表项,我们都要多调用一遍createListItem
,上面的代码为了创建name,height,weight
标签,调用了三遍createListItem
,这里显然还有精简的空间。因此,我们再进一步封装:
- const student = {
- 'first-name': 'Jessica',
- 'last-name': 'Bre',
- 'height': 180,
- 'weight': 70,
- }
- const createList = function(kvPairs){
- const createListItem = function (label, content) {
- const li = document.createElement('li')
- const labelSpan = document.createElement('span')
- labelSpan.textContent = label
- const contentSpan = document.createElement('span')
- contentSpan.textContent = content
- li.appendChild(labelSpan)
- li.appendChild(contentSpan)
- return li
- }
- const root = document.createElement('ul')
- kvPairs.forEach(function (x) {
- root.appendChild(createListItem(x.key, x.value))
- })
- return root
- }
- const ul = createList([
- {
- key: 'Name: ',
- value: student['first-name'] + ' ' + student['last-name']
- },
- {
- key: 'Height: ',
- value: student['height'] / 100 + 'm'
- },
- {
- key: 'Weight: ',
- value: student['weight'] + 'kg'
- }])
- document.body.appendChild(ul)
有没有看到MVVM风格的影子?student
对象是原始数据,相当于Model
层;createList
创建了dom
树,相当于View
层,那么ViewModel
层呢?仔细观察,其实我们传给createList
函数的参数就是Model
的数据的改造,为了让Model
的数据符合View
的结构,我们做了这样的改造,因此虽然这段函数里面没有独立的ViewModel
层,但是它确实是存在的!聪明的同学应该想到了,下一步就是来独立出ViewModel
层了吧~
- // Model
- const tk = {
- 'first-name': 'Jessica',
- 'last-name': 'Bre',
- 'height': 180,
- 'weight': 70,
- }
- //View
- const createList = function(kvPairs){
- const createListItem = function (label, content) {
- const li = document.createElement('li')
- const labelSpan = document.createElement('span')
- labelSpan.textContent = label
- const contentSpan = document.createElement('span')
- contentSpan.textContent = content
- li.appendChild(labelSpan)
- li.appendChild(contentSpan)
- return li
- }
- const root = document.createElement('ul')
- kvPairs.forEach(function (x) {
- root.appendChild(createListItem(x.key, x.value))
- })
- return root
- }
- //ViewModel
- const formatStudent = function (student) {
- return [
- {
- key: 'Name: ',
- value: student['first-name'] + ' ' + student['last-name']
- },
- {
- key: 'Height: ',
- value: student['height'] / 100 + 'm'
- },
- {
- key: 'Weight: ',
- value: student['weight'] + 'kg'
- }]
- }
- const ul = createList(formatStudent(tk))
- document.body.appendChild(ul)
这看上去更舒服了。但是,最后两行还能封装~
- const smvvm = function (root, {model, view, vm}) {
- const rendered = view(vm(model))
- root.appendChild(rendered)
- }
- smvvm(document.body, {
- model: tk,
- view: createList,
- vm: formatStudent
- })
这种写法,熟悉vue或者regular的同学,应该会觉得似曾相识吧?
让我们来加点互动
前面学生信息的身高的单位都是默认m
,如果新增一个需求,要求学生的身高的单位可以在m
和cm
之间切换呢?
首先需要一个变量来保存度量单位,因此这里必须用一个新的Model:
- const tk = {
- 'first-name': 'Jessica',
- 'last-name': 'Bre',
- 'height': 180,
- 'weight': 70,
- }
- const measurement = 'cm'
为了让tk
更方便的被其他模块重用,这里选择增加一个measurement
数据源,而不是直接修改tk
。
在视图部分要增加一个radio单选表单,用来切换身高单位。
一个只有十行的精简MVVM框架的更多相关文章
- 一个只有十行的精简MVVM框架(下篇)
本文来自网易云社区. 让我们来加点互动 前面学生信息的身高的单位都是默认m,如果新增一个需求,要求学生的身高的单位可以在m和cm之间切换呢? 首先需要一个变量来保存度量单位,因此这里必须用一个新的Mo ...
- 一个只有十行的精简MVVM框架(上篇)
本文来自网易云社区. 前言 MVVM模式相信做前端的人都不陌生,去网上搜MVVM,会出现一大堆关于MVVM模式的博文,但是这些博文大多都只是用图片和文字来进行抽象的概念讲解,对于刚接触MVVM模式的新 ...
- ViewModel从未如此清爽 - 轻量级WPF MVVM框架Stylet
Stylet是我最近发现的一个WPF MVVM框架, 在博客园上搜了一下, 相关的文章基本没有, 所以写了这个入门的文章推荐给大家. Stylet是受Caliburn Micro项目的启发, 所以借鉴 ...
- 实现一个类 Vue 的 MVVM 框架
Vue 一个 MVVM 框架.一个响应式的组件系统,通过把页面抽象成一个个组件来增加复用性.降低复杂性 主要特色就是数据操纵视图变化,一旦数据变化自动更新所有关联组件~ 所以它的一大特性就是一个数据响 ...
- 迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC
大凡出名的MVC,MVVM框架都有todo例子,我们也搞一下看看avalon是否这么便宜. 我们先从react的todo例子中扒一下HTML与CSS用用. <!doctype html> ...
- 如何实现一个简单的MVVM框架
接触过web开发的同学想必都接触过MVVM,业界著名的MVVM框架就有AngelaJS.今天闲来无事,决定自己实现一个简单的MVVM框架玩一玩.所谓简单,就是仅仅实现一个骨架,仅表其意,不摹其形. 分 ...
- 剖析手写Vue,你也可以手写一个MVVM框架
剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...
- 迷你MVVM框架 avalonjs 1.3.7发布
又到每个月的15号了,现在avalon已经固定在每个月的15号发布新版本.这次发布又带来许多新特性,让大家写码更加轻松,借助于"操作数据即操作DOM"的核心理念与双向绑定机制,现在 ...
- Vue.js-----轻量高效的MVVM框架(一、初识Vue.js)
1.什么是Vue.js? 众所周知,最近几年前端发展非常的迅猛,除各种框架如:backbone.angular.reactjs外,还有模块化开发思想的实现库:sea.js .require.js .w ...
随机推荐
- PHP-----TP框架基础
TP框架基础 把Thinkphp框架的压缩包解压到php文件夹下----www目录下. Thinkphp文件夹都有什么呢??? (所有用TP框架做的程序,要访问程序里面的网页.内容的话,全部要走这 ...
- PHP-------文件上传的实例
文件上传的实例 一.上传头像 在注册时,要求用户上传头像,登陆主页面,显示用户信息时,头像也要显示出来. 例子: 注册页面: 图片存数据库时是text类型. <title>无标题文档< ...
- Kali-linux安装并配置NVIDIA显卡驱动
显卡驱动程序就是用来驱动显卡的程序,它是硬件所对应的软件.驱动程序即添加到操作系统中的一小块代码,其中包含有关硬件设备的信息.有了此信息,计算机就可以与设备进行通信.驱动程序是硬件厂商根据操作系统编写 ...
- git的一些小命令
git_cmd git常用命令 <>代表变量,例如 代表分支名称 远程库 查看远程库信息 git remote -v 查看远程仓库:$ git remote -v 添加远程仓库:$ git ...
- Servlet是线程安全的吗
Servlet默认是单例模式,在web容器中只创建一个实例,所以多个线程同时访问Servlet是不安全的. 解决此类问题的办法是: 只要Servlet实现 SingleThreadModel 接口
- Redis通过IO进行序列化+反序列化
必须引用序列化Serializable接口 创建类:Role package com.wbg.springRedis.entity; import java.io.Serializable; publ ...
- Swift_类型选择
Swift_类型选择 点击查看源码 //类型选择 func test() { class MediaItem { } class Movie: MediaItem { } class Song: Me ...
- 学习笔记(1)centos7 下安装nginx
学习笔记(1)centos7 下安装nginx 这里我是通过来自nginx.org的nginx软件包进行安装的. 1.首先为centos设置添加nginx的yum存储库 1.通过vi命令创建一个rep ...
- 并发、并行与C++多线程——基础一
1.什么是并发? 并发指的是两个或多个独立的活动在同一时段内发生.生活中并发的例子并不少,例如在跑步的时候你可能同时在听音乐:在看电脑显示器的同时你的手指在敲击键盘.这时我们称我们大脑并发地处理这些事 ...
- 竞赛题解 - Karp-de-Chant Number(BZOJ-4922)
Karp-de-Chant Number(BZOJ-4922) - 竞赛题解 进行了一次DP的练习,选几道题写一下博客~ 标签:BZOJ / 01背包 / 贪心 『题目』 >> There ...