class Vue {
constructor(options) {
this.$data = options.data
// 监听者 监听this.$data的每一个属性
Observer(this.$data)
// 属性代理 可以直接在vm上挂载 相当于找vm.name = vm.$data.name 做一层代理
// 循环每个$data上的属性 然后挂载到
for (let key in this.$data) {
// 为this定义属性
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
// 当有人向vm拿属性的时候 直接返回$data上的
return this.$data[key]
},
set(newValue) {
this.$data[key] = newValue
}
})
}
//调用模板编译 结构 数据
Compile(options.el, this)
}
}
// 数据劫持
function Observer(obj) {
// 递归遍历
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
for (let key in obj) {
let value = obj[key]
// 递归子节点
Observer(value)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('触发get');
// new 的watcher实例存入dep.subs中
console.log(dep);
Dep.target && dep.addSubs(Dep.target)
return value
},
set(newValue) {
console.log('触发set');
value = newValue
//由于对象重新赋值 get和set都会没有 所以要对新赋值的进行监听
Observer(value)
dep.notify()
}
})
}
}
// 模板编译的方法
function Compile(el, vm) {
// 得到Dom元素
vm.$el = document.querySelector(el)
// 创建文档碎片
const fragment = document.createDocumentFragment()
// 把所有元素全部存入fragment里
while (vm.$el.firstChild) {
fragment.appendChild(vm.$el.firstChild)
}
// 进行模板编译
replace(fragment)
// 把 编译完成的文档碎片放回去
vm.$el.appendChild(fragment)
// 负责对DOM模板进行编译的方法
function replace(node) {
let reg = /\{\{(.*)\}\}/
if (node.nodeType === 3) {
// 是文本节点 终止递归 在此进行模板语言替换 {{ }} node.nodeValue == node.textContent
let text = node.nodeValue
// text.match(reg) == reg.exec(text)
if (reg.exec(text)) {
// 用reduce的方法 如果reg.exec(text)[1] 是info.a 类似的字符串 则先分隔 通过reduce滚雪球来取值
const value = reg.exec(text)[1].split('.').reduce((newObj, key) => newObj[key], vm)
console.log(reg.exec(text)[1].split('.'),'value');
node.nodeValue = text.replace(reg, value)
// 在此时创建watcher类的实例 因为此时作为node 你已经知道怎么更新自己的内容
// 为了再次更新 就把这行代码存入watcher实例的cb函数中
new watcher(vm, reg.exec(text)[1], (newValue) => {
//
node.nodeValue = text.replace(reg, newValue)
})
}
return
}
// 如果是input 且有v-model
if (node.nodeType === 1 && node.tagName === 'INPUT') {
console.log(node);
const attrs = Array.from(node.attributes)
const finRes = attrs.find((x) => x.name === 'v-model')
const expStr = finRes.value
if (finRes) {
console.log(finRes,'finRes');
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
node.value = value
// 创建watcher
new watcher(vm,expStr,(newValue) => node.value = newValue)
}
// 监听文本框的输 入 双向数据榜单 拿到文本框最新的值 把最新的值放到VM上
node.addEventListener('input', e => {
const keyArr = expStr.split('.')
console.log(keyArr,'keyArr');
const obj = keyArr.slice(0,keyArr.length - 1).reduce((newObj,k) => newObj(k),vm)
obj[keyArr[keyArr.length-1]] = e.target.value
})
}
// 不是文本节点 需要递归 把它的子节点放入
node.childNodes.forEach(child => replace(child))
}
}
// 依赖收集者
class Dep {
constructor() {
this.subs = []
}
//提供添加subs的方法
addSubs(watcher) {
this.subs.push(watcher)
}
// 通知订阅者 调用订阅者存在subs里的回调函数
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 订阅者
class watcher {
// cb回调函数中记录订阅者如何更新自己的文本内容
// 还必须拿到最新的数据 ,所以要在new Watcher期间把VM传递进来 因为vm保存着最新数据
// 还要根据Key来找到vm上自己对应的数据
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 负责把watcher实例存在dep实例的subs数组中
Dep.target = this
// 主要是为了去调用get 此时Dep.target还是指向Wather 存入subs
key.split('.').reduce((newObj, k) => newObj[k], vm)
Dep.target = null
}
// update 让发布者能够通知我们去更新
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
姓名是:{{name}}
<div>
年龄是:{{age}}
</div>
<div>
{{info.a}}
</div>
name:<input type="text" v-model="name">
age:<input type="text" v-model="age">
</div>
</body>
<script src="./MVVM.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: "ruirui",
age: '20',
info: {
a: 'a1',
c: 'c1'
}
}
})
</script>
</html>