zoukankan      html  css  js  c++  java
  • 一个简单的 MVVM 实现

    简介

    一个简单的带有双向绑定的 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)
    
  • 相关阅读:
    安装oracle xe一些注意点
    常用的软件设计模式的Java实现——让编程从野生到飞起
    Eclipse oxygen安装中文包
    Centos安装Redis
    Lunx下 怎样启动和关闭oracle数据库
    ORA-12537:TNS:connectionclosed错误处理过程
    启动Oracle时提示:ORA-01078:failure in processing system parameters
    Java 内存溢出(java.lang.OutOfMemoryError)的常见情况和处理方式总结
    windows下Tomcat配置多实例
    Liunx下安装jdk7
  • 原文地址:https://www.cnblogs.com/wbin91/p/5897746.html
Copyright © 2011-2022 走看看