MMVM模式并不是很新的东西,怎么实现网上也是有一大堆文章,虽然最后都能实现,但是过程感觉还是比较复杂,不够简练,为了便于自己更好理解,基于现有的一些技术手段,结合MVVM的思想,自己从0到1实现了这样一个功能,接下来将呈现我的整个一个思考过程
关于MVVM(Model-View-ViewModel)的概念这里就不做赘述了,相信用过Vue以及React的同学都已经非常熟悉,那么我们直接进入正题
视图模板
视图模板对应View视图层,是一个包含了一些指令信息的一个视图呈现,里面不处理任何逻辑,参考Vue,写下这样一个模板,以及实例化操作
<div id="mvvm">
<input type="text" y-model="msg">
<p>{{msg}}</p>
<button y-click="handleClik">点我</button>
</div>
const vm = new Mvvm({
el: '#mvvm',
data: {
msg: 'hello world!'
},
methods: {
handleClik() {
this.msg = 'hahhahahha'
}
},
})
这里是一个逆向的逻辑,首先我们先想象自己想要的一个模板呈现,可以是字符串、html节点、jsx等,这里我们用最简单的html来呈现,后面就是基于模板上的一些特殊指令(这里是带有特殊标识的字符串),这里我们需要实现如下功能,y-model这个指令绑定的值要响应式的呈现在input框中以及在进行输入交互时要更新到Model数据层、{{msg}}需要在数据更新时同步到p标签的文本节点,y-click这个指令标识在点击button时触发绑定的函数
数据模板
接下来是数据层(Model)的实现,数据的操作无非增删改查,这里我们主要是对取值(查)以及赋值(改)操作进行处理,我们希望在做这两个操作的时候自动的进行一些事件处理程序,而不是每次手动执行,那么就需要的这两个操作进行代理或者拦截,es5的实现方式是用过Object.defineProperty这个方法,那么在es6提供了Proxy方法,这里我们选择Proxy来实现
这里我们声明一个叫做Observer的类,传入我们的数据对象,并返回数据对象的代理
class Observer {
constructor (data) {
this._data = new Proxy(data, {
get: function(target, propkey) {
return Reflect.get(target, propkey)
},
set: function(target, propkey, value) {
Reflect.set(target, propkey, value)
return true
}
})
}
}
这里用到了es6的Reflect将取值与赋值改写为函数行为
因为我们在之前的模板中同时在input和p中都绑定了msg这个变量,我们在输入框改变msg时希望p中的msg也能够改变,这就好比msg被观察了,任何关于msg的变动所有观察者都能够接收响应并执行对应操作,说简单的就是我们需要实现一个观察者模式来处理这种情况,关于观察者的实现网上也有很详细的解释,这里我们就直接使用
class Dep {
constructor() {
this.subs = []
}
add(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
class Watcher {
constructor(vm,cb) {
this.vm = vm
this.cb = cb
cb.call(vm)
}
update() {
this.cb.call(this.vm)
}
}
这里说明一下,由于我们所有的数据处理都是跟实例挂钩的,所以我们传入一个上下文对象vm来指向实例,以便于内部的函数执行能正确访问到对应变量
那么我们的Observer现在要实例化一个观察者并添加到vm对象上
class Observer {
constructor (data, vm) {
vm.dep = new Dep()
vm._data = new Proxy(data, {
get: function(target, propkey) {
return Reflect.get(target, propkey)
},
set: function(target, propkey, value) {
Reflect.set(target, propkey, value)
vm.dep.notify()
return true
}
})
}
}
现在,我们的vm对象上添加了_data这个被代理的数据对象以及一个dep对象,里面保存着_data的观察者
视图数据层
接下来是viewModel层的实现,这里要实现的逻辑是处理传入的视图模板,解析模板指令,并绑定数据同时需要实现我们一开始写模板时想要实现的一些功能,这里我们也用类来实现,叫做Compile
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
this.init()
}
init() {
[].slice.call(this.el.children).forEach(node => {
let text = node.textContent
let reg = /\{\{(.*)\}\}/
if (reg.test(text)) {
let key = reg.exec(text)[1]
this.vm.dep.add(new Watcher(this.vm, () => node.innerText = this.vm[key]))
}
if (node.getAttribute('y-model')) {
let modelKey = node.getAttribute('y-model')
node.addEventListener('input', e => {
this.vm[modelKey] = e.target.value || ''
})
this.vm.dep.add( new Watcher(this.vm, () => node.value = this.vm[modelKey] ) )
node.removeAttribute('y-model')
}
if (node.getAttribute('y-click')) {
let eventKey = node.getAttribute('y-click')
node.addEventListener('click', this.vm[eventKey].bind(this.vm), false)
node.removeAttribute('y-click')
}
})
}
}
这里我们需要传入一个元素,也就是我们一开始模板里的id="mvvm"这个元素对象,并对其包含的子元素进行了遍历处理,在init这个方法里,我们分别对我们一开始先要实现的功能进行了处理,这里需要注意的地方就是我们在梳理{{msg}}以及y-model这两个指令分别实例化了Watcher并传入的需要执行的事件处理程序,并在Observer阶段添加的dep上添加了Watcher示例,这样在进行赋值操作的时候,所有绑定了对应变量的地方都会响应到变化并更新视图
结合Dep、Watcher、Observer、Compile这四个类以及可以基本实现我们一开始想要的功能了,接下来我们开始使用
const Mvvm = function(options) {
this.$options = options
const el = document.querySelector(this.$options.el)
new Observer(this.$options.data, this)
new Compile(el, this)
}
这里我们声明一个构造函数,传入基本配置,这样基本已经可以使用了,但是请注意我们在Compile传入的vm对象,在这里是构造函数的this,我们在init的方法中调用变量的时候都是直接在this这个对象上查找的,但实际我们传入的this对象上并不一定都有这些变量,我们把传入的配置添加到了this.$options上,dep还是直接添加在了this上。所以并不是Compile所有的变量都能在this上直接访问到,如果先要实现上面这种写法,那么我们需要在Compile示例化之前再做一层处理
此处实现有两种方式,Object.defineProperty和Proxy,因为我们是一个基于Proxy的实现,还是用Proxy来实现,我们的Mvvm构造函数相应变成
const Mvvm = function(options) {
this.$options = options
const el = document.querySelector(this.$options.el)
const vm = new Proxy(this, {
get(target, propKey) {
return Reflect.get(Reflect.has(target, propKey) ? target : Reflect.has(target._data, propKey) ? target._data : target.$options.methods, propKey)
},
set(target, propkey, value) {
Reflect.set(target._data, propkey, value)
return true
}
})
new Observer(this.$options.data, this)
new Compile(el, vm)
}
我们先对this对象做了一层代理并返回代理对象vm,在get方法中处理查值操作,此时我们传入Compile中的不再是this,而是其代理对象vm,这样我们再直接在vm上对变量进行操作了
完整代码
<div id="mvvm">
<input type="text" y-model="msg">
<p>{{msg}}</p>
<button y-click="handleClik">点我</button>
</div>
<script>
class Dep {
constructor() {
this.subs = []
}
add(sub) {
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => sub.update())
}
}
class Watcher {
constructor(vm,cb) {
this.vm = vm
this.cb = cb
cb.call(vm)
}
update() {
this.cb.call(this.vm)
}
}
class Observer {
constructor (data, vm) {
vm.dep = new Dep()
vm._data = new Proxy(data, {
get: function(target, propkey) {
return Reflect.get(target, propkey)
},
set: function(target, propkey, value) {
if ( target[propkey] === value ) return true
Reflect.set(target, propkey, value)
vm.dep.notify()
return true
}
})
}
}
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
this.init()
}
init() {
[].slice.call(this.el.children).forEach(node => {
let text = node.textContent
let reg = /\{\{(.*)\}\}/
if (reg.test(text)) {
let key = reg.exec(text)[1]
this.vm.dep.add(new Watcher(this.vm, () => node.innerText = this.vm[key]))
}
if (node.getAttribute('y-model')) {
let modelKey = node.getAttribute('y-model')
node.addEventListener('input', e => {
this.vm[modelKey] = e.target.value || ''
})
this.vm.dep.add( new Watcher(this.vm, () => node.value = this.vm[modelKey] ) )
node.removeAttribute('y-model')
}
if (node.getAttribute('y-click')) {
let eventKey = node.getAttribute('y-click')
node.addEventListener('click', this.vm[eventKey].bind(this.vm), false)
node.removeAttribute('y-click')
}
})
}
}
const Mvvm = function(options) {
this.$options = options
const el = document.querySelector(this.$options.el)
const vm = new Proxy(this, {
get(target, propKey) {
return Reflect.get(Reflect.has(target, propKey) ? target : Reflect.has(target._data, propKey) ? target._data : target.$options.methods, propKey)
},
set(target, propkey, value) {
Reflect.set(target._data, propkey, value)
return true
}
})
new Observer(this.$options.data, this)
new Compile(el, vm)
}
const vm = new Mvvm({
el: '#mvvm',
data: {
msg: 'hello world!'
},
methods: {
handleClik() {
this.msg = 'hahhahahha'
}
},
})
</script>
当然这只是一个简单的不能再简单的MVVM模式,也没有对其中的步骤做任何优化,但是我们这里重点要呈现的是一个思路,体现自己的思考过程