Vue中的双向绑定,让开发者能够更加集中精力于业务逻辑而不是操作dom。
实现数据响应式的原理主要有Object.defineProperty为对象设置存取器属性,以及ES6的proxy对象代理。
这里我通过proxy实现了简单的数据响应式。
数据渲染
<div id="app">
<div>
{{msg1}}
</div>
{{msg1}} ~~ {{msg2}}
<form>
<input type="text" v-model="msg1">
</form>
</div>
<script>
var data = {
msg1: 'hello world',
msg2: 'bye world'
}
var vm = new MyVue ({
el: '#app',
data
})
vm.data.msg2 = 'ok'
</script>
我们想通过将data中的数据渲染到页面上,即将html中的标签模版替换为对应的数据,这部分通过正则表达式即可实现。
具体来说,对div#app遍历子节点,文本节点即替换,元素节点就递归遍历子节点。
同时对元素节点,判断v-model属性是否存在,若存在,则设置其value为对应的数据。
数据单向绑定
单向绑定就是,如果在代码中改变了数据的值,那么页面立即重新渲染对应的元素。如上面代码中页面渲染后,执行代码vm.data.msg2 = 'ok'
,那么会自动更新页面内容。
简单说就是 data -> DOM 的绑定。
如何实现呢?可以分为两步:
- 监听到数据的变化
- 数据变化后,相应节点的重新渲染
监听数据变化可以通过Object.defineProperty,但这里采用了proxy的方式数据对象创建代理。
observe (data) {
return new Proxy (data, {
set: (target, prop, newValue) => {
console.log('数据正在被修改:', prop, newValue)
return Reflect.set(target, prop, newValue)
}
})
}
监听到数据变化后如何让相应的元素立即重新渲染呢?我是这样考虑的:
在第一次数据渲染的同时,为每个node注册自定义事件’dataChanged’,可以通过事件对象e.detail.newValue获取到新设定的值。对于文本节点,则用正则替换内容,元素节点则根据v-model属性中的值设定其value。
// 文本节点 注册自定义事件
node.addEventListener('dataChanged', (e) => {
node.textContent = templateText.replace(reg, (match, capture1) => this.data[capture1])
})
// 元素节点 注册自定义事件
node.addEventListener('dataChanged', (e) => {
node.value = e.detail.newValue
})
注册了事件,我们只需要在监听到数据变化后,为相关节点手动触发(分派)事件就行了。比如监听到msg1数据变化了,就需要为所有涉及到msg1数据的节点触发事件。 所以要实现这个,还需要在第一次数据渲染的同时也将某个数据存在于哪些节点中记录下来。
if (this.prop2nodes[prop]) this.prop2nodes[prop].push(node)
else this.prop2nodes[prop] = [node]
这样,在监听到数据变化后就可以,获得被修改的数据涉及的节点,同时触发自定义事件。
var changeDataEvent = new CustomEvent ('dataChanged', {
detail: {
newValue
}
})
var nodesRelated = this.prop2nodes[prop]
for (var node of nodesRelated) {
node.dispatchEvent(changeDataEvent)
}
双向绑定
完成了data->DOM的绑定后,DOM->data的绑定就简单许多。
有v-model属性的元素,监听’input’事件,每次改变就讲其value赋值给data。
var prop = node.getAttribute('v-model')
if (prop) {
// 数据渲染
node.value = this.data[prop]
// 用户通过输入框修改,将修改的值与数据绑定
node.addEventListener('input', (e) => {
this.data[prop] = node.value
})
}
整体效果
来看看效果:
完整代码
html 部分
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title></title>
<script src="./myVue.js"></script>
</head>
<body>
<div id="app">
<div>
{{msg1}}
</div>
{{msg1}} ~~ {{msg2}}
<form>
<input type="text" v-model="msg1">
</form>
</div>
<script>
var data = {
msg1: 'hello world',
msg2: 'bye world'
}
var vm = new MyVue ({
el: '#app',
data
})
vm.data.msg2 = 'ok'
</script>
</body>
</html>
javascript部分 – myVue.js
class MyVue {
constructor (option) {
this.option = option
// proxy 对象代理,监听数据的改变
this.data = this.observe(this.option.data)
// 保存数据对应的节点
this.prop2nodes = {}
this.el = document.querySelector(this.option.el)
// 渲染数据
this.compileNode(this.el)
}
observe (data) {
return new Proxy (data, {
// 监听到数据被修改
set: (target, prop, newValue) => {
// 通过 Reflect 设置数据
Reflect.set(target, prop, newValue)
// 新建 CustomEvent,detail中存放事件相关的数据,这里放了数据被修改后的新值
var changeDataEvent = new CustomEvent ('dataChanged', {
detail: {
newValue
}
})
// 获取所有设计到该数据的节点
var nodesRelated = this.prop2nodes[prop]
// 为每个节点手动触发'dataChanged'事件, 事件对象为 changeDataEvent
for (var node of nodesRelated) {
node.dispatchEvent(changeDataEvent)
}
return true
}
})
}
compileNode (el) {
var childNodes = el.childNodes
;[... childNodes].forEach((node) => {
// 文本节点
if (node.nodeType === 3) {
var reg = /\{\{([^{}]+)\}\}/g
var templateText = node.textContent
// 数据渲染,通过正则替换
node.textContent = templateText.replace(reg, (match, capture1) => this.data[capture1])
// 将该节点涉及的数据保存到 prop2nodes 中
var execResult, prop
while (execResult = reg.exec(templateText)) {
prop = execResult[1]
if (this.prop2nodes[prop]) this.prop2nodes[prop].push(node)
else this.prop2nodes[prop] = [node]
}
// 为该节点注册'dataChanged'事件, 通过事件对象能拿到数据被修改后的新值
node.addEventListener('dataChanged', (e) => {
// 数据修改后,正则替换,重新渲染
node.textContent = templateText.replace(reg, (match, capture1) => this.data[capture1])
})
// 元素节点
} else if (node.nodeType === 1) {
var prop = node.getAttribute('v-model')
// 如果设置了v-model属性
if (prop) {
// 渲染数据
node.value = this.data[prop]
// 将该节点涉及的数据保存到 prop2nodes 中
if (this.prop2nodes[prop]) this.prop2nodes[prop].push(node)
else this.prop2nodes[prop] = [node]
// 注册 dataChanged 事件,数据被修改后,重新设置该元素节点的value
node.addEventListener('dataChanged', (e) => {
node.value = e.detail.newValue
})
// 注册 input 事件,监听用户修改数据。实现 Dom -> data 的绑定
node.addEventListener('input', (e) => {
this.data[prop] = node.value
})
}
// 递归遍历该元素节点的子节点
this.compileNode(node)
}
})
}
}