index.html
<!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">
<input type="text" v-model='value.a'>
{{value.a}}
<button @click='button'>加1</button>
</div>
</body>
<script src="./mvvm.js"></script>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./dep.js"></script>
<script src="./compile.js"></script>
<script>
var app = new mvvm({
el:'#app',
data:{
value:{
a:'123'
},
},
methods:{
button(){
this.value.a++
},
}
})
</script>
</html>
mvvm.js
class mvvm {
constructor(config) {
//把模板元素节点绑定到实例的$el属性
this.$el = document.querySelector(config.el)
if (!this.$el) {
throw new Error('组件的DOM根元素不能为空')
}
this.$data = config.data
this.methods = config.methods
//数据代理
this.proxyData(config.data)
//数据监测
new Observer(config.data)
//编译模板
new Compile(this.$el, this)
}
//代理的方法
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this.$data[key]
},
set(newValue) {
this.$data[key] = newValue
}
})
})
}
}
observer.js
class Observer {
constructor(data) {
this.data = data
this.observer(data)
}
observer(data) {
if (!data || typeof data !== 'object') { return }
//Object.keys会返回对象的key组成的数组
//循环通过Object.defineProperty来监听属性的变化
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]) //数据监测
//如果data[key]是对象里面含有对象,需要继续监听深层次的数据
this.observer(data[key])
})
}
defineReactive(data, key, value) {
let dep = new Dep()
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: () => {
// 将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。
Dep.target && dep.addSub(Dep.target)
console.log('获取了数据' + value, key);
return value
},
//set的参数是该属性赋的新值
set: (newValue) => {
//替换原来旧的值
if (newValue != value) {
console.log('更新了数据' + newValue);
//新的值也需要重新监听
this.observer(newValue)
value = newValue
//触发watcher的update()
dep.notify()
}
}
})
}
}
watcher.js
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm
this.exp = exp//v-model等指令的属性值 如 v-model="value.a",exp 就是value.a,或者或者插值符号中的属性,如{{value.a}} ,exp 就是 value.a.
this.cb = cb
//存储上一个值
this.oldValue = this.get()
}
get() {
Dep.target = this //把watcher实例缓存到 Dep.target
//获取vm上的数据,就会出发数据对象的getter,这样就会从 Dep.target中读取 watcher实例并添加到Dep中
// let value = this.vm.$data[this.exp] //这种写法只能获取对象最外面一级,如value.a就获取不到了
let value = this.getValue(this.vm, this.exp)
Dep.target = null
return value
}
update() {
// let newValue = this.vm.$data[this.exp]
let newValue = this.getValue(this.vm, this.exp)
if (this.oldValue !== newValue) {
this.oldValue = newValue
this.cb(newValue) //执行回调函数
}
}
getValue(vm, exp) {
exp = exp?.split('.') // [message.a]
return exp?.reduce((prev, next) => {
return prev[next]
}, vm.$data)
}
}
dep.js
class Dep {
constructor() {
//使用一个数组来存watcher
this.subs = []
}
addSub(watcher) {
//把watcher存起来
this.subs.push(watcher)
}
notify() {
//更新值的时候找到对应的watcher调用update()
this.subs.forEach(watcher => {
watcher.update()
})
}
}
//这是一个全局的Watcher,同一时间只能有一个全局的Wathcer被计算
Dep.target = null
compile.js
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
// 生成虚拟dom(用对象或者数组的方式来描述节点)
this.complierNodes();
//根据虚拟dom重新生成节点
this.createElement()
}
//生成虚拟dom
complierNodes() {
//根节点的dom元素下的所有子节点
var nodeList = this.el.childNodes;
//生成vmNodes虚拟dom
this.vm.vmNodes = this.complierNodesChild(nodeList)
}
//通过递归生成虚拟dom函数
complierNodesChild(nodeList) {
//初始化vmNodes数组,最后用来返回
var vmNodes = []
//使用一个对象来拼接我们的数据
var data = {}
//nodeList为伪数组,不能直接遍历
Array.from(nodeList).forEach(node => {
// 匹配字符串中插值表达式的内容
var reg = /\{\{[^\{\}]*\}\}/g;
data = {
node: node,//存放节点元素
nodeName: node.nodeName,//节点的名字
nodeValue: node.nodeValue,//元素的值
nodeType: node.nodeType,//元素的节点类型,1为普通元素,3位文本节点,8为注释
data: [],//存放节点中的插值表达式的内容
attrs: node.attributes,//元素节点的属性
props: {},//除了v-开头剩下的属性
directives: {},//专门存放指令的数组
children: [],//用来存放子节点
events: {},//存放节点的事件
}
//如果当前节点是一个文本节点
if (node.nodeType === 3) {
// nodeValue的值为空的话直接返回
if (node.nodeValue.trim() === '') {
return false
} else {
//字符串的match方法会根据正则返回一个数组
var arr = node.nodeValue.match(reg) || [];
//循环去掉大括号
arr.forEach(v => {
v = v.replace(/[/{/}]/g, "");
data.data.push(v)
})
}
}
//如果当前是一个普通节点
if (node.nodeType === 1) {
//获取元素节点的属性
var attrObj = { ...node.attributes }
Object.keys(attrObj).forEach(index => {
var prop = attrObj[index]
//判断属性是否是v-开头的
if (/^(v-)+/.test(prop.name)) {
//把指令保存到data的directives中
data.directives[prop.name] = prop.value
} else if (/^(@)+/.test(prop.name)) {
//如果是以@开头的属性,说明他是一个事件
data.events[prop.name.replace('@', '')] = prop.value
} else {
//把非v-开头的属性添加到data.props
data.props[prop.name] = prop.value
}
})
}
//如果节点还有子节点,就要循环递归执行当前的这个函数
if (node.childNodes.length > 0) {
data.children = this.complierNodesChild(node.childNodes)
}
//把对象放到虚拟节点上
vmNodes.push(data)
})
return vmNodes
}
/**
* reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,
* 接受四个参数:初始值(或者上一次回调函数的返回值),
* 当前元素值,当前索引,调用 reduce 的数组。
*/
//使用reduce函数来获取最底层的key
getValue(exp) {
exp = exp?.split('.')
return exp?.reduce((prev, next) => {
return prev[next]
}, this.vm.$data)
}
getInputSetValue(vm, exp, value) {
exp = exp?.split('.')
return exp?.reduce((prev, next, currentIndex) => {
//找出最后一个索引,也就是vm.$data里面的value[a]赋值value
if (currentIndex == exp.length - 1) {
prev[next] = value
}
return prev[next]
}, vm.$data)
}
//递归生成子节点
createElementChild(parentNode, nodeList) {
//循环虚拟node节点重新生成dom元素
nodeList.forEach(node => {
var newNode;
//如果是元素节点(非文本节点)使用createElement
if (node.nodeType === 1) {
newNode = document.createElement(node.nodeName)
//元素节点会可能会有时间,使用addEventListener绑定事件
Object.keys(node.events).forEach(eventName => {
newNode.addEventListener(eventName, (event) => {
//一下这种写法的this指向不对,需要让methods里面的this指向vm实例
// this.vm.methods[node.events[eventName]](event);
this.vm.methods[node.events[eventName]].call(this.vm, event);
})
})
//如果当前的节点是input元素,input元素中也含有v-model
if (node.nodeName == 'INPUT' && node.attrs['v-model']) {
//获取vm.$data所对应的属性值来赋值当前节点的value
let value = this.getValue(node.attrs['v-model'].value)
newNode.value = value
//监听v-model绑定的 data属性的值的变化
new Watcher(this.vm, node.attrs['v-model'].value, () => {
newNode.value = this.getValue(node.attrs['v-model'].value)
})
//处理input标签的input事件
newNode.addEventListener('input', (event) => {
//获取输入框的值
var inputValue = event.target.value;
//修改vm.$data下面的某个属性,会触发set方法更新页面
this.getInputSetValue(this.vm, node.attrs['v-model'].value, inputValue)
})
}
}
//文本节点
if (node.nodeType === 3) {
//把节点的插值表达式内容替换为vm.$data的属性值
var text = this.replaceElementText(node.nodeValue);
newNode = document.createTextNode(text)
//监听vm.$data当前节点属性值的变化
new Watcher(this.vm, node.data[0], () => {
node.node.nodeValue = this.replaceElementText(node.nodeValue)
})
}
//注释节点
if (node.nodeType === 8) {
return
}
//把新的dom节点覆盖原来的dom节点
node.node = newNode
parentNode.appendChild(newNode)
if (node.children.length > 0) {
//判断如果有子元素,中心的执行函数,函数也会相对的变化
this.createElementChild(newNode, node.children)
}
})
}
//把节点的插值表达式内容替换为vm.$data的属性值
replaceElementText(value) {
// 全局的正则表达式
var reg = /\{\{[^\{\}]*\}\}/g;
// 把带有两个大括号的数据返回一个数组
var regArr = value.match(reg);
// 如果是一个数组代表值里面有两个大括号
if (Array.isArray(regArr)) {
// 循环的替换value的值
regArr.forEach(v => {
// 相当于把{{value.a}}两侧的大括号给删掉
var prop = v.replace(/[/{/}]/g, "");
value = this.getValue(prop)
})
}
return value;
}
// 根据虚拟dom重新生成节点
createElement() {
//创建一个虚拟的模板节点(文档片段)
var fragment = document.createDocumentFragment();
//返回最后所有虚拟节点生成的dom元素
this.createElementChild(fragment, this.vm.vmNodes);
//清空根节点所有内容
this.el.innerHTML = '';
//重新生成dom元素
this.el.appendChild(fragment)
}
}