实现简单的MVVM
2019-06-28 本文已影响0人
YellowPoint
MVVM框架的含义
- M Model 模型层
- V View 视图层
- VM VIewModel 视图模型,V与M连接的桥梁
- html标签是V、data是M、Vue的实例是vm
- View 通过事件绑定来操作Model
- Model通过数据绑定来操作View
MVVM框架的核心
- 模板解析
- 响应式 (Object.defineProperty)
- 渲染 (fragment)
MVVM框架的实现
- new Observer的时候给每一个值都加上了一个 newDep来做发布者,搜集有用到这个值的订阅者
- 先走compiler的模板解析 如 text()方法 new一个watcher,监听数据变化
- new watcher的时候就在Dep中加入target=this
- 然后this.getVMValue(vm, expr) 就执行了 observer的get ,把Dep.target(也就是当前的监听者watcher)加到订阅者数组中
- 然后在watcher里 // 清空Dep.target Dep.target = null 方便后面在添加,(这里应该就相当于一个临时变量吧)
-
然后当用 vm.$data.msg=‘xx'’时 触发setter,就执行dep的notify,然后执行watcher的update,就触发了 每个指令或是文本节点解析的回调,就改变了值
MVVM示意图
代码如下
/src/compiler.js 编译相关
//定义一个类,用于创建vue实例
class Compiler {
constructor(el, 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. 在内存中编译
this.compile(fragment)
// 3. 把fragment一次性添加到页面
this.el.appendChild(fragment)
}
}
/* 核心方法 */
node2fragment(node) {
let fragment = document.createDocumentFragment()
let childNodes = node.childNodes
// console.log(childNodes instanceof Array)
this.toArray(childNodes).forEach(node => {
// console.log(node)
// 把所有子节点都添加到fragment中
fragment.appendChild(node)
})
return fragment
}
/*
编译文档碎片
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node => {
// 编译节点
// console.log( JSON.stringify(node)) ==>{} 为啥是空对象
// console.log(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)
}
})
}
// 解析元素节点
compileElement(node) {
// console.log('解析元素节点')
// 1. 获取当前节点下所有属性
let attributes = node.attributes
this.toArray(attributes).forEach(attr => {
// 2. 解析vue指令(所有以v-开始的属性)
// console.dir(attr)
let attrName = attr.name
if (this.isDirective(attrName)) {
let expr = attr.value
let type = attrName.slice(2)
// 解析v-on指令
if (this.isEventDirective(type)) {
CompileUtil['eventHandler'](node, this.vm, type, expr)
} else {
// 解析其他指令
CompileUtil[type](node, this.vm, expr)
}
}
})
}
// 解析文本节点
compileText(node) {
CompileUtil.mustache(node, this.vm)
}
/* 工具方法 */
toArray(likeArray) {
return [].slice.call(likeArray)
}
// nodeType:节点类型 1:元素节点,3:文本节点,很神奇没有2,哦被废弃了
isElementNode(node) {
return node.nodeType === 1
}
isTextNode(node) {
return node.nodeType === 3
}
// 是否是vue的指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 是否是v-on指令
isEventDirective(type) {
return type.split(':')[0] === 'on'
}
}
let CompileUtil = {
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
let expr = RegExp.$1
node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
new watcher(vm, expr, newValue => {
node.textContent = newValue
})
}
},
text(node, vm, expr) {
node.textContent = this.getVMValue(vm, expr)
// 通过watcher对象,监听expr的数据变化
new watcher(vm, expr, newValue => {
node.textContent = newValue
})
},
html(node, vm, expr) {
node.innerHtml = this.getVMValue(vm, expr)
new watcher(vm, expr, newValue => {
node.innerHtml = newValue
})
},
model(node, vm, expr) {
node.value = this.getVMValue(vm, expr)
let that = this
// 实现双向的数据绑定,给node注册input事件
node.addEventListener('input', function () {
that.setVMValue(vm, expr, this.value)
})
new watcher(vm, expr, newValue => {
node.value = newValue
})
},
eventHandler(node, vm, type, expr) {
// 给当前元素注册事件
let eventType = type.split(':')[1]
// 错误处理
let fn = vm.$methods && vm.$methods[expr]
if (eventType && fn) {
// 将方法的this指向当前实例vm
node.addEventListener(eventType, fn.bind(vm))
}
},
// 获取VM中的数据
getVMValue(vm, expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
},
setVMValue(vm, expr, value) {
let data = vm.$data
let arr = expr.split('.')
arr.forEach((key, index) => {
if (index < arr.length - 1) {
data = data[key]
} else {
data[key] = value
}
})
}
}
/src/observer.js 响应式
/*
observer用于给data中所有的数据添加getter,setter
*/
class Observer {
constructor(data) {
this.data = data
this.walk(data)
}
//核心方法
// 遍历data中所有的数据,都添加getter和setter
walk(data) {
if (!data || typeof data != 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
this.walk(data[key])
})
}
// 定义响应式的数据(数据劫持)
defineReactive(obj, key, value) {
let that = this
let dep = new Dep()
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
// 如果Dep.target中有watcher对象,则存储到订阅者数组中
// 注意这里的 Dep和dep
Dep.target && dep.addSub(Dep.target)
console.log('获取了',key,Dep.target)
return value
},
set(newValue) {
if (value === newValue) {
return
}
value = newValue
dep.notify()
// 如果newValue是一个对象,则继续劫持
that.walk(newValue)
}
})
}
}
/src/watcher.js
// watcher模块把compile模块与observe模块关联起来
class watcher {
//vm:当前vue实例
// expr:data中的数据
// 一旦数据发送变化,就调用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
}
// 对外暴露的一个方法,用于更新页面
update() {
// 对比expr是否发生改变,如果改变则调用cb
let oldValue = this.oldValue
let newValue = this.getVMValue(this.vm, this.expr)
if (oldValue != newValue) {
this.cb(newValue, oldValue)
}
}
// 获取VM中的数据
getVMValue(vm, expr) {
let data = vm.$data
expr.split('.').forEach(key => {
data = data[key]
})
return data
}
}
// dep对象用于管理所有的订阅者和通知这些订阅者
class Dep {
constructor() {
// 用户管理订阅者
this.subs = []
}
// 添加订阅者
addSub(watcher) {
console.log('this.addSub', watcher)
this.subs.push(watcher)
}
// 通知
notify() {
console.log('this.subs', this.subs)
// 遍历所有的订阅者,调用wathcer的uodate方法
this.subs.forEach(sub => {
sub.update()
})
}
}
/src/vue.js
//import Compiler from './compiler.js'
//定义一个类,用于创建vue实例
class Vue {
constructor(options = {}) {
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
// 监视data中的数据
new Observer(this.$data)
// 把data中所有的数据代理到了vm上
this.proxy(this.$data)
// 把methods中所有的方法代理到vm上
this.proxy(this.$methods)
if (this.$el) {
//Compiler负责解析模板的内容
//需要:模板和数据
//(为啥不只传一个this,而是要分两个)
new Compiler(this.$el, this)
}
}
proxy(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (data[key] == newValue) {
return
}
data[key] = newValue
}
})
})
}
}
index.html
<!DOCTYPE html>
<html lang="zh">
<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>Document</title>
</head>
<body>
<div id="app">
<!-- <p>fsafas{{msg}}++{{msg}}</p> -->
<p>fsafas{{msg}}</p>
<div v-text="msg"></div>
<div v-html="tag"></div>
<p>{{car.brand}}</p>
<div v-text="car.color"></div>
<input type="text" v-model="car.color">
<button v-on:click="clickFn">点我</button>
</div>
<script src="./src/watcher.js"></script>
<script src="./src/observer.js"></script>
<script src="./src/compiler.js"></script>
<script src="./src/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hahah',
tag: '<h3>哎哟 不错</h3>',
car: {
brand: '特斯拉',
color: 'red'
},
// $data:"我是data"
},
methods: {
clickFn() {
this.$data.msg ="点击了"
}
}
})
// console.log(vm)
</script>
</body>
</html>