八四、MVVM框架进阶与实现(手动实现一个简易版vue)
程序员文章站
2022-07-01 20:50:31
...
MVVM框架介绍
- M(Model,模型层 ),
- V(View,视图层),
- VM(ViewModel,视图模型,V与M连接的桥梁)
- MVVM框架实现了数据双向绑定
- 当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改
- 修改V层则会通知M层数据进行修改
- MVVM框架实现了视图与模型层的相互解耦
几种双向数据绑定的方式
1 发布-订阅者模式(backbone.js)
- 一般通过pub、sub的方式来实现数据和视图的绑定,但是使用起来比较麻烦
2 脏值检查(angular.js)
- angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变。
3 数据劫持
- vue.js 则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。(vuejs不兼容IE8以下的版本)
Vue实现思路
- 实现一个Compiler模板解析器,能够对模版中的指令和插值表达式进行解析,并且赋予不同的操作
- 实现一个Observer数据监听器,能够对数据对象的所有属性进行监听
- 实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM
- 创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,也就是vue
- html中使用
new Vue > compile > 指令 表达式 解析(new Watcher -订阅数据变化 ) > observe 数据劫持(简体数据改变 new Dep > addSub(watcher存储起来)> 数据改变就通知dep.notify)> watcher 接受到通知 触发updata 更新视图
多个watcher 怎么管理?> 使用发布订阅者模式> 有watcher就存储起来 > 数据改变调用updata通知所有的订阅者更新数据
发布-订阅者模式,也叫观察者模式
它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。
例子:微信公众号
- 订阅者:只需要要订阅微信公众号
- 发布者(公众号):发布新文章的时候,推送给所有订阅者
- 优点:解耦合(订阅者不用每次去查看公众号是否有新的文章
发布者不用关心谁订阅了它,只要给所有订阅者推送即可)
上代码
Compiler.js
/* compile 专门扶着解析模板内容 */
class Compile {
/**
*
* @param el --new Vue传递的选择器
* @param vm --vue实例
*/
constructor(el, vm) {
console.log(vm)
this.el = typeof el === 'string' ? document.querySelector(el) : el
this.vm = vm
//编译模板
if (this.el) {
//1. 把el中所有的子节点都放到内存中,fragment
let fragment = this.node2fragment(this.el)
//2. 在内存中编译fragment
this.compile(fragment)
//3. 把fragment一次性添加到页面
this.el.appendChild(fragment)
}
}
/*核心方法*/
/**
* 将节点添加到fragment 中
* @param node
* @returns {DocumentFragment}
*/
node2fragment(node) {
let fragment = document.createDocumentFragment()
//把 el中所有的子节点挨个添加到文档碎片中
let childNodes = node.childNodes
this.toArray(childNodes).forEach(node => {
//把所有的子节点都添加到fragment中
fragment.appendChild(node)
})
return fragment
}
/**
* 编译文档碎片
* @param fragment
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 如果是元素,需要解析指令
this.compileElement(node)
}
if (this.isTextNode(node)) {
// 如果是文本节点,需要解析指令需要解析插值表达式
this.compileText(node)
}
if (node.childNodes && node.childNodes.length > 0) {
//如果当前节点还有子节点 需要递归解析
this.compile(node)
}
})
}
/**
* 解析html标签
* @param node
*/
compileElement(node) {
//1 获取到当前节点下的所有属性
let attributes = node.attributes
this.toArray(attributes).forEach(attr => {
let attrName = attr.name
//2 解析vue指令(以v-on开头的指令)
if (this.isDirective(attrName)) {
//指令类型
let type = attrName.slice(2)
//指令值
let expr = attr.value
if (this.isEventDirective(type)) {
CompileUtil['enentHandler'](node, this.vm, type, expr)
} else {
CompileUtil[type] && CompileUtil[type](node, this.vm, expr)
}
}
})
}
/**
* 解析文本节点
* @param node
*/
compileText(node) {
CompileUtil.mustache(node, this.vm)
}
/*工具方法*/
/**
* 转化为数组
* @param likeArray
* @returns {*[]}
*/
toArray(likeArray) {
return [].slice.call(likeArray)
}
/**
* nodeType:节点类型 1:元素节点 3:文本节点
* @param node
*/
isElementNode(node) {
return node.nodeType === 1
}
isTextNode(node) {
return node.nodeType === 3
}
/**
* 判断是否为v-开头的指令
* @param attr
* @returns {boolean}
*/
isDirective(attr) {
return attr.startsWith("v-")
}
/**
* 判断是否是事件指令
* @param attr
* @returns {boolean}
*/
isEventDirective(attr) {
return attr.split(':')[0] === 'on'
}
}
//util 将编译的方法提取出来 方便增删改查
let CompileUtil = {
//处理文本
mustache(node, vm) {
let text = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(text)) {
//通过正则分组 取到内容 将原来的美容替换为data里的数据
let expr = RegExp.$1
node.textContent = text.replace(reg, this.getVMValue(vm, expr))
new Watcher(vm, expr, newVlue => {
node.textContent = text.replace(reg, newVlue)
})
}
},
//处理v-text
text(node, vm, expr) {
node.textContent = this.getVMValue(vm, expr)
//通过watcher监听expr的数据变化,一旦改变执行回调
new Watcher(vm, expr, newVlue => {
node.textContent = newVlue
})
},
//处理v-html
html(node, vm, expr) {
node.innerHTML = this.getVMValue(vm, expr)
new Watcher(vm, expr, newVlue => {
node.innerHTML = newVlue
})
},
//处理v-model
model(node, vm, expr) {
let that = this
node.value = this.getVMValue(vm, expr)
//实现双向数据绑定,给node注册input事件,当前元素value值发生改变,data里数据也要改变
node.addEventListener('input', function () {
console.log(this.value)
that.setVMValue(vm, expr, this.value)
})
new Watcher(vm, expr, newVlue => {
node.value = newVlue
})
},
//处理事件
enentHandler(node, vm, type, expr) {
let eventType = type.split(":")[1]
let fn = vm.$methods && vm.$methods[expr]
if (eventType && fn) node.addEventListener(eventType, fn.bind(vm))
},
/**
* 获取VM中的数据 (主要解决对象中的数据)
* @param vm
* @param expr
* @returns {*}
*/
getVMValue(vm, expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
},
/**
* 获取VM中的数据 (主要解决对象中的数据)
* @param vm
* @param expr
* @returns {*}
*/
setVMValue(vm, expr, value) {
let data = vm.$data
// debugger
let arr = expr.split('.')
arr.forEach((key, i) => {
if (i < arr.length - 1) {
data = data[key]
} else {
data[key] = value
}
})
}
}
Observe.js
/* observe 用于给data中所有的数据天机getter setter 方便我们在获取或者设置data中数据的时候,实现一下逻辑 */
class Observer {
constructor(data) {
this.data = data
this.walk(data)
}
/*核心方法*/
/**
* 遍历data中的数据,都添加上getter,setter
* @param data
*/
walk(data) {
if (!data || typeof data != "object") {
return
}
Object.keys(data).forEach(key => {
//给data对象的可以设置setter,getter
this.defineReactive(data, key, data[key])
//如果$data[key]是复杂类型 递归walk
this.walk(data[key])
})
}
/**
* 定义响应式的数据(数据劫持)
* data中的每一个数据都应该维护一个dep对象
* dep保存了所有的订阅了该数据的订阅者
* @param obj
* @param key
* @param value
* @returns {*}
*/
defineReactive(obj, key, value) {
let that = this
let dep = new Dep()
Object.defineProperty(obj, key, {
configurable: true, // 表示属性可以配置
enumerable: true, // 表示这个属性可以遍历
// 每次获取对象的这个属性的时候,就会被这个get方法给劫持到 getter
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
// 每次设置这个对象的属性的时候,就会被set方法劫持到
// 设置的值也会劫持到 setter
set(newValue) {
console.log('set方法执行了---',newValue)
value !== newValue ? value = newValue : null
//如果newValue也是一个对象 也要调用walk
that.walk(value)
//发生改变 调用wather的updata方法 (发布通知)
dep.notify()
}
})
}
}
Watcher.js
/*watcher 模块负责把compile 模块 与observe 模块连接起来(桥梁)*/
class Watcher {
/**
* @param vm:当前vm实例
* @param expr:data中数据的名字
* @param cb:一旦数据发生了改变,需要调用cb
*/
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
//this 表示新创建的watcher对象 存储到dep.target 属性上
Dep.target = this
//把expr的旧值储存起来
this.oldValue = this.getVMValue(vm, expr)
//清空dep.target
Dep.target = null
}
/**
* 对外暴露的方法,用于更新页面
* 对比新旧的值 改变就调用cb
*/
updata() {
let oldValue = this.oldValue
let newValue = this.getVMValue(this.vm, this.expr)
if (oldValue != newValue) {
this.cb(newValue, oldValue)
}
}
/**
* 获取VM中的数据 (主要解决对象中的数据)
* @param vm
* @param expr
* @returns {*}
*/
getVMValue(vm, expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
}
}
/* dep 对象用于管理所有的订阅者和通知这些订阅者*/
class Dep {
constructor() {
//用于管理订阅者
this.subs = []
}
//添加订阅者
addSub(watcher) {
this.subs.push(watcher)
}
//通知 发布
notify() {
//通知所有的订阅者,调用watcher的update方法
this.subs.forEach(sub => {
sub.updata()
})
}
}
Vue.js
/* 定义一个类,用于创造vue实例*/
class Vue {
constructor(options = {}) {
//给vue实例增加属性
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
//通过observe监视data数据
new Observer(this.$data)
//把data中的数据代理到vm上
this.proxy(this.$data)
//把methods的数据代理到vm上
this.proxy(this.$methods)
//如果指定了el参数,对el进行解析
if (this.$el) {
//compile 负责解析模板的内容
// 需要:模板、数据
let c = new Compile(this.$el, this)
}
}
/**
*使用proxy代理 将this.$data上的数据代理到this上
* @param data
*/
proxy(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(v) {
if(data[key] == v) return
data[key] = v
}
})
})
}
}
index.html
<!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>vue-mvvm-demo</title>
</head>
<body>
<div id="app">
插值表达式
<h3>{{msg}}</h3>
<div>
hhh,{{text}}
<p v-html="demo"></p>
</div>
<h4>{{color.red}}</h4>
<h4>{{color.other.block}}</h4>
<!-- vue的指令 -->
<p v-text="msg"></p>
<input type="text" v-model="msg">
<button v-on:click='_handleClick'>按钮</button>
</div>
<script src="./src/watcher.js"></script>
<script src="./src/observe.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/vue.js"></script>
<script>
// let app = document.getElementById('app')
const vm = new Vue({
el: '#app',
data: {
msg: 'hello vue',
demo: '<h1>我是h1标签</h1>',
text: '呵呵呵呵',
color: {
red: 'red',
yellow: 'yellow',
other: {
block: 'block'
}
}
},
methods: {
_handleClick() {
//vue 中this指向当前vm实例
console.log(this.msg)
this.msg = '改变red'
}
}
})
</script>
</body>
</html>
上一篇: 中断可恢复性-爬虫系统(广度优先-python单进程版)
下一篇: arcpy提取文本文件中坐标