效果
简述原理
配置对象传入vue实例
模板解析,遍历出所有文本节点,利用正则替换插值表达式为真实数据
data数据代理给vue实例,以后通过this.xxx访问
给每个dom节点增加观察者实例,由观察者群组管理,内部每一个键值含有多个对不同dom的观察者
data数据劫持,给data的每个属性增加get和set函数,当值改变时触发观察者的update方法,更新所有与当前属性值相关的dom元素
劫持数据,说的挺好听的,就是加工数据嘛,多了set变化触发了模板重新渲染,该渲染方式使用观察者模式,获取观察者收集的各个dom的所有属性 div,观察的属性,div的属性textContent,同时根据最新值渲染模板
div.textContent=vm[key]
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- <script src="./vue.js"></script> -->
</head>
<body>
<div id="app">
{{ name }} {{age}}
<h1>{{age}}</h1>
<button @click="cli">按钮</button>
<input type="text" v-model="name">
</div>
</body>
<script src="./vue.js">
</script>
<script>
new Vue({
el: '#app',
data: {
name: 'Zwwwww',
age: 18,
},
methods: {
cli() {
console.log(this);
console.log(this.age);
}
},
})
</script>
</html>
js代码
class Vue {
constructor(options) {
// 获取配置对象的节点,存放在vm$el身上
this.$el = document.querySelector(options.el)
// console.log(this.$el)
// 将配置对象的data对象代理到$data
this.$data = options.data
// 获取配置对象的method值,
// vue实例监听,当触发了方法执行对应函数
this.$methods = options.methods
// 代理数据,后续通过this调用data对象的值
this.$allWatcher = {}
this.proxyData()
// 劫持数据,为其增加观察者监视数据变化引起视图渲染
this.observe()
// 收集所有观察者,用对象的属性存放
this.compile(this.$el)
}
// 数据代理到vue实例身上,后续this调用方法和data值
proxyData() {
// 遍历$data身上所有key
for (let key in this.$data) {
// 数据代理给vue实例,this
Object.defineProperty(this, key, {
// 使用get和set后续触发获取值和设置值做额外操作
get() {
// 返回当前data对应的key属性值
return this.$data[key]
},
set(value) {
// 设置新值给当前属性
this.$data[key] = value
},
})
}
}
// js数据替换{{name}},模板解析
compile(node) {
// 遍历根节点下的所有节点
node.childNodes.forEach((item, index) => {
//递归元素节点,
//如果还没到文本节点,也就是说元素节点内还有元素节点
//则继续递归,直到元素节点没有子节点
//第二种可能,如果为元素元素节点,判断是否有@click属性,并获取值
//该值为绑定的methods方法
if (item.nodeType === 1) {
if (item.childNodes.length > 0) {
this.compile(item)
}
if (item.hasAttribute('@click')) {
let domKey = item.getAttribute('@click')
// console.log('我是dom标签的key', domKey)
// 设置监听器,如果被点击了,触发配置对象中的method函数
item.addEventListener('click', () => {
// 通过模板获取的属性值方法命,调用函数
// 由于$methods只是引用地址,this指向还是原来的methods
// 我们这里使用call来绑定他的上下文this,也就是绑定他的调用者
// 在html部分我们就可以使用this.$data.age来获取vue实例上的数据
// 如果我们想直接this.age 就需要将data代理到vue实例身上
this.$methods[domKey.trim()].call(this)
})
}
if (item.hasAttribute('v-model')) {
let vmodelKey = item.getAttribute('v-model').trim()
// console.log('我是v-model的key', vmodelKey)
// 设置监听器,如果被点击了,触发配置对象中的method函数
// 先单向给input框设置值
item.value = this.$data[vmodelKey]
item.addEventListener('input', () => {
console.log('用户正在输入')
// 每次输入时将输入框的值重新赋给data对象属性值,完成双向绑定
this.$data[vmodelKey] = item.value
console.log(this.$data[vmodelKey])
// 数据更新的同时重新解析模板
// 这里使用观察者类观察数据变化所作出的响应
})
}
}
// 判断是否为文本节点,nodeType == 3
// console.log(item.nodeType)
// 如果是文本节点,进行数据替换
// 如果不是文本节点,为元素节点则往里递归遍历文本节点
if (item.nodeType === 3) {
// 定义正则,替换{{xxx}}形式的字串为data下的属性值
let reg = /\{\{(.*?)\}\}/g
// 获取原本标签里的值,后续进行替换
let text = item.textContent
// console.log(text)
item.textContent = text.replace(reg, (match, dataKey) => {
// 先将dataKey去空格处理
dataKey = dataKey.trim()
// match为匹配到的整体,datakey为捕获到的子内容(.*?)
//我们这里只需获取dataKey对应的值并塞入即可
// console.log(match, dataKey)
// 返回值作为替换内容 去除dataKey的前后空格
// 增加观察者,传vue实例对象,data属性,item标签,标签属性
// 相当于给每个文本节点都添加了一个观察者
// 将所有观察者收集到vue实例上,在数据发生变化时调用观察者的update方法
let watcher = new Watcher(this, dataKey, item, 'textContent')
// 先进行判断观察者群组里是否有该节点的观察者
// 如果有,就push添加,因为一个dataKey可能有多个模板使用
// 举个例子,name属性可能在div1里使用也在div2里使用
// 也就是将多个文本节点与同个datakey绑定
if (this.$allWatcher[dataKey]) {
this.$allWatcher[dataKey].push(watcher)
}
// 如果没有该属性的观察者存在,则新建空数组,push该观察者进入
else {
this.$allWatcher[dataKey] = []
this.$allWatcher[dataKey].push(watcher)
}
return this.$data[dataKey]
})
}
})
}
observe() {
console.log('开始劫持')
// 遍历所有的key,对其data数据劫持,值增加响应式功能
for (let key in this.$data) {
// 先获取value,否则数据重新定义后值会丢失
// 此处的value变量不会随着observe方法的结束而销毁
// 与内部匿名函数get和set作为闭包永远绑定在一起
// 同时value值是对$data的一个引用,修改value值会引起$data变化
let value = this.$data[key]
// 保存一份vue的引用_this=this,
// 防止后续在组件外部,也就是input输入框
// 此时触发的set为一个闭包环境,上下文变成由defineproper定义的this.$data数据对象
// 此时找不到vue实例作为上下文,对key和其他数据的引用也会失效
let _this = this
Object.defineProperty(this.$data, key, {
get() {
console.log('有人要获取劫持数据值', value)
// 返回上面存储的value值
// 由于是响应式的,只有当观察到数据变化时所以才接触数据
// 其value值作用域也作用在劫持过程中
return value
},
set(newValue) {
console.log('劫持到数据,修改值为', newValue)
console.log('劫持前的数据为', value)
value = newValue
// 更新值的同时进行模板更新
// 由于观察者队列含有观察者来观察不同属性管理的若干个模板
// 调用该属性值下所有模板观察者即可,
// 只要属性值变化,该属性值下的所有观察者重新渲染模板
console.log(_this.$allWatcher)
console.log(_this.$allWatcher[key])
_this.$allWatcher[key].forEach((watcher, index) => {
watcher.update()
})
},
})
}
console.log('劫持成功')
}
}
class Watcher {
constructor(vm, key, node, attr) {
this.vm = vm
this.key = key
this.node = node
this.attr = attr
}
// item.textContent = this.$data[dataKey.trim()]
update() {
console.log('开始渲染')
// 将原始dom标签内容值替换为 data里的属性值
this.node[this.attr] = this.vm[this.key]
}
}
代码参考
VUE双向绑定原理分析~实现视图和数据的双向绑定~_哔哩哔哩_bilibili