大家都知道,关于Vue的双向绑定,核心是Object.defineProperty()方法,那接下来我们就简单介绍一下!
Object.defineProperty()
语法:Object.defineProperty(obj,prop,descriptor)
- obj——要在其上定义属性的对象。
- prop——要定义或修改的属性的名称。
- descriptor——将被定义或修改的属性描述符。
其实,简单点来说,就是通过此方法来定义一个值。调用,使用到了get方法,赋值,使用到了set方法。 例子:
let obj = {}
Object.defineProperty(obj, 'name', {
get: function() {
console.log('调用了get方法')
},
set: function(newVal) {
console.log('调用了set方法,方法的值是:' + newVal)
}
})
obj.name
obj.name = '吕小布'
当我们调用时候,就会自动打印出两行文字。注意:get 和 set 方法内部的 this 都指向 obj,这意味着 get 和 set 方法可以操作对象内部的值。另外,访问器属性(也就是Object.defineProperty()中的属性)的会”覆盖”同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。
实现JS双向绑定
既然我们已经知道了,每当有改变的时候都会调用到set方法,我们可以根据此来实现一个双向绑定!
<body>
<div id="app">
<input id="a" type="text">
<h1 id="b"></h1>
</div>
<script>
const domA = document.getElementById('a')
const domB = document.getElementById('b')
let obj = {}
let val = '吕'
Object.defineProperty(obj, 'name', {
get: function() {
console.log('get val:' + val)
return val
},
set: function(newVal) {
val = newVal
console.log('set val:' + val)
domA.value = val
domB.innerHTML = val
}
})
domA.addEventListener('keyup', function(e) {
obj.name = e.target.value
})
</script>
</body>
效果图: 此例实现的效果是:随文本框输入文字的变化,h1中会同步显示相同的文字内容;在js或控制台显式的修改 obj.name 的值,视图会同步更新。这样就实现了 model => view 以及 view => model 的双向绑定。通过添加事件监听keyup来触发调用set方法,而set在修改了访问器属性的同时,既改变了文本框的内容,也改变了h1标签内的文本。
实现Vue双向绑定
一、实现效果
我们真正想实现的双向绑定是这样的: view:
<div id="app">
<input v-model="text" type="text">
{{text}}
</div>
model:
let vm = new Vue({
el: 'app',
data: {
text: 'lvxiaobu'
}
})
我们的工作:
- 将vm实例中的data中的内容绑定到输入框以及文本节点当中
- 当输入框改变时,vm实例中的data的内容也跟着改变,实现 【view => model】
- 当data中的内容发生变化的时候,输入框的内容以及文本节点的内容也发生变化,实现 【model=> view】
二、内容绑定原理
先来了解一下DocumentFragment。 说到内容绑定,我们不得不来介绍DocuemntFragment(碎片化文档)这个概念,简单的来讲,你可以把它认为是一个dom节点的容器,当你创造了10个节点,当每个节点都插入到文档当中都会引发一次浏览器的回流,也就是说浏览器要回流10次,十分消耗资源。而使用碎片化文档,也就是说我把10个节点都先放入到一个容器当中,最后我再把容器直接插入到文档就可以了!浏览器只回流了1次。
注意:还有一个很重要的特性是,如果使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点。 举个例子,使用console.log(document.getElementById(‘app’)) 可以看到,我的app中有两个子节点,一个元素节点,一个文本节点 ;但是,当我通过DocumentFragment 劫持数据一下后:
const app = document.getElementById('app')
function nodeToFragment(node) {
let fragment = document.createDocumentFragment()
let child
while (child = node.firstChild) {
fragment.appendChild(child)
}
return fragment
}
let dom = nodeToFragment(app)
console.log(dom)
console.log(app)
注意:我的碎片化文档是将子节点都劫持了过来,而我的id为app的div内已经没有内容了。
同时要主要我while的判断条件。判断是否有子节点,因为我每次appendChild都把node中的第一个子节点劫持走了,node中就会少一个,直到没有的时候,child也就变成了undefined,也就终止了循环。
三、如何实现内容绑定
我们要考虑两个问题,一个是如何绑定到input上,另一个是如何绑定到文本节点中。
这样思路就来了,我们已经获取到了div的所有子节点了,就在DocumentFragment里面,然后对每一个节点进行处理,看是不是有跟vm实例中有关联的内容,如果有,修改这个节点的内容。然后重新添加入DocumentFragment中。
首先,我们写一个处理每一个节点的编译方法,如果有input绑定v-model属性或者有{{ xxx }}的文本节点出现,就进行内容替换,替换为vm实例中的data中的内容 :
function compile(node, vm) {
let reg = /\{\{(.*)\}\}/
if (node.nodeType === 1) {
let attr = node.attributes
for (let i = 0;i < attr.length;i++) {
if (attr[i].nodeName == 'v-model') {
let name = attr[i].nodeValue
node.value = vm.data[name]
node.removeAttribute('v-model')
}
}
}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
let name = RegExp.$1
name = name.trim()
node.nodeValue = vm.data[name]
}
}
}
然后,在向碎片化文档中添加节点时,每个节点都要处理一下:
function nodeToFragment(node, vm) {
let fragment = document.createDocumentFragment()
let child
while (child = node.firstChild) {
compile(child, vm)
fragment.appendChild(child)
}
return fragment
}
创建Vue的构造函数:
function Vue(options) {
this.data = options.data
let id = options.el
let dom = nodeToFragment(document.getElementById(id), this)
document.getElementById(id).appendChild(dom)
}
let vm = new Vue({
el: 'app',
data: {
text: 'lvxiaobu'
}
})
以上完整代码:
<body>
<div id="app">
<input v-model="text" type="text">
{{text}}
</div>
<script>
function compile(node, vm) {
let reg = /\{\{(.*)\}\}/
if (node.nodeType === 1) {
let attr = node.attributes
for (let i = 0;i < attr.length;i++) {
if (attr[i].nodeName == 'v-model') {
let name = attr[i].nodeValue
node.value = vm.data[name]
node.removeAttribute('v-model')
}
}
}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
let name = RegExp.$1
name = name.trim()
node.nodeValue = vm.data[name]
}
}
}
function nodeToFragment(node, vm) {
let fragment = document.createDocumentFragment()
let child
while (child = node.firstChild) {
compile(child, vm)
fragment.appendChild(child)
}
return fragment
}
function Vue(options) {
this.data = options.data
let id = options.el
let dom = nodeToFragment(document.getElementById(id), this)
document.getElementById(id).appendChild(dom)
}
let vm = new Vue({
el: 'app',
data: {
text: 'lvxiaobu'
}
})
</script>
</body>
效果图: 我们成功将内容都绑定到了输入框与文本节点上!
四、view => model
|