MVVM
一句话就是 vm层(视图模型层)通过接口从后台m层(model层)请求数据
vm层继而和v(view层)实现数据的双向绑定
数据绑定
单向绑定:数据 =>视图
双向绑定:视图 <=> 数据
双向数据绑定无非就是在单向绑定的基础上给
可输入元素添加了change事件,来动态修改model和view
实现数据绑定的做法大致有如下几种:
发布者-订阅者模式 backbone.js
脏值检查 angular.js
数据劫持 vue.js
发布者-订阅者模式
一般通过sub pub的方式实现数据和视图的绑定监听更新数据的方式通常是vm.set(‘property’,value);
脏值检查
一般通过检测对比的方式查看数据是否有变更,来决定是否更新视图,
最简单的就是通过setInterval()计时器来定时检测数据变动,
但是angular不会这么low,它只有在指定的事件触发时才进入脏值检测,比如DOM事件,XHR响应事件等
数据劫持
vue则是采用数据劫持结合发布订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter getter,在数据变动时发布消息给订阅者,触发监听回调
基本流程:
MVVM作为绑定入口,整合Observer,Compile和watcher;
通过Observer监听数据变化,通过Compile来解析编译模板指令
利用watcher搭建Observer和Compile之间的通信达到双向绑定
代码实现
创建index.html
<body>
<div id="app">
<h2>{{person.name}}--{{person.age}}</h2>
<h3>{{person.sex}}</h3>
<div v-text='person.name'></div>
<div v-html='htmlStr'></div>
<input type="text" v-model='msg' />
<ul>
<li>{{msg}}</li>
<li>22</li>
<li>33</li>
<li>44</li>
</ul>
<button v-on:click='funs'>按钮1</button>
<button @click='funs'>{{msg}}</button>
</div>
<!-- <script src="./myVue.js"></script>官方vue.js文件 -->
<!-- 引入自定义js文件 -->
<script src="./Obsever.js"></script>
<script src="./myVue.js"></script>
<script>
let vm = new MyVue({
el: '#app',
data: {
person: {
name: '小张',
age: 23,
sex: '男',
},
msg: '实现mvvm原理',
htmlStr: '网页'
},
methods: {
funs() {
console.log(this)
this.person.name = '2333'
}
}
})
</script>
</body>
myVue.js
创建一个MyVue类
//MyVue
class MyVue {
//构造
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$option = options;
//如果节点存在
if (this.$el) {
//1.数据劫持
new Observe(this.$data)
//2.实现一个指令解析器
new Compile(this.$el, this);
//添加代理
this.proxyData(this.$data);
}
}
//添加代理
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
}
}
}
MyVue类中
1.实例化一个指令解析器
2.数据劫持
3.添加对象代理
创建一个指令解析器
作用:
1.初始化视图
1.订阅数据变化,绑定更新函数
//指令解析器
class Compile {
constructor(el, vm) {
//节点传给el
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
//1.获取文档碎片对象,放入内存中减少回流和重绘
//文档碎片对象 :document.createDocumentFragment() 用于暂时存放创建的dom元素
//将需要添加的大量元素 先添加到文档碎片,文档碎片添加到需要插入的位置,大大减少dom操作
const fragment = this.nodeFragment(this.el);
// console.log(fragment);
//2.编译模板
this.compile(fragment);
//3.追加子元素到根元素
this.el.appendChild(fragment);
}
//编译模板
compile(fragment) {
const childNode = fragment.childNodes;
//遍历每个子节点
[...childNode].forEach(childItem => {
if (this.isElementNode(childItem)) {
// console.log('元素节点', childItem);
//处理节点元素
this.compileElementNode(childItem);
} else {
// console.log('文本节点', childItem);
//处理文本元素
this.compileElementText(childItem)
}
//遍历子节点下的子节点
if (childItem.childNodes && childItem.childNodes.length) {
this.compile(childItem);
}
})
}
//处理节点元素
compileElementNode(node) {
//获取节点属性
const attributes = node.attributes;
//遍历节点每个属性
[...attributes].forEach(item => {
const { name, value } = item;
//是否指令
if (this.isDirecative(name)) { //v-text v-html v-model v-on:click
// console.log(item)
const [, dir] = name.split('-'); //text html model on:click
const [dirName, eventName] = dir.split(':');//text html model on click
//更新视图,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
//删除有指令的标签属性
node.removeAttribute('v-' + dir);
}
//是否事件
else if (this.isEventName(name)) { //@click='fun'
const [, dir] = name.split('@');
const eventName = dir.split('=');
compileUtil['on'](node, value, this.vm, eventName);
}
// console.log(name, value)
})
}
//处理文本元素
compileElementText(node) {
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
console.log(content);
compileUtil['text'](node, content, this.vm);
}
}
//转化为文档节点
nodeFragment(el) {
//创建文档对象
const f = document.createDocumentFragment();
let firstChild;
//循环装入第一子节点
while (firstChild = el.firstChild) {
f.appendChild(firstChild);
}
//
return f;
}
//是否是事件
isEventName(attrName) {
return attrName.startsWith('@')
}
//是否是指令
isDirecative(attrName) {
return attrName.startsWith('v-');
}
//判断是否节点
isElementNode(node) {
return node.nodeType === 1;
}
}
添加一个指令对象,处理v-html v-model v-on v-text等指令
const compileUtil = {
//指令(节点,名称,MyVue对象)
text(node, expr, vm) {
//获取MyVue对象 data的某一属性的值
console.log(expr)
// const value = this.getVal(expr, vm);
let value;
//去除{{}}
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//挂在一个观察者
new Watcher(vm, args[1], (newVal) => {
console.log(this.getContenVal(expr, vm))
this.updater.textUpdater(node, this.getContenVal(expr, vm));
})
return this.getVal(args[1], vm);
})
} else {
console.log(this.getContenVal(expr, vm))
value = this.getVal(expr, vm);
}
//更新函数
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getVal(expr, vm);
//挂在一个观察者
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
})
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getVal(expr, vm);
//数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
})
//视图=>数据=>视图
node.addEventListener('input', (e) => {
this.setVal(expr, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
//事件
on(node, expr, vm, eventName) {
let fn = vm.$option.methods && vm.$option.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
//更新函数
updater: {
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
textUpdater(node, value) {
//返回指定节点的文本内容
node.textContent = value;
}
},
//
setVal(expr, vm, inputVal) {
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal;
return data[currentVal];
}, vm.$data);
},
//获取值
getVal(expr, vm) {
//person.name
//[person,name]
return expr.split('.').reduce((data, currentVal) => {
return data[currentVal]
}, vm.$data);
},
//
getContenVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm);
})
}
}
在每个指令执行时候为每个对象挂载一个观察者new Watcher()
2.数据劫持
Object.defineProperty劫持数据的get和set方法
//数据劫持
class Observe {
constructor(data) {
this.observer(data);
}
//遍历所有属性
observer(data) {
if (data && typeof data === 'object') {
console.log(Object.keys(data))
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
//数据劫持
defineReactive(data, key, value) {
//递归遍历子属性
this.observer(value);
//添加dep容器
const dep = new Dep();
//劫持所有属性 为每个属性添加get set方法
Object.defineProperty(data, key, {
enumerable: true,//是否可遍历
configurable: false,//是否可更改编写
get() {
//订阅数据变化时,往Deo中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => {
//对新值也进行劫持
this.observer(newVal);
if (newVal !== value) {
value = newVal;
}
//通知观察者去跟新
dep.notify();
}
})
}
}
订阅数据时,往Deo中为每个属性添加观察者
数据变化时通知Dep的属性观察者去更新
创建一个Dep容器
//*Obsever.js*
class Dep {
constructor() {
this.subs = [];
}
//收集观察者
addSub(watcher) {
this.subs.push(watcher);
}
//通知观察者去跟新
notify() {
console.log('观察者', this.subs);
this.subs.forEach(watcher => {
//通知观察者更新数据
watcher.updata();
})
}
}
Dep容器的作用是去存储所有观察者,当数据变化时,通知观察者去跟新
观察者
class Watcher {
constructor(vm, expr, callback) {
this.vm = vm;
this.expr = expr;
this.callback = callback;
//先把旧值保存
this.oldVal = this.getOldVal();
}
//保存旧值
getOldVal() {
//挂载观察者
Dep.target = this; //target暂存的this,this指向Watcher对象,把对象push进dep就销毁
//获取旧值
const oldVal = compileUtil.getVal(this.expr, this.vm);
//清除观察者
Dep.target = null;
return oldVal;
}
//更新数据
updata() {
//获取变化值
const newVal = compileUtil.getVal(this.expr, this.vm);
//如果改变则执行回调
if (newVal !== this.oldVal) {
this.callback(newVal)
}
}
}