Recently, I watched Everest’s architecture class-Implementing an MVVM.
First, let’s understand what MVVM is.
MVVM is shorthand for Model-View-ViewModel. It is essentially an improved version of MVC. MVVM abstracts the state and behavior of the View, allowing us to separate the view UI from the business logic. Of course, the ViewModel has done these things for us. It can take out the data of the Model and help process the business logic involved in the View due to the need to display content.
Post the code first, and then analyze it.
DOCTYPE html>
<html lang="en">
<head >
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title >myMVVMtitle>
head>
<body >
<div id="root">
<input type="text" v-model='person.name' >
<h1 >{{message}}h1>
<ul >
<li >1li>
<li >2li>
ul>
{{person.age}}
<br />
<button v-on:click="change">Click mebutton>
<br />
{{getNewName}}
<div v-html="message"> div>
div>
<script src="Vue.js">script>
<script >
let vm = new Vue({
el: '#root',
data: {
message: 'she is a bad girl',
person: {
age: 20,
name: 'zhangsan'
}
},
computed: {
getNewName() {
return this.person.name + ' hahaha'
}
},
methods: {
change() {
console.log("11111")
}
}
})
script>
body>
html>
Simple implementation of MVVM (there are many functions not involved)
/**
* Subscribe to publish dispatch center
*/
class Dep {
constructor() {
this.subs = [] // Store all watchers
}
// add watcher, subscribe
addSub(watcher) {
this.subs.push(watcher)
}
// Publish
notify() {
this.subs.forEach(watcher => watcher.update() )
}
}
/**
* Observer Mode Observer, Observed
*/
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// The old value is stored first by default
this.oldValue = this.get(vm, expr)
}
// Get the old value
get() {
Dep.target = this
let value = CompileUtil.getVal(this.vm, this span>.expr)
Dep.target = null
return value
}
// Data update
update() {
let newVal = CompileUtil.getVal(this.vm, this.expr)
if(newVal != this.oldValue) {
this.cb(newVal)
}
}
}
/**
* Realize data hijacking
*/
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
if(data && typeof data =='object ') {
for(let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
defineReactive(obj, key, value) {
this.observe(value)
let dep = new Dep() //< span style="color: #008000;"> Add a publish/subscribe function to each attribute
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newValue) => {
if(newValue != value) {
this.observe(newValue)
value = newValue
dep.notify()
}
}
})
}
}
/**
* Compile template
*/
class Compiler {
constructor(el, vm) {
this.vm = vm
// Determine whether el is an element, and if not, get it< /span>
this.el = this.isElementNode(el) ? el: document.querySelector(el)
// console.log(this.el)
// Put the elements in the current node into memory
let fragment = this.node2fragment(this.el)
// Replace the content in the node
// Compile template Compile with data
this.compile(fragment)
// cram the content back into the page
this.el.appendChild(fragment)
}
// Is it at the beginning of the instruction v-
isDirective(attrName) {
// startsWith('v-') or split('- ')
return attrName.indexOf(‘v-‘) !== -1
}
// Compile elements
compileElement(node) {
let attributes = node.attributes
// [...attributes] or Array.from equivalent Array.prototype.slice.call
Array.from(attributes).forEach(attr => {
let {name, value: expr} = attr
if(this.isDirective(name)) {
//
let [, directive] = name.split(‘-‘)
// console.log(name,node, expr, directive)
if(directive.indexOf(‘:‘ !== -1)) {
let [directiveName, eventName] = directive.split(‘:‘)
CompileUtil[directiveName](node, expr, this.vm, eventName )
} else {
CompileUtil[directive](node, expr, this.vm)
}
}
})
}
// Compiled text find {{}}
compileText(node) {
let content = node.textContent
if(/\{\{(.+?)\}\}/.test(content)) {
// console.log(content) // find all text< /span>
CompileUtil[‘text‘](node, content, this.vm)
}
}
// Compiling method of dom node core in memory span>
compile(node) {
let childNodes = node.childNodes
Array.from(childNodes).forEach(child => {
if(this.isElementNode(child)) {
this.compileElement(child)
// If it’s an element, you need to remove yourself before traversing Child node
this.compile(child)
} else {
this.compileText(child)
}
})
}
// move the node to memory appendChild method
node2fragment(node) {
let fragment = document.createDocumentFragment()
let firstChild
while(firstChild = node.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
// Determine whether it is an element node
isElementNode(node) {
return node.nodeType === 1
}
}
/**
* Compiler tool functions
*/
CompileUtil = {
// Get the corresponding data according to the expression
getVal(vm, expr) {
return expr.split('.').reduce((data, current) => {
return data[current]
}, vm.$data)
},
getContentVal(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (... args) => {
return this.getVal(vm, args[1 ])
})
},
setVal(vm, expr, value) {
expr.split(‘.‘).reduce((data, current, index, arr) => {
if(index === arr.length-1) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
// Parse v-model instructions
model(node, expr, vm) {
// node.value
let fn = this.updater[‘modelUpdater‘]
new Watcher(vm, expr, (newVal) => {/ / Add an observer to the input box, update the data later, trigger this method, and assign a value to the input box with the new value
fn(node, newVal)
})
node.addEventListener(‘input’, e => {
let value = e.target.value
this.setVal(vm, expr, value)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
on(node, expr, vm, eventName) {
node.addEventListener(eventName, e => {
vm[expr].call(vm, e)
})
},
text(node, expr, vm) {
let fn = this.updater[‘textUpdater‘]
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// Add observers to each variable in the expression< /span>
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr)) / / returns a full string
})
return this.getVal(vm, args[1 ])
})
fn(node, content)
},
html(node, expr, vm) {
// node.innerHTML
let fn = this.updater[‘htmlUpdater‘]
new Watcher(vm, expr, (newVal) => {/ / Add an observer to the input box, update the data later, trigger this method, and assign a value to the input box with the new value
fn(node, newVal)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
updater: {
modelUpdater(node, value) {
node.value = value
},
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
}
}
}
/**
* Vue constructor
*/
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
let computed = options.computed
let methods = options.methods
if(this.$el) {
// Do data hijacking
new Observer(this.$data)
// console.log(this.$data)
for(let key in computed) {// Dependencies
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this< span style="color: #000000;">)
}
})
}
for(let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]
}
})
}
// All value operations on vm are proxy on vm.$ data on
this.proxyVm(this.$data)
// Compile template
new Compiler(this.$el, this)
}
}
// Agency
proxyVm(data) {
for(let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
}
}
}
To be continued…
< /p>
DOCTYPE html>
<html lang="en">
<head >
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title >myMVVMtitle>
head>
<body >
<div id="root">
<input type="text" v-model='person.name' >
<h1 >{{message}}h1>
<ul >
<li >1li>
<li >2li>
ul>
{{person.age}}
<br />
<button v-on:click="change">Click mebutton>
<br />
{{getNewName}}
<div v-html="message">div>
div>
<script src="Vue.js">script>
<script>
let vm = new Vue({
el: ‘#root‘,
data: {
message: ‘she is a bad girl‘,
person: {
age: 20,
name: ‘zhangsan‘
}
},
computed: {
getNewName() {
return this.person.name + ‘hahaha‘
}
},
methods: {
change() {
console.log("11111")
}
}
})
script>
body>
html>
/**
* 订阅发布 调度中心
*/
class Dep {
constructor() {
this.subs = [] // 存放所有的watcher
}
// 添加watcher, 订阅
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
/**
* 观察者模式 观察者,被观察者
*/
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 默认先存放旧值
this.oldValue = this.get(vm, expr)
}
// 获取旧的值
get() {
Dep.target = this
let value = CompileUtil.getVal(this.vm, this.expr)
Dep.target = null
return value
}
// 数据更新
update() {
let newVal = CompileUtil.getVal(this.vm, this.expr)
if(newVal != this.oldValue) {
this.cb(newVal)
}
}
}
/**
* 实现数据劫持
*/
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
if(data && typeof data == ‘object‘) {
for(let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
defineReactive(obj, key, value) {
this.observe(value)
let dep = new Dep() // 给每一个属性都加上一个具有发布订阅的功能
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newValue) => {
if(newValue != value) {
this.observe(newValue)
value = newValue
dep.notify()
}
}
})
}
}
/**
* 编译模板
*/
class Compiler {
constructor(el, vm) {
this.vm = vm
// 判断el是否是个元素,如果不是就获取它
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// console.log(this.el)
// 把当前节点中的元素放到内存中
let fragment = this.node2fragment(this.el)
// 把节点中的内容进行替换
// 编译模板 用数据来编译
this.compile(fragment)
// 把内容再塞回页面中
this.el.appendChild(fragment)
}
// 是否是指令 v-开头的
isDirective(attrName) {
// startsWith(‘v-‘) 或 split(‘-‘)
return attrName.indexOf(‘v-‘) !== -1
}
// 编译元素
compileElement(node) {
let attributes = node.attributes
// [...attributes] 或 Array.from 等价 Array.prototype.slice.call
Array.from(attributes).forEach(attr => {
let { name, value: expr } = attr
if(this.isDirective(name)) {
//
let [, directive] = name.split(‘-‘)
// console.log(name,node, expr, directive)
if(directive.indexOf(‘:‘ !== -1)) {
let [directiveName, eventName] = directive.split(‘:‘)
CompileUtil[directiveName](node, expr, this.vm, eventName)
} else {
CompileUtil[directive](node, expr, this.vm)
}
}
})
}
// 编译文本 找{{}}
compileText(node) {
let content = node.textContent
if(/\{\{(.+?)\}\}/.test(content)) {
// console.log(content) // 找到所有文本
CompileUtil[‘text‘](node, content, this.vm)
}
}
// 编译内存中的dom节点 核心的编译方法
compile(node) {
let childNodes = node.childNodes
Array.from(childNodes).forEach(child => {
if(this.isElementNode(child)) {
this.compileElement(child)
// 如果是元素的话,需要除去自己,再去遍历子节点
this.compile(child)
} else {
this.compileText(child)
}
})
}
// 把节点移动到内存中 appendChild方法
node2fragment(node) {
let fragment = document.createDocumentFragment()
let firstChild
while(firstChild = node.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
// 判断是否为元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
/**
* 编译工具函数
*/
CompileUtil = {
// 根据表达式取对应的数据
getVal(vm, expr) {
return expr.split(‘.‘).reduce((data, current) => {
return data[current]
}, vm.$data)
},
getContentVal(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1])
})
},
setVal(vm, expr, value) {
expr.split(‘.‘).reduce((data, current, index, arr) => {
if(index === arr.length - 1) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
// 解析v-model指令
model(node, expr, vm) {
// node.value
let fn = this.updater[‘modelUpdater‘]
new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
fn(node, newVal)
})
node.addEventListener(‘input‘, e => {
let value = e.target.value
this.setVal(vm, expr, value)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
on(node, expr, vm, eventName) {
node.addEventListener(eventName, e => {
vm[expr].call(vm, e)
})
},
text(node, expr, vm) {
let fn = this.updater[‘textUpdater‘]
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 给表达式每个变量都加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr)) // 返回一个全的字符串
})
return this.getVal(vm, args[1])
})
fn(node, content)
},
html(node, expr, vm) {
// node.innerHTML
let fn = this.updater[‘htmlUpdater‘]
new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
fn(node, newVal)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
updater: {
modelUpdater(node, value) {
node.value = value
},
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
}
}
}
/**
* Vue构造函数
*/
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data
let computed = options.computed
let methods = options.methods
if(this.$el) {
// 做数据劫持
new Observer(this.$data)
// console.log(this.$data)
for(let key in computed) { // 依赖关系
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
}
for(let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]
}
})
}
// vm上的取值操作都代理上vm.$data上
this.proxyVm(this.$data)
// 编译模板
new Compiler(this.$el, this)
}
}
// 代理
proxyVm(data) {
for(let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
}
}
}