简介

一个简单的带有双向绑定的 MVVM 实现.

例子

使用

新建一个 ViewModel 对象, 参数分别为 DOM 元素以及绑定的数据即可.

指令

本 MVVM 的指令使用 data 数据, 即 data-html = "text" 表示这个 DOM 元素的 innerHTMl 为 model 中的 text 属性.

对某些指令还可以添加参数, 比如 data-on="reverse:click", 表示 DOM 元素添加 click 事件, 处理函数为 model 中的 reverse 属性.

  • value: 可以在 input 中使用, 只对 checkbox 进行特殊处理
  • text, html: 分别修改 innerText 和 innerHTML
  • show: 控制指定元素显示与否
  • each: 循环 DOM 元素, 每个元素绑定新的 ViewModel, 通过 $index 可以获取当前索引, $root 表示根 ViewModel 的属性
  • on: 绑定事件,
  • *: 绑定特定属性

参考

本实现主要参考 rivets.js 的 es6 分支, 其中 Observer 类是参考 adapter.js 实现.

Binding 就是 bindings.js 对应的简化, 相当于其他 MVVM 中指令, ViewModel 对应 view.js.

PS: 由于双向绑定只是简单的实现, 因此指令中的值只能是 Model 的属性

下面的代码采用 es6 实现, 如果想要本地运行的话, 请 clone git@github.com:445141126/mvvm.git, 然后执行 npm install 安装依赖, 最后 npm run dev 开启开发服务器, 浏览器中打开 http://127.0.0.1:8080/

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MVVM</title>
</head>
<body>
<div id="vm">
设置style: <input type="text" data-value="text" data-*="text: style">
<br/>
显示: <input type="checkbox" data-value="show">
<br/>
style: <span data-show="show" data-html="text"></span>
<br/>
<button data-on="reverse: click">reverse</button>
</div>
<script src="bundle.js"></script>
</body>
</html>
import _ from 'lodash'

