Make great things

What I cannot create, I do not understand.

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

简介

一个简单的带有双向绑定的 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)
posted on 2016-09-22 20:03  wbin91  阅读(536)  评论(0编辑  收藏  举报