vue2.x双向绑定原理
vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。
一、Object.defineProperty
用于在对象上定义一个新属性,或者修改对象现有属性,并返回此对象。
语法:
Object.defineProperty(obj, prop, descriptor)
参数:
- obj:必需。目标对象
- prop:必需。需定义或修改的属性的名字
- descriptor:必需。目标属性所拥有的特性
返回值:
传入函数的对象,即第一个参数obj
属性:
- get:获取值时调用的方法
- set:设置值时调用的方法
举例:
let number = 18
let person = {
name:'张三',
sex:'男'
}
Object.defineProperty(person,'age',{
value:18,
enumerable:true, //控制属性是否可以枚举,默认值是false
writable:true, //控制属性是否可以被修改,默认值是false
configurable:true //控制属性是否可以被删除,默认值是false
//当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
get(){
console.log('将要读取person.age属性')
return number
},
//当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
set(value){
console.log('修改person.age属性,新值为',value)
number = value
}
})
console.log(Object.keys(person))
console.log(person)
二、JS的双向绑定
每当值改变的时候都会调用set方法,可以重写set方法,来实现js的双向绑定
<body>
<div id="app">
<input id="a" type="text">
<p id="b"></p>
</div>
<script>
const domA = document.getElementById('a')
const domB = document.getElementById('b')
let obj = {}
let val = 'new'
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>
修改输入框内容,或手动更改obj.name的值时,p标签的内容也会随即显示相同的内容。
三、DocuemntFragment
文档片段,一个没有父对象的最小文档对象。
当我们需要批量的向目标 DOM 中插入大量的节点或内容时,使用传统方式会多次触发回流和重绘(每次插入数据后,页面会立即反映出这个变化),影响页面性能,此时就可以考虑使用 DocumentFragment ,把所有的新节点附加其上,然后把文档碎片的内容一次性添加到 document 中。相比传统操作,这个操作仅发生一个重渲染。
vue进行编译时,就是将挂载目标的所有子节点劫持到DocumentFragment中,经过一番处理之后,再将DocumentFragment整体返回插入挂载目标。
实现变量绑定到input和文本节点上
- 处理每一个节点的编译方法,如果有input绑定v-model属性或者有
{{ xxx }}的文本节点出现,就进行内容替换,替换为vm实例中的data中的内容 - 在向DocumentFragmen中添加节点时,每个节点都要按照上述方法处理一下
- 创建Vue的构造函数
四、view => model
通过改写set方法,使页面上的输入框改变值,vm中的data需要获取最新的value
在input的input、keyup、change事件中获取输入框的新值,利用Object.defineProperty将新值赋值给vm.text,把vm实例中的data下的text通过Object.defineProperty设置为访问器属性,这样给vm.text赋值,就触发了set。
并且实现一个观察者,对于一个实例每一个属性值都进行观察。
五、model=> view
通过修改vm实例的属性,该改变输入框的内容与文本节点的内容。
页面中可能多处用到 data中的属性,这是1对多的。也就是说,改变1个model的值可以改变多个view中的值。
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作
完整代码
<body>
<div id="app">
<input v-model="text" type="text">
{{text}}
</div>
<script>
// 编译方法
function compile(node, vm) {
let reg = /\{\{(.*)\}\}/ // 匹配{{ xxx }}中的xxx
// 如果是元素节点
if (node.nodeType === 1) {
// attributes包含了元素的所有属性
let attr = node.attributes
// 解析元素节点的所有属性
for (let i = 0;i < attr.length;i++) {
if (attr[i].nodeName == 'v-model') {
let name = attr[i].nodeValue // 看看是与哪一个数据绑定
node.addEventListener('input', function(e) {
vm[name] = e.target.value // 将实例的text修改为最新值
// 实时更新的是vm的访问器属性text,vm.data[text]并不会更新
console.log(vm)
})
// node.value = vm[name] // 将data的值赋给该节点
new Watcher(vm, node, name) // 不直接赋值,而是通过绑定一个订阅者。vm变,输入框数据跟着变
node.removeAttribute('v-model')
}
}
}
// 如果是文本节点
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
// 获取到匹配的字符串xxx
let name = RegExp.$1
name = name.trim()
// node.nodeValue = vm[name] // 将vm[text]的值赋给该节点
new Watcher(vm, node, name) // 不直接赋值,而是通过绑定一个订阅者
// console.log(new Watcher(vm, node, name))
}
}
}
// 把app的所有子节点都劫持过来,在向碎片化文档中添加节点时,每个节点都处理一下
function nodeToFragment(node, vm) {
let fragment = document.createDocumentFragment()
let child
// 每次循环都是先把node.firstChild赋值给child,然后当child非空时,才会进入循环,并不是根据两者是否相等
while (child = node.firstChild) {
compile(child, vm)
fragment.appendChild(child)
}
return fragment
}
// 响应式监听属性的方法
function defindeReactive(obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
get: function() {
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set: function(newVal) {
if (newVal === val) return
val = newVal
console.log(`新值:${val}`)
// 一旦更新马上发布消息
dep.notify()
}
})
}
// 观察者方法
function observe(obj, vm) {
for (let key of Object.keys(obj)) {
defindeReactive(vm, key, obj[key])
}
}
// Watcher构造函数
function Watcher(vm, node, name) {
Dep.target = this // Dep.target是一个全局变量
this.vm = vm
this.node = node
this.name = name
this.update()
Dep.target = null
}
Watcher.prototype = {
update() {
this.get()
this.node.nodeValue = this.value // 注意,这是更新节点内容的关键
this.node.value = this.value // 更新输入框数据的关键。vm变,输入框数据跟着变
},
get() {
this.value = this.vm[this.name] // 触发相应的get
}
}
// Dep构造函数
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub)
},
notify() {
// 执行所有订阅者的回调函数update
this.subs.map(item => {
item.update()
})
}
}
// Vue构造函数
function Vue(options) {
this.data = options.data
let data = this.data
// console.log(this) // this指向vm,vm = {data: {text: 'lvxiaobu'}},仅有data一个属性,vm != options
// vm还有额外的隐式属性值为options.data里面的所有属性值text: 'lvxiaobu',方便get和set方法进行双向绑定
observe(data, this)
let id = options.el
// 这个this也就是实例化对象本身
let dom = nodeToFragment(document.getElementById(id), this)
// 处理完所有DOM节点后,重新将内容添加回去
document.getElementById(id).appendChild(dom)
}
// 实例化Vue
let vm = new Vue({
el: 'app',
data: {
text: 'lvxiaobu'
}
})
</script>
</body>