function defined(obj) {
return !_.isUndefined(obj) && !_.isNull(obj)
} class Observer {
constructor(obj, key, cb) {
this.obj = obj
this.key = key
this.cb = cb
this.obj.$$callbacks = this.obj.$$callbacks || {}
this.obj.$$callbacks[this.key] = this.obj.$$callbacks[this.key] || [] this.observe()
} observe() {
const observer = this
const obj = observer.obj
const key = observer.key
const callbacks = obj.$$callbacks[key]
let value = obj[key] const desc = Object.getOwnPropertyDescriptor(obj, key)
if(!(desc && (desc.get || desc.set))) {
Object.defineProperty(obj, key, {
get() {
return value
},
set(newValue) {
if(value !== newValue) {
value = newValue callbacks.forEach((cb) => {
cb()
})
}
}
})
}
if(callbacks.indexOf(observer.cb) === -1) {
callbacks.push(observer.cb)
}
} unobserve() {
if(defined(this.obj.$$callbacks[this.key])) {
const index = this.obj.$$callbacks[this.key].indexOf(this.cb)
this.obj.$$callbacks[this.key].splice(index, 1)
}
} get value() {
return this.obj[this.key]
} set value(newValue) {
this.obj[this.key] = newValue
}
} class Binding {
constructor(vm, el, key, binder, type) {
this.vm = vm
this.el = el
this.key = key
this.binder = binder
this.type = type
if(_.isFunction(binder)) {
this.binder.sync = binder
} this.bind = this.bind.bind(this)
this.sync = this.sync.bind(this)
this.update = this.update.bind(this) this.parsekey()
this.observer = new Observer(this.vm.model, this.key, this.sync)
} parsekey() {
this.args = this.key.split(':').map((k) => k.trim())
this.key = this.args.shift()
} bind() {
if(defined(this.binder.bind)) {
this.binder.bind.call(this, this.el)
}
this.sync()
} unbind() {
if(defined(this.observer)) {
this.observer.unobserve()
}
if(defined(this.binder.unbind)) {
this.binder.unbind(this.this.el)
}
} sync() {
if(defined(this.observer) && _.isFunction(this.binder.sync)) {
this.binder.sync.call(this, this.el, this.observer.value)
}
} update() {
if(defined(this.observer) && _.isFunction(this.binder.value)) {
this.observer.value = this.binder.value.call(this, this.el)
}
}
} class ViewModel {
constructor(el, model) {
this.el = el
this.model = model
this.bindings = [] this.compile(this.el) this.bind()
} compile(el) { let block = false if(el.nodeType !== 1) {
return
} const dataset = el.dataset
for(let data in dataset) {
let binder = ViewModel.binders[data]
let key = dataset[data] if(binder === undefined) {
binder = ViewModel.binders['*']
} if(defined(binder)) {
this.bindings.push(new Binding(this, el, key, binder))
}
} if(!block) {
el.childNodes.forEach((childEl) => {
this.compile(childEl)
})
}
} bind() {
this.bindings.sort((a, b) => {
let aPriority = defined(a.binder) ? (a.binder.priority || 0) : 0
let bPriority = defined(b.binder) ? (b.binder.priority || 0) : 0
return bPriority - aPriority
})
this.bindings.forEach(binding => {
binding.bind()
})
} unbind() {
this.bindins.forEach(binding => {
binding.unbind()
})
}
} ViewModel.binders = {
value: {
bind(el) {
el.addEventListener('change', this.update)
}, sync(el, value) {
if(el.type === 'checkbox') {
el.checked = !!value
} else {
el.value = value
}
}, value(el) {
if(el.type === 'checkbox') {
return el.checked
} else {
return el.value
}
}
}, html: {
sync(el, value) {
el.innerHTML = value
}
}, show: {
priority: 2000,
sync(el, value) {
el.style.display = value ? '' : 'none'
}
}, each: {
block: true
}, on: {
bind(el) {
el.addEventListener(this.args[0], () => { this.observer.value() })
}
}, '*': {
sync(el, value) {
if(defined(value)) {
el.setAttribute(this.args[0], value)
} else {
el.removeAttribute(this.args[0])
}
}
}
} const obj = {
text: 'Hello',
show: false,
reverse() {
obj.text = obj.text.split('').reverse().join('')
}
}
const ob = new Observer(obj, 'a', () => {
console.log(obj.a)
})
obj.a = 'You should see this in console'
ob.unobserve()
obj.a = 'You should not see this in console' const vm = new ViewModel(document.getElementById('vm'), obj)

一个简单的 MVVM 实现的更多相关文章

  1. 230行实现一个简单的MVVM

    作者:mirone链接:https://zhuanlan.zhihu.com/p/24451202来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. MVVM这两年在前端届 ...

  2. 如何实现一个简单的MVVM框架

    接触过web开发的同学想必都接触过MVVM,业界著名的MVVM框架就有AngelaJS.今天闲来无事,决定自己实现一个简单的MVVM框架玩一玩.所谓简单,就是仅仅实现一个骨架,仅表其意,不摹其形. 分 ...

  3. 撸一个简单的MVVM例子

    我个人以为mvvm框架里面最重要的一点就是VM这部分,它要与Model层建立联系,将Model层转换成可以被View层识别的数据结构:其次也要同View建立联系,将数据及时更新到View层上,并且响应 ...

  4. JavaScript 实现一个简单的MVVM前端框架(ES6语法)

    前言 随着前端各大框架的崛起,为我们平时的开发带来了相当的便利,我们不能一直停留在应用层面,今天就自己动手实现一个乞丐版的MVVM小框架 完整代码github地址 效果 html代码 <div ...

  5. 基于vue实现一个简单的MVVM框架(源码分析)

    不知不觉接触前端的时间已经过去半年了,越来越发觉对知识的学习不应该只停留在会用的层面,这在我学jQuery的一段时间后便有这样的体会. 虽然jQuery只是一个JS的代码库,只要会一些JS的基本操作学 ...

  6. 一个简单的MVVM雏形

    这是@尚春实现的MVVM,使用定时器轮询,只支持{{}}与input.value的修改. 这只能算是一个玩具,真正的MVVM需要有更复杂的扫描机制,JS解析器,双向绑定链什么的. <!DOCTY ...

  7. 用js实现一个简单的mvvm

    这里利用的object.defineproperty() 方法; <input     id='input'><p id='p'><p/>js: const dat ...

  8. MVVM之旅(1)创建一个最简单的MVVM程序

    这是MVVM之旅系列文章的第一篇,许多文章和书喜欢在开篇介绍某种技术的诞生背景和意义,但是我觉得对于程序员来说,一个能直接运行起来的程序或许能够更直观的让他们了解这种技术.在这篇文章里,我将带领大家一 ...

  9. 【UWP开发】一个简单的Toast实现

    Toast简介 在安卓里Toast是内置原生支持,它是Android中用来显示显示信息的一种机制.它主要用于向用户显示提示消息,没有焦点,显示的时间有限,过一定的时间就会自动消失.在UWP中虽然没有原 ...

随机推荐

  1. 跟我学Windows Azure 二 使用SQL Azure创建数据库服务器,创建数据库,创建表

    登陆Windows Azure门户 输入我们上一节课所注册的帐号及密码,点击登陆. 选择SQL 数据库,选择服务器 选择创建数据库服务器 设置访问数据库服务器的登陆帐号及密码 点击确定完成数据库服务器 ...

  2. 查看哪些表的哪些列含有指定字符串(如‘andy’存在哪些表的哪些列中)

    -- 查看表中列含有指定字符.SQL> select * from demo1;NAME                                   ID---------------- ...

  3. docker-centos 7.2

    1.安装准备 预防volumes项出现Permission denied setenforce 0 #关闭selinux防火墙,临时关闭.永久关闭需改/etc/selinux/config文件,将SE ...

  4. Jmeter基础之---jmeter基础概念

    Jmeter基础之---jmeter基础概念 JMeter 介绍: 一个非常优秀的开源的性能测试工具. 优点:你用着用着就会发现它的重多优点,当然不足点也会呈现出来. JMeter 介绍: 一个非常优 ...

  5. VPS/服务器优化网络、加速方法总结与参考

    在国外的服务器上因为受各种因素影响,即使国外的服务器都是百兆共享或者G口到国内下载速度都不是很让人满意,大部分人购买国外服务器是用作存储下载或者扶墙,速度慢的所以话影响我们的使用体验.所以就搞出了很多 ...

  6. 富文本编辑器防止xss注入javascript版

    富文本编辑器:ueditor 其实富文本编辑器已经有防止xss注入功能,但是你服务端程序在接收的时候在做一次转义,否则有可能然后前端验证直接提交数据导致被xss攻击. 为了节省后端程序开销则在前端 显 ...

  7. IOS培训还值得么

    文章结构 1培训机构 各方面的评价 培训安排 收获 2 市场 就业 是否饱和 3 姿势 做好的事情 IOS这几年在IT界一直是热门的讨论话题,之前看着拉钩出品的北上广高薪岗位的人员技术流动也主要指向这 ...

  8. [Gradle]填坑记录

    1.初次打开Gradle工程特别慢,一直提示下载更新Gradle 解决办法:打开Gradle工程子目录:"\gradle\wrapper" 下的 "gradle-wrap ...

  9. 承接Unity3D外包公司 — 技术分享

    Cardboard SDK for Unity的使用 上一篇文章作为系列的开篇,主要是讲了一些虚拟现实的技术和原理,本篇就会带领大家去看一看谷歌的Cardboard SDK for Unity,虽然目 ...

  10. JS脚本修改控件宽度

    var winWidth=0;if (window.innerWidth) { winWidth = window.innerWidth; } else if ((document.body) &am ...