双向绑定,其实就是V改变,M跟着改,M改变,V跟着改
这是一个最普通的双向绑定的例子,输入框的值改变,下面的文本也跟着改变,这里面的过程大概是这样的,输入框改变---》Model改变---》改变依赖于改变的Model的Dom
具体场景很简单,那么我们应该怎么做呢?其实双向绑定的场景很简单,就是VchangeM,MchangeV,V改变M,我们可以监听各个dom的事件,比如input,click,touch等,主要实现的是怎么去实现M改变V
如上图所示,当我们修改输入框的值时,要同时修改红色的字,由以前的文章我们知道,可以使用defineProperty来监听设置和获取数据的操作,那么,我们只需要在设置数据时,通知依赖于这个Model值的dom进行改变即可(理论上可能有成千上万个),由上文我们知道,实现一个最简单的例子就是在set时,同时去操作dom
1.simple demo
"use strict"
var obj = {}
var txtTest = document.querySelector('input')
var divTest = document.querySelector('div')
Object.defineProperty(obj, 'key1', {
set: (newValue) => {
divTest.innerHTML = newValue
txtTest.value = newValue
return newValue
},
get: () => {
}
})
txtTest.addEventListener('input',()=>{
obj.key1 = txtTest.value
})
但是这只能适用简单的场景,数据结构肯定是变的,dom肯定也是变的,我们需要定义一套规则来把数据绑定到dom上,本demo中,用到的是v-model
2.observer
function reDefineObj(obj) {
if (!obj || Object.prototype.toString.call(obj).toLowerCase() != '[object object]') {
return
}
Object.keys(obj).forEach((key) => {
// 取值,并且把这个值存起来,要是在defineProperty后再取值,会陷入死循环
var value = obj[key]
Object.defineProperty(obj, key, {
set: (newVal) => {
if (value != newVal) {
value = newVal
}
// 设置值时,需要通知所有的订阅者,即那些用模板语言定义的属性,demo中用的{{data}}这种
Dep.notify()
},
get: () => {
// 此处的会在新建watch时做一个判断,如果有,则add,没有就不add
if (Dep.target) {
Dep.addSub(Dep.target)
}
return value
}
})
// 递归
reDefineObj(value)
})
return obj
}
上面的Dep是一个订阅者容器,它包含了所有需要监听的对象,也就是那些自定义了绑定属性的Dom,如vue中的v-model或者双括弧绑定,由此可见,我们可以在解析dom时,遇到带有v-model属性的dom,新增一个订阅者,那么,当通知这些订阅者时,算出这个dom依赖的v-model值,在重新对dom赋值即可
3.Dep
var Dep = {
subs: [],
addSub: function(sub) {
Dep.subs.push(sub)
},
notify: function() {
Dep.subs.forEach((item) => {
item.update()
})
},
target: null
}
Dep只是进行了简单的新增订阅者和通知订阅者的功能
4.Watch
function Watcher(vm, exp, cb) {
this.cb = cb;
this.exp = exp;
this.vm = vm
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.value = setValueByExp(this, this.exp);
this.cb && this.cb(this.value)
},
get: function() {
Dep.target = this; // 缓存自己
var value = setValueByExp(this, this.exp); // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
这就是订阅者自己的一个简单实现,初始化时,去调用了依赖属性的get操作,那么就会主动把当前这个订阅者加到订阅者列表中了
5.Compiler
在我们的demo中,我们使用了v-model来绑定数据,这只是我们为了实现双向绑定的一个手段,它并不是标准的dom属性,所以需要将v-model编译成标准的dom,这里用到了createElementFragment和递归
// 把当前dom下的元素全部放到临时的dom树中
nodeToFragment: function(el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild
}
return fragment;
}
转成fragment之后,就需要编译节点了,本demo中只处理nodeType=1的节点
// 根据指令编译Element,有属性和事件,编译结束,删除指令(本示例只对绑定v-model指令进行处理)
compile: function(node) {
var attr = node.getAttribute('v-model');
var self = this
if (attr) {
attr = /\{\{(.*)\}\}/.exec(attr)[1]
var val = setValueByExp(self, attr)
node.innerText = val
new Watcher(vm, attr, function(value) {
node.innerText = value || ''
})
node.removeAttribute('v-model')
}
}
由代码可见,我们获取了v-model这个属性,它一般是一个对数据对象的链式调用,如{{obj.parent.name}},双括弧是参照vue,然后根据这个属性从数据对象中找到对应的值,对使用该值的dom对象进行赋值,再加上订阅器即可,执行完成功之后,删除该属性
dom结构
<div id="app">
<input /><br/>
你好,<span v-model={{name}}></span><br/>
热烈欢迎<span v-model={{name}}></span>来到我们的网站
</div>
初始化
var obj = {
name: '张三',
parent: {
name: '爸爸',
parent: {
name: '爷爷',
parent: {
name: '老祖'
}
}
}
}
var vm = {
data: reDefineObj(obj)
}
new Compile('#app', vm)
document.querySelector('input').addEventListener('input', function() {
obj.name = event.target.value
})
6.总结
其实,双向绑定,主要是实现M到V的自动绑定,而不用我们再去选择dom再进行操作,在绑定的的过程中,我们采用了观察者模式,即数据改变,依赖于数据的所有dom都进行改变
参考资料:https://www.cnblogs.com/libin-1/p/6893712.html