简介
一个简单的带有双向绑定的 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: 绑定事件,
- *: 绑定特定属性
参考
本实现主要参考 , 其中 Observer 类是参考 实现.
Binding 就是 对应的简化, 相当于其他 MVVM 中指令, ViewModel 对应 .PS: 由于双向绑定只是简单的实现, 因此指令中的值只能是 Model 的属性
下面的代码采用 es6 实现, 如果想要本地运行的话, 请 clone git@github.com:445141126/mvvm.git, 然后执行 npm install 安装依赖, 最后 npm run dev 开启开发服务器, 浏览器中打开 http://127.0.0.1:8080/
MVVM 设置style: 显示: style:
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)
posted on 2016-09-22 20:03 阅读( ...) 评论( ...)