MMVM mode is not very new things, how to achieve online is also a lot of articles, although finally can achieve, but the process is still more complex, not concise, in order to facilitate their better understanding, based on some existing technical means, combined with the idea of MVVM, their own from 0 to 1 to achieve such a function, What follows is my entire thought process
The model-view-viewModel (MVVM) concept will not be repeated here, I believe that those of you who have used Vue and React are already familiar with it, so let’s get straight to the topic
The view template
The View template corresponds to the View layer, which is a rendering of a View that contains some command information, no logic, reference Vue, write such a template, and instantiate operations
<div id=" MVVM "> <input type="text" y-model=" MSG "> <p>{{MSG}}</p> <button y-click="handleClik"> </div> const vm = new Mvvm({ el: '#mvvm', data: { msg: 'hello world! ' }, methods: { handleClik() { this.msg = 'hahhahahha' } }, })Copy the code
Here is a reverse logic, first we think they want a template, can be a string, HTML node, JSX, here we use the most simple HTML to render some special instructions on the back is based on template (here with special identification string), here we need to implement the following function, The value bound by the y-model directive is rendered in the input box in a responsive manner and updated to the model data layer during input interaction. {{MSG}} needs to be synchronized to the text node of the P label when the data is updated. The y-click directive identifies the function that triggers the binding when the button is clicked
Data templates
Next is the realization of the data layer (Model), the operation of the data only add and delete, here we mainly and assign values (check), and (to) operation for processing, we hope to do the two operations automatically when some event handlers, but not every time we performed manually, you need to the two operations agent or intercept, Object. DefineProperty is used in ES5, so es6 provides a Proxy method, here we choose Proxy to implement
Here we declare a class called Observer, pass in our data object, and return the proxy for the data object
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
}
})
}
}
Copy the code
Here we use es6’s Reflect to rewrite values and assignments as function behavior
Since we have bound the variable MSG in input and P in the previous template, we hope that the MSG in P can also be changed when the input box changes MSG. This is just like the MSG is observed, and any change in MSG can be received by all observers and the corresponding operation can be performed. The simple thing is that we need to implement an observer pattern to handle this situation. The implementation of the observer is well explained on the web, so we’ll use it directly
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)
}
}
Copy the code
As a note, since all of our data processing is tied to the instance, we pass in a context object VM to point to the instance so that the internal function execution has proper access to the corresponding variable
So our Observer will now instantiate an Observer and add it to the VM object
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
}
})
}
}
Copy the code
Now we add _data, the proxied data object, and a DEP object that holds the observer of _data to our VM object
View data layer
The next step is to implement the viewModel layer, where the logic to implement is to process the incoming view template, parse the template instructions, and bind the data and implement some of the functionality that we wanted to implement when we first wrote the template, which we also implement with a class called 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')
}
})
}
}
Copy the code
So what we’re doing here is we’re passing in an element, the element object id=” MVVM “that we started with in our template, and we’re going to iterate over the child elements that it contains, and in init, we’re going to do the same thing that we started with, Note that {{MSG}} and y-model instantiate the Watcher and pass in the event handler that needs to be executed, and add the Watcher example to the DEP that was added in the Observer phase. All places that bind the corresponding variable respond to the change and update the view
Dep, Watcher, Observer, Compile these four classes and can basically achieve what we want at the beginning, then we start to use
const Mvvm = function(options) {
this.$options = options
const el = document.querySelector(this.$options.el)
new Observer(this.$options.data, this)
new Compile(el, this)
}
Copy the code
Here we declare a constructor, pass in the basic configuration, and the basic is ready to use, but notice that when we Compile the vm object that we pass in, in this case the constructor’s this, we look up this directly when we call variables in our init method, We don’t always pass these variables to this. We add the configuration to this.$options, and we add the deP to this. Not all of Compile’s variables are directly accessible on this. If we want to implement this, we need to do another layer of processing before Compile is instantiated
There are two ways to implement this, Object.defineProperty and Proxy. Since we are a proxy-based implementation or a Proxy implementation, our Mvvm constructor becomes
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)
}
Copy the code
We first do a layer of proxy on this object and return the proxy object VM, in the get method to process the value lookup operation, now we pass in this object vm instead of this, so we can directly operate on the variable on the VM
The complete code
< div id = "MVVM" > < input type = "text" - model y = "MSG" > < p > {{MSG}} < / p > < button - click y = "handleClik" > me < / 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>Copy the code
Of course, this is just a simple MVVM mode, which could not be simpler, and there is no optimization of the steps, but what we want to present here is an idea, reflect their own thinking process