<!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>Document</title>
</head>
<body>
<div class="app">
<form>
<label for="name">
名字:
<input id="name" type="text" v-model="name">
</label>
<label for="carName">
车牌:
<input id="carName" type="text" v-model="car.name">
</label>
<label for="carColor">
颜色:
<input id="carColor" type="text" v-model="car.color">
</label>
<button @click="reset">重置</button>
</form>
<h4>
我的名字叫:<span v-text="name"></span>,我有一辆<span v-html="car.color"></span>的<span v-text="car.name"></span>
</h4>
</div>
<script>
/*
原理:数据劫持
思路:
1. 遍历数据,并利用 Object.defineProperty 进行数据劫持(根据键名);绑定订阅者,数据发生变化时统一更新。
2. 遍历编译 虚拟DOM 树,生成订阅者对象,绑定到数据对象上。
对象:
MyVm:模拟 Vue 对象
属性:
_el:根节点
_data:数据
_methods:方法
_property:数据与订阅者对应关系(存放订阅者对象)
功能:
_listen:遍历数据并劫持
_compile:编译生成订阅者
Watcher:订阅者对象
属性:
_el:需要操作的节点
_vm:vm对象
_attr:需要操作的DOM属性
_dataName:数据依赖
功能:
_update:更新订阅者
*/
class MyVm {
constructor({
el,
data,
methods
}) {
this._el = document.querySelector(el)
this._data = data
this._methods = methods
this._property = {}
this._listen(this._data, this._property)
this._compile(this._el)
}
_listen(data, parent) {
const that = this
Object.keys(data).map(key => {
if (data.hasOwnProperty(key)) {
let value = data[key]
// 绑定订阅者
parent[key] = {
_subscriber: []
}
// 数据劫持
Object.defineProperty(data, key, {
get() {
return value
},
set(newValue) {
value = newValue
// 更新订阅者
parent[key]._subscriber.map(watcher => watcher._update())
}
})
// 递归遍历
if (typeof value === 'object') {
this._listen(value, parent[key])
}
}
})
}
_compile(root) {
const nodeList = root.children
if (nodeList.length != 0) {
Object.values(nodeList).map(node => {
// 编译 v-html 属性
if (node.hasAttribute('v-html')) {
// 找到对应数据
const attr = node.getAttribute('v-html').split('.')
let _p = this._property
attr.map(a => {
_p = _p[a]
})
// 绑定订阅者
const watcher = new Watcher({
el: node,
vm: this,
attr: 'innerHTML',
dataName: attr
})
_p._subscriber.push(watcher)
}
// 编译 v-text 属性
if (node.hasAttribute('v-text')) {
// 找到对应数据
const attr = node.getAttribute('v-text').split('.')
let _p = this._property
attr.map(a => {
_p = _p[a]
})
// 绑定订阅者
const watcher = new Watcher({
el: node,
vm: this,
attr: 'innerText',
dataName: attr
})
_p._subscriber.push(watcher)
}
// 编译 v-model 属性
if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
// 找到对应数据
const attr = node.getAttribute('v-model').split('.')
let _p = this._property
attr.map(a => {
_p = _p[a]
})
// 绑定订阅者
const watcher = new Watcher({
el: node,
vm: this,
attr: 'value',
dataName: attr
})
_p._subscriber.push(watcher)
// 监听事件
node.addEventListener('input', () => {
let _d = this._data
attr.map((a, i) => {
if (i < attr.length - 1) {
_d = _d[a]
} else {
_d[a] = node.value
}
})
})
}
// 编译事件
Object.values(node.attributes).map(attr => {
if (attr.name.match(/^@.*/)) {
const type = attr.name.substring(1)
const method = node.getAttribute(attr.name)
// 绑定事件
node.addEventListener(type, this._methods[method].bind(this._data))
}
})
// 递归遍历
this._compile(node)
})
}
}
}
class Watcher {
constructor({
el,
vm,
attr,
dataName
}) {
this._el = el
this._vm = vm
this._attr = attr
this._dataName = dataName
// 加载默认数据
this._update()
}
_update() {
this._dataName.map(a => {
let _d = this._vm._data
this._dataName.map((item, index) => {
_d = _d[item]
})
this._el[this._attr] = _d
})
}
}
const app = new MyVm({
el: '.app',
data: {
name: '张三',
car: {
name: '宝马',
color: '<span style="color: red">红色</span>'
}
},
methods: {
reset() {
this.name = '张三'
this.car.name = '宝马'
this.car.color = '白色'
}
}
})
</script>
</body>
</html>
Vue 双向数据绑定原理(数据劫持和发布者-订阅者)
最新推荐文章于 2024-07-02 10:28:56 发